summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrzegorz Bizon <grzesiek.bizon@gmail.com>2017-11-30 12:52:04 +0100
committerGrzegorz Bizon <grzesiek.bizon@gmail.com>2017-11-30 12:52:04 +0100
commit5df4ba0a93fd266105ccf35fd49fa18e6403c15b (patch)
tree57d32dbd0af5d54643060faba9ab6ba993357a1f
parent1486950bc9396f9b8384763d68f36387326eb745 (diff)
parentfeece7713247a063bfa71ab701f8a164e6fa71bb (diff)
downloadgitlab-ce-5df4ba0a93fd266105ccf35fd49fa18e6403c15b.tar.gz
Merge branch 'master' into backstage/gb/build-pipeline-in-a-separate-class
* master: (1794 commits)
-rw-r--r--.gitlab-ci.yml192
-rw-r--r--.gitlab/issue_templates/Feature Proposal.md43
-rw-r--r--.gitlab/merge_request_templates/Database Changes.md46
-rw-r--r--.gitlab/route-map.yml3
-rw-r--r--.nvmrc2
-rw-r--r--.rubocop.yml4
-rw-r--r--.ruby-version2
-rw-r--r--.scss-lint.yml9
-rw-r--r--CHANGELOG.md505
-rw-r--r--CONTRIBUTING.md30
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION3
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile20
-rw-r--r--Gemfile.lock73
-rw-r--r--MAINTENANCE.md34
-rw-r--r--PROCESS.md40
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/auth_buttons/signin_with_google.pngbin3983 -> 8001 bytes
-rw-r--r--app/assets/images/emoji.pngbin1218558 -> 1219696 bytes
-rw-r--r--app/assets/images/emoji/gay_pride_flag.pngbin0 -> 2340 bytes
-rw-r--r--app/assets/images/emoji/mrs_claus.pngbin2206 -> 3338 bytes
-rw-r--r--app/assets/images/emoji/speech_left.pngbin0 -> 390 bytes
-rw-r--r--app/assets/images/emoji@2x.pngbin2976505 -> 2977099 bytes
-rw-r--r--app/assets/images/icons.json2
-rw-r--r--app/assets/images/icons.svg2
-rw-r--r--app/assets/images/illustrations/clusters_empty.svg1
-rw-r--r--app/assets/images/illustrations/convdev/convdev_no_data.svg1
-rw-r--r--app/assets/images/illustrations/convdev/convdev_no_index.svg1
-rw-r--r--app/assets/images/illustrations/convdev/convdev_overview.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_1.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_10.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_2.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_3.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_4.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_5.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_6.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_7.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_8.svg1
-rw-r--r--app/assets/images/illustrations/convdev/i2p_step_9.svg1
-rw-r--r--app/assets/images/illustrations/epics.svg1
-rw-r--r--app/assets/images/illustrations/gitlab_logo.svg1
-rw-r--r--app/assets/images/illustrations/logos/go_logo.svg1
-rw-r--r--app/assets/images/illustrations/logos/mattermost_logo.svg1
-rw-r--r--app/assets/images/illustrations/monitoring/getting_started.svg2
-rw-r--r--app/assets/images/illustrations/monitoring/loading.svg2
-rw-r--r--app/assets/images/illustrations/monitoring/unable_to_connect.svg2
-rw-r--r--app/assets/images/illustrations/no_commits.svg1
-rw-r--r--app/assets/images/illustrations/pipelines_pending.svg1
-rw-r--r--app/assets/images/illustrations/slack_logo.svg1
-rw-r--r--app/assets/images/illustrations/welcome/add_new_group.svg1
-rw-r--r--app/assets/images/illustrations/welcome/add_new_project.svg1
-rw-r--r--app/assets/images/illustrations/welcome/add_new_user.svg1
-rw-r--r--app/assets/images/illustrations/welcome/configure_server.svg1
-rw-r--r--app/assets/images/illustrations/welcome/ee_trial.svg1
-rw-r--r--app/assets/images/illustrations/welcome/globe.svg1
-rw-r--r--app/assets/images/illustrations/welcome/lightbulb.svg1
-rw-r--r--app/assets/images/illustrations/wiki-fro-logged-out-users.svg1
-rw-r--r--app/assets/javascripts/abuse_reports.js4
-rw-r--r--app/assets/javascripts/api.js15
-rw-r--r--app/assets/javascripts/autosave.js31
-rw-r--r--app/assets/javascripts/awards_handler.js2
-rw-r--r--app/assets/javascripts/behaviors/autosize.js6
-rw-r--r--app/assets/javascripts/behaviors/copy_as_gfm.js481
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js73
-rw-r--r--app/assets/javascripts/behaviors/index.js4
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js5
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js3
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js3
-rw-r--r--app/assets/javascripts/blob/viewer/index.js2
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js3
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js54
-rw-r--r--app/assets/javascripts/boards/components/board_card.js67
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue71
-rw-r--r--app/assets/javascripts/boards/components/board_list.js2
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js20
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js5
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js2
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js9
-rw-r--r--app/assets/javascripts/boards/models/issue.js13
-rw-r--r--app/assets/javascripts/boards/services/board_service.js14
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js8
-rw-r--r--app/assets/javascripts/broadcast_message.js45
-rw-r--r--app/assets/javascripts/build.js289
-rw-r--r--app/assets/javascripts/build_artifacts.js34
-rw-r--r--app/assets/javascripts/build_variables.js16
-rw-r--r--app/assets/javascripts/ci_lint_editor.js7
-rw-r--r--app/assets/javascripts/clusters.js112
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js221
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue185
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue114
-rw-r--r--app/assets/javascripts/clusters/constants.js12
-rw-r--r--app/assets/javascripts/clusters/event_hub.js3
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js20
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js68
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue6
-rw-r--r--app/assets/javascripts/commits.js55
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/contextual_sidebar.js81
-rw-r--r--app/assets/javascripts/copy_as_gfm.js475
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js74
-rw-r--r--app/assets/javascripts/create_label.js3
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js456
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js3
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js4
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue2
-rw-r--r--app/assets/javascripts/diff.js15
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js2
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js2
-rw-r--r--app/assets/javascripts/dispatcher.js197
-rw-r--r--app/assets/javascripts/droplab/constants.js2
-rw-r--r--app/assets/javascripts/droplab/drop_down.js29
-rw-r--r--app/assets/javascripts/droplab/hook.js2
-rw-r--r--app/assets/javascripts/droplab/plugins/filter.js2
-rw-r--r--app/assets/javascripts/droplab/utils.js2
-rw-r--r--app/assets/javascripts/dropzone_input.js560
-rw-r--r--app/assets/javascripts/due_date_select.js52
-rw-r--r--app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js16
-rw-r--r--app/assets/javascripts/emoji/support/unicode_support_map.js21
-rw-r--r--app/assets/javascripts/environments/components/container.vue71
-rw-r--r--app/assets/javascripts/environments/components/empty_state.vue42
-rw-r--r--app/assets/javascripts/environments/components/environment.vue268
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue3
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue34
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue3
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue4
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue128
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue16
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js35
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js31
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue255
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js167
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js7
-rw-r--r--app/assets/javascripts/files_comment_button.js9
-rw-r--r--app/assets/javascripts/filterable_list.js7
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js7
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js7
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js5
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js10
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js13
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js21
-rw-r--r--app/assets/javascripts/flash.js154
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js3
-rw-r--r--app/assets/javascripts/gl_dropdown.js3
-rw-r--r--app/assets/javascripts/gl_field_error.js5
-rw-r--r--app/assets/javascripts/gl_field_errors.js36
-rw-r--r--app/assets/javascripts/gl_form.js170
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js3
-rw-r--r--app/assets/javascripts/group_avatar.js31
-rw-r--r--app/assets/javascripts/group_label_subscription.js11
-rw-r--r--app/assets/javascripts/groups/components/app.vue194
-rw-r--r--app/assets/javascripts/groups/components/group_folder.vue38
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue228
-rw-r--r--app/assets/javascripts/groups/components/groups.vue26
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue95
-rw-r--r--app/assets/javascripts/groups/components/item_caret.vue25
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue98
-rw-r--r--app/assets/javascripts/groups/components/item_type_icon.vue34
-rw-r--r--app/assets/javascripts/groups/constants.js35
-rw-r--r--app/assets/javascripts/groups/groups_filterable_list.js64
-rw-r--r--app/assets/javascripts/groups/index.js196
-rw-r--r--app/assets/javascripts/groups/new_group_child.js62
-rw-r--r--app/assets/javascripts/groups/service/groups_service.js42
-rw-r--r--app/assets/javascripts/groups/services/groups_service.js38
-rw-r--r--app/assets/javascripts/groups/store/groups_store.js105
-rw-r--r--app/assets/javascripts/groups/stores/groups_store.js167
-rw-r--r--app/assets/javascripts/groups_select.js185
-rw-r--r--app/assets/javascripts/header.js23
-rw-r--r--app/assets/javascripts/importer_status.js144
-rw-r--r--app/assets/javascripts/init_changes_dropdown.js4
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js9
-rw-r--r--app/assets/javascripts/init_legacy_filters.js11
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js4
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js3
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js11
-rw-r--r--app/assets/javascripts/issuable_context.js96
-rw-r--r--app/assets/javascripts/issuable_form.js197
-rw-r--r--app/assets/javascripts/issuable_index.js201
-rw-r--r--app/assets/javascripts/issue.js12
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue52
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue16
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue10
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue16
-rw-r--r--app/assets/javascripts/issue_show/components/fields/title.vue8
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue23
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue56
-rw-r--r--app/assets/javascripts/issue_status_select.js57
-rw-r--r--app/assets/javascripts/job.js283
-rw-r--r--app/assets/javascripts/jobs/components/header.vue10
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js2
-rw-r--r--app/assets/javascripts/jobs/job_details_mediator.js14
-rw-r--r--app/assets/javascripts/jobs/services/job_service.js9
-rw-r--r--app/assets/javascripts/label_manager.js209
-rw-r--r--app/assets/javascripts/labels.js75
-rw-r--r--app/assets/javascripts/labels_select.js831
-rw-r--r--app/assets/javascripts/layout_nav.js6
-rw-r--r--app/assets/javascripts/lazy_loader.js15
-rw-r--r--app/assets/javascripts/lib/utils/axios_utils.js22
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js73
-rw-r--r--app/assets/javascripts/lib/utils/datefix.js33
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js19
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js28
-rw-r--r--app/assets/javascripts/lib/utils/poll.js12
-rw-r--r--app/assets/javascripts/lib/utils/sticky.js8
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js153
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js241
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js8
-rw-r--r--app/assets/javascripts/logo.js8
-rw-r--r--app/assets/javascripts/main.js88
-rw-r--r--app/assets/javascripts/member_expiration_date.js94
-rw-r--r--app/assets/javascripts/members.js125
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js2
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js2
-rw-r--r--app/assets/javascripts/merge_request.js3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js18
-rw-r--r--app/assets/javascripts/milestone.js88
-rw-r--r--app/assets/javascripts/milestone_select.js21
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue25
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue12
-rw-r--r--app/assets/javascripts/monitoring/services/monitoring_service.js13
-rw-r--r--app/assets/javascripts/monitoring/utils/measurements.js8
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js49
-rw-r--r--app/assets/javascripts/namespace_select.js134
-rw-r--r--app/assets/javascripts/network/network_bundle.js2
-rw-r--r--app/assets/javascripts/new_branch_form.js168
-rw-r--r--app/assets/javascripts/new_commit_form.js54
-rw-r--r--app/assets/javascripts/new_sidebar.js81
-rw-r--r--app/assets/javascripts/notes.js43
-rw-r--r--app/assets/javascripts/notes/components/issue_comment_form.vue15
-rw-r--r--app/assets/javascripts/notes/components/issue_discussion.vue8
-rw-r--r--app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue17
-rw-r--r--app/assets/javascripts/notes/components/issue_note.vue9
-rw-r--r--app/assets/javascripts/notes/components/issue_note_awards_list.vue3
-rw-r--r--app/assets/javascripts/notes/components/issue_notes_app.vue12
-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.vue54
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js3
-rw-r--r--app/assets/javascripts/notes/stores/actions.js10
-rw-r--r--app/assets/javascripts/notifications_dropdown.js2
-rw-r--r--app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue26
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue14
-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/navigation_tabs.vue90
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue164
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue23
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue12
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js3
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js3
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediatior.js3
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue2
-rw-r--r--app/assets/javascripts/profile/profile.js2
-rw-r--r--app/assets/javascripts/project.js249
-rw-r--r--app/assets/javascripts/project_avatar.js31
-rw-r--r--app/assets/javascripts/project_find_file.js3
-rw-r--r--app/assets/javascripts/project_import.js17
-rw-r--r--app/assets/javascripts/project_label_subscription.js79
-rw-r--r--app/assets/javascripts/project_new.js272
-rw-r--r--app/assets/javascripts/project_select.js130
-rw-r--r--app/assets/javascripts/project_show.js11
-rw-r--r--app/assets/javascripts/project_variables.js60
-rw-r--r--app/assets/javascripts/projects/ci_cd_settings_bundle.js19
-rw-r--r--app/assets/javascripts/projects/project_new.js40
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_item.vue25
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js6
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js5
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js5
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue2
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue9
-rw-r--r--app/assets/javascripts/registry/stores/actions.js6
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js2
-rw-r--r--app/assets/javascripts/render_gfm.js20
-rw-r--r--app/assets/javascripts/render_math.js75
-rw-r--r--app/assets/javascripts/render_mermaid.js32
-rw-r--r--app/assets/javascripts/repo/components/commit_sidebar/list.vue89
-rw-r--r--app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue35
-rw-r--r--app/assets/javascripts/repo/components/commit_sidebar/list_item.vue36
-rw-r--r--app/assets/javascripts/repo/components/new_branch_form.vue108
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/index.vue84
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/modal.vue98
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/upload.vue68
-rw-r--r--app/assets/javascripts/repo/components/repo.vue83
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue262
-rw-r--r--app/assets/javascripts/repo/components/repo_edit_button.vue75
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue162
-rw-r--r--app/assets/javascripts/repo/components/repo_file.vue198
-rw-r--r--app/assets/javascripts/repo/components/repo_file_buttons.vue61
-rw-r--r--app/assets/javascripts/repo/components/repo_file_options.vue25
-rw-r--r--app/assets/javascripts/repo/components/repo_loading_file.vue96
-rw-r--r--app/assets/javascripts/repo/components/repo_prev_directory.vue56
-rw-r--r--app/assets/javascripts/repo/components/repo_preview.vue49
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue140
-rw-r--r--app/assets/javascripts/repo/components/repo_tab.vue70
-rw-r--r--app/assets/javascripts/repo/components/repo_tabs.vue49
-rw-r--r--app/assets/javascripts/repo/helpers/monaco_loader_helper.js25
-rw-r--r--app/assets/javascripts/repo/helpers/repo_helper.js279
-rw-r--r--app/assets/javascripts/repo/index.js118
-rw-r--r--app/assets/javascripts/repo/lib/common/disposable.js14
-rw-r--r--app/assets/javascripts/repo/lib/common/model.js56
-rw-r--r--app/assets/javascripts/repo/lib/common/model_manager.js32
-rw-r--r--app/assets/javascripts/repo/lib/decorations/controller.js43
-rw-r--r--app/assets/javascripts/repo/lib/diff/controller.js71
-rw-r--r--app/assets/javascripts/repo/lib/diff/diff.js30
-rw-r--r--app/assets/javascripts/repo/lib/diff/diff_worker.js10
-rw-r--r--app/assets/javascripts/repo/lib/editor.js79
-rw-r--r--app/assets/javascripts/repo/lib/editor_options.js2
-rw-r--r--app/assets/javascripts/repo/mixins/repo_mixin.js17
-rw-r--r--app/assets/javascripts/repo/services/index.js44
-rw-r--r--app/assets/javascripts/repo/services/repo_service.js82
-rw-r--r--app/assets/javascripts/repo/stores/actions.js145
-rw-r--r--app/assets/javascripts/repo/stores/actions/branch.js20
-rw-r--r--app/assets/javascripts/repo/stores/actions/file.js110
-rw-r--r--app/assets/javascripts/repo/stores/actions/tree.js162
-rw-r--r--app/assets/javascripts/repo/stores/getters.js40
-rw-r--r--app/assets/javascripts/repo/stores/index.js15
-rw-r--r--app/assets/javascripts/repo/stores/mutation_types.js30
-rw-r--r--app/assets/javascripts/repo/stores/mutations.js61
-rw-r--r--app/assets/javascripts/repo/stores/mutations/branch.js9
-rw-r--r--app/assets/javascripts/repo/stores/mutations/file.js54
-rw-r--r--app/assets/javascripts/repo/stores/mutations/tree.js27
-rw-r--r--app/assets/javascripts/repo/stores/repo_store.js203
-rw-r--r--app/assets/javascripts/repo/stores/state.js24
-rw-r--r--app/assets/javascripts/repo/stores/utils.js127
-rw-r--r--app/assets/javascripts/search.js2
-rw-r--r--app/assets/javascripts/search_autocomplete.js16
-rw-r--r--app/assets/javascripts/settings_panels.js45
-rw-r--r--app/assets/javascripts/shortcuts.js224
-rw-r--r--app/assets/javascripts/shortcuts_blob.js3
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js56
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js157
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js51
-rw-r--r--app/assets/javascripts/shortcuts_network.js37
-rw-r--r--app/assets/javascripts/shortcuts_wiki.js2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js3
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue31
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue31
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue125
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue26
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue46
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue64
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js6
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js5
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js34
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js19
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js22
-rw-r--r--app/assets/javascripts/smart_interval.js29
-rw-r--r--app/assets/javascripts/star.js46
-rw-r--r--app/assets/javascripts/subscription.js45
-rw-r--r--app/assets/javascripts/subscription_select.js49
-rw-r--r--app/assets/javascripts/task_list.js3
-rw-r--r--app/assets/javascripts/test_utils/index.js2
-rw-r--r--app/assets/javascripts/test_utils/simulate_input.js23
-rw-r--r--app/assets/javascripts/two_factor_auth.js3
-rw-r--r--app/assets/javascripts/u2f/authenticate.js188
-rw-r--r--app/assets/javascripts/u2f/error.js43
-rw-r--r--app/assets/javascripts/u2f/register.js151
-rw-r--r--app/assets/javascripts/u2f/util.js15
-rw-r--r--app/assets/javascripts/users/index.js8
-rw-r--r--app/assets/javascripts/users_select.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js90
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue104
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js2
-rw-r--r--app/assets/javascripts/vue_shared/ci_action_icons.js21
-rw-r--r--app/assets/javascripts/vue_shared/ci_status_icons.js43
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue93
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue56
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue58
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue81
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue49
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue143
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue19
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue76
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue70
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue73
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue79
-rw-r--r--app/assets/javascripts/vue_shared/components/popup_dialog.vue112
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue46
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue109
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue163
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue37
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue31
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue29
-rw-r--r--app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js42
-rw-r--r--app/assets/javascripts/wikis.js3
-rw-r--r--app/assets/javascripts/zen_mode.js10
-rw-r--r--app/assets/stylesheets/framework.scss8
-rw-r--r--app/assets/stylesheets/framework/animations.scss9
-rw-r--r--app/assets/stylesheets/framework/avatar.scss7
-rw-r--r--app/assets/stylesheets/framework/banner.scss25
-rw-r--r--app/assets/stylesheets/framework/blank.scss83
-rw-r--r--app/assets/stylesheets/framework/blocks.scss31
-rw-r--r--app/assets/stylesheets/framework/buttons.scss53
-rw-r--r--app/assets/stylesheets/framework/callout.scss14
-rw-r--r--app/assets/stylesheets/framework/common.scss141
-rw-r--r--app/assets/stylesheets/framework/contextual-sidebar.scss474
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss23
-rw-r--r--app/assets/stylesheets/framework/emoji-sprites.scss2052
-rw-r--r--app/assets/stylesheets/framework/files.scss88
-rw-r--r--app/assets/stylesheets/framework/filters.scss34
-rw-r--r--app/assets/stylesheets/framework/flash.scss12
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss15
-rw-r--r--app/assets/stylesheets/framework/header.scss57
-rw-r--r--app/assets/stylesheets/framework/highlight.scss4
-rw-r--r--app/assets/stylesheets/framework/icons.scss14
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/framework/layout.scss38
-rw-r--r--app/assets/stylesheets/framework/lists.scss95
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss83
-rw-r--r--app/assets/stylesheets/framework/mixins.scss38
-rw-r--r--app/assets/stylesheets/framework/modal.scss6
-rw-r--r--app/assets/stylesheets/framework/new-nav.scss0
-rw-r--r--app/assets/stylesheets/framework/new-sidebar.scss510
-rw-r--r--app/assets/stylesheets/framework/popup.scss15
-rw-r--r--app/assets/stylesheets/framework/responsive-tables.scss137
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss165
-rw-r--r--app/assets/stylesheets/framework/secondary-navigation-elements.scss6
-rw-r--r--app/assets/stylesheets/framework/selects.scss57
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss29
-rw-r--r--app/assets/stylesheets/framework/tables.scss2
-rw-r--r--app/assets/stylesheets/framework/tabs.scss35
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/assets/stylesheets/framework/tooltips.scss2
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss27
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss33
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss32
-rw-r--r--app/assets/stylesheets/framework/vue_transitions.scss9
-rw-r--r--app/assets/stylesheets/framework/wells.scss31
-rw-r--r--app/assets/stylesheets/framework/zen.scss2
-rw-r--r--app/assets/stylesheets/highlight/white.scss26
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss26
-rw-r--r--app/assets/stylesheets/pages/boards.scss15
-rw-r--r--app/assets/stylesheets/pages/builds.scss51
-rw-r--r--app/assets/stylesheets/pages/ci_projects.scss2
-rw-r--r--app/assets/stylesheets/pages/clusters.scss7
-rw-r--r--app/assets/stylesheets/pages/commits.scss9
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss18
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss57
-rw-r--r--app/assets/stylesheets/pages/editor.scss8
-rw-r--r--app/assets/stylesheets/pages/environments.scss111
-rw-r--r--app/assets/stylesheets/pages/events.scss6
-rw-r--r--app/assets/stylesheets/pages/groups.scss115
-rw-r--r--app/assets/stylesheets/pages/help.scss20
-rw-r--r--app/assets/stylesheets/pages/issuable.scss108
-rw-r--r--app/assets/stylesheets/pages/issues.scss88
-rw-r--r--app/assets/stylesheets/pages/login.scss102
-rw-r--r--app/assets/stylesheets/pages/members.scss47
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss50
-rw-r--r--app/assets/stylesheets/pages/milestone.scss36
-rw-r--r--app/assets/stylesheets/pages/note_form.scss63
-rw-r--r--app/assets/stylesheets/pages/notes.scss192
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss203
-rw-r--r--app/assets/stylesheets/pages/profile.scss2
-rw-r--r--app/assets/stylesheets/pages/projects.scss210
-rw-r--r--app/assets/stylesheets/pages/repo.scss507
-rw-r--r--app/assets/stylesheets/pages/runners.scss7
-rw-r--r--app/assets/stylesheets/pages/search.scss4
-rw-r--r--app/assets/stylesheets/pages/settings.scss40
-rw-r--r--app/assets/stylesheets/pages/sherlock.scss16
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss16
-rw-r--r--app/assets/stylesheets/pages/status.scss4
-rw-r--r--app/assets/stylesheets/pages/todos.scss2
-rw-r--r--app/assets/stylesheets/pages/tree.scss9
-rw-r--r--app/assets/stylesheets/pages/wiki.scss6
-rw-r--r--app/assets/stylesheets/test.scss5
-rw-r--r--app/controllers/admin/applications_controller.rb15
-rw-r--r--app/controllers/admin/impersonation_tokens_controller.rb2
-rw-r--r--app/controllers/admin/runners_controller.rb3
-rw-r--r--app/controllers/application_controller.rb37
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/boards/issues_controller.rb1
-rw-r--r--app/controllers/concerns/group_tree.rb24
-rw-r--r--app/controllers/concerns/issuable_actions.rb77
-rw-r--r--app/controllers/concerns/issuable_collections.rb91
-rw-r--r--app/controllers/concerns/issues_action.rb8
-rw-r--r--app/controllers/concerns/lfs_request.rb14
-rw-r--r--app/controllers/concerns/merge_requests_action.rb9
-rw-r--r--app/controllers/concerns/notes_actions.rb15
-rw-r--r--app/controllers/concerns/preview_markdown.rb23
-rw-r--r--app/controllers/concerns/renders_notes.rb2
-rw-r--r--app/controllers/confirmations_controller.rb2
-rw-r--r--app/controllers/dashboard/groups_controller.rb33
-rw-r--r--app/controllers/dashboard/projects_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb2
-rw-r--r--app/controllers/dashboard_controller.rb2
-rw-r--r--app/controllers/explore/groups_controller.rb16
-rw-r--r--app/controllers/groups/children_controller.rb39
-rw-r--r--app/controllers/groups/milestones_controller.rb3
-rw-r--r--app/controllers/groups_controller.rb47
-rw-r--r--app/controllers/help_controller.rb4
-rw-r--r--app/controllers/import/github_controller.rb4
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb1
-rw-r--r--app/controllers/invites_controller.rb2
-rw-r--r--app/controllers/jwt_controller.rb6
-rw-r--r--app/controllers/metrics_controller.rb16
-rw-r--r--app/controllers/oauth/applications_controller.rb21
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb8
-rw-r--r--app/controllers/passwords_controller.rb16
-rw-r--r--app/controllers/profiles/keys_controller.rb10
-rw-r--r--app/controllers/profiles/passwords_controller.rb2
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb2
-rw-r--r--app/controllers/profiles_controller.rb14
-rw-r--r--app/controllers/projects/application_controller.rb10
-rw-r--r--app/controllers/projects/blob_controller.rb1
-rw-r--r--app/controllers/projects/branches_controller.rb4
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb25
-rw-r--r--app/controllers/projects/clusters_controller.rb41
-rw-r--r--app/controllers/projects/commit_controller.rb21
-rw-r--r--app/controllers/projects/commits_controller.rb5
-rw-r--r--app/controllers/projects/deployments_controller.rb1
-rw-r--r--app/controllers/projects/environments_controller.rb1
-rw-r--r--app/controllers/projects/git_http_client_controller.rb6
-rw-r--r--app/controllers/projects/group_links_controller.rb14
-rw-r--r--app/controllers/projects/issues_controller.rb86
-rw-r--r--app/controllers/projects/jobs_controller.rb7
-rw-r--r--app/controllers/projects/labels_controller.rb1
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests/conflicts_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb8
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests_controller.rb51
-rw-r--r--app/controllers/projects/milestones_controller.rb12
-rw-r--r--app/controllers/projects/notes_controller.rb1
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb19
-rw-r--r--app/controllers/projects/refs_controller.rb10
-rw-r--r--app/controllers/projects/settings/repository_controller.rb10
-rw-r--r--app/controllers/projects/wikis_controller.rb22
-rw-r--r--app/controllers/projects_controller.rb31
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/controllers/snippets/notes_controller.rb1
-rw-r--r--app/controllers/snippets_controller.rb12
-rw-r--r--app/controllers/unicorn_test_controller.rb14
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/finders/autocomplete_users_finder.rb2
-rw-r--r--app/finders/branches_finder.rb2
-rw-r--r--app/finders/group_descendants_finder.rb153
-rw-r--r--app/finders/group_projects_finder.rb1
-rw-r--r--app/finders/groups_finder.rb8
-rw-r--r--app/finders/issuable_finder.rb1
-rw-r--r--app/finders/notes_finder.rb3
-rw-r--r--app/finders/personal_access_tokens_finder.rb1
-rw-r--r--app/finders/projects_finder.rb3
-rw-r--r--app/finders/runner_jobs_finder.rb22
-rw-r--r--app/helpers/appearances_helper.rb7
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/application_settings_helper.rb67
-rw-r--r--app/helpers/auto_devops_helper.rb18
-rw-r--r--app/helpers/boards_helper.rb11
-rw-r--r--app/helpers/button_helper.rb56
-rw-r--r--app/helpers/ci_status_helper.rb29
-rw-r--r--app/helpers/commits_helper.rb26
-rw-r--r--app/helpers/compare_helper.rb4
-rw-r--r--app/helpers/diff_helper.rb7
-rw-r--r--app/helpers/emails_helper.rb1
-rw-r--r--app/helpers/events_helper.rb10
-rw-r--r--app/helpers/gitlab_routing_helper.rb6
-rw-r--r--app/helpers/groups_helper.rb9
-rw-r--r--app/helpers/icons_helper.rb9
-rw-r--r--app/helpers/instance_configuration_helper.rb18
-rw-r--r--app/helpers/issuables_helper.rb65
-rw-r--r--app/helpers/lazy_image_tag_helper.rb1
-rw-r--r--app/helpers/markup_helper.rb25
-rw-r--r--app/helpers/namespaces_helper.rb7
-rw-r--r--app/helpers/nav_helper.rb8
-rw-r--r--app/helpers/notifications_helper.rb1
-rw-r--r--app/helpers/preferences_helper.rb3
-rw-r--r--app/helpers/projects_helper.rb47
-rw-r--r--app/helpers/search_helper.rb3
-rw-r--r--app/helpers/sorting_helper.rb11
-rw-r--r--app/helpers/storage_health_helper.rb5
-rw-r--r--app/helpers/tree_helper.rb16
-rw-r--r--app/helpers/visibility_level_helper.rb2
-rw-r--r--app/models/application_setting.rb74
-rw-r--r--app/models/blob.rb21
-rw-r--r--app/models/ci/artifact_blob.rb17
-rw-r--r--app/models/ci/build.rb8
-rw-r--r--app/models/ci/pipeline.rb83
-rw-r--r--app/models/ci/runner.rb6
-rw-r--r--app/models/clusters/applications/helm.rb35
-rw-r--r--app/models/clusters/applications/ingress.rb44
-rw-r--r--app/models/clusters/cluster.rb95
-rw-r--r--app/models/clusters/concerns/application_status.rb43
-rw-r--r--app/models/clusters/platforms/kubernetes.rb178
-rw-r--r--app/models/clusters/project.rb8
-rw-r--r--app/models/clusters/providers/gcp.rb80
-rw-r--r--app/models/commit.rb25
-rw-r--r--app/models/commit_collection.rb44
-rw-r--r--app/models/commit_range.rb8
-rw-r--r--app/models/commit_status.rb28
-rw-r--r--app/models/concerns/avatarable.rb25
-rw-r--r--app/models/concerns/awardable.rb1
-rw-r--r--app/models/concerns/cache_markdown_field.rb3
-rw-r--r--app/models/concerns/group_descendant.rb56
-rw-r--r--app/models/concerns/has_variable.rb4
-rw-r--r--app/models/concerns/ignorable_column.rb4
-rw-r--r--app/models/concerns/issuable.rb60
-rw-r--r--app/models/concerns/loaded_in_group_list.rb72
-rw-r--r--app/models/concerns/manual_inverse_association.rb17
-rw-r--r--app/models/concerns/mentionable.rb4
-rw-r--r--app/models/concerns/milestoneish.rb8
-rw-r--r--app/models/concerns/protected_branch_access.rb18
-rw-r--r--app/models/concerns/protected_ref_access.rb24
-rw-r--r--app/models/concerns/referable.rb8
-rw-r--r--app/models/concerns/repository_mirroring.rb32
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb2
-rw-r--r--app/models/concerns/subscribable.rb2
-rw-r--r--app/models/concerns/time_trackable.rb9
-rw-r--r--app/models/diff_note.rb3
-rw-r--r--app/models/email.rb3
-rw-r--r--app/models/environment.rb11
-rw-r--r--app/models/epic.rb7
-rw-r--r--app/models/external_issue.rb8
-rw-r--r--app/models/fork_network.rb4
-rw-r--r--app/models/fork_network_member.rb10
-rw-r--r--app/models/gcp/cluster.rb113
-rw-r--r--app/models/group.rb36
-rw-r--r--app/models/group_custom_attribute.rb6
-rw-r--r--app/models/identity.rb16
-rw-r--r--app/models/instance_configuration.rb71
-rw-r--r--app/models/issue.rb33
-rw-r--r--app/models/key.rb8
-rw-r--r--app/models/label.rb6
-rw-r--r--app/models/lfs_object.rb10
-rw-r--r--app/models/merge_request.rb164
-rw-r--r--app/models/merge_request_diff.rb83
-rw-r--r--app/models/merge_request_diff_commit.rb4
-rw-r--r--app/models/milestone.rb14
-rw-r--r--app/models/namespace.rb15
-rw-r--r--app/models/note.rb28
-rw-r--r--app/models/oauth_access_token.rb10
-rw-r--r--app/models/pages_domain.rb17
-rw-r--r--app/models/project.rb194
-rw-r--r--app/models/project_custom_attribute.rb6
-rw-r--r--app/models/project_services/chat_message/issue_message.rb2
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb3
-rw-r--r--app/models/project_services/kubernetes_service.rb15
-rw-r--r--app/models/project_services/packagist_service.rb65
-rw-r--r--app/models/project_services/prometheus_service.rb6
-rw-r--r--app/models/project_wiki.rb8
-rw-r--r--app/models/protected_branch.rb4
-rw-r--r--app/models/protected_tag.rb4
-rw-r--r--app/models/protected_tag/create_access_level.rb12
-rw-r--r--app/models/repository.rb194
-rw-r--r--app/models/sent_notification.rb6
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/snippet.rb15
-rw-r--r--app/models/storage/hashed_project.rb1
-rw-r--r--app/models/system_note_metadata.rb10
-rw-r--r--app/models/user.rb127
-rw-r--r--app/models/wiki_page.rb19
-rw-r--r--app/policies/ci/build_policy.rb11
-rw-r--r--app/policies/clusters/cluster_policy.rb12
-rw-r--r--app/policies/gcp/cluster_policy.rb12
-rw-r--r--app/policies/group_policy.rb2
-rw-r--r--app/policies/namespace_policy.rb1
-rw-r--r--app/presenters/clusters/cluster_presenter.rb9
-rw-r--r--app/presenters/gcp/cluster_presenter.rb9
-rw-r--r--app/serializers/base_serializer.rb7
-rw-r--r--app/serializers/blob_entity.rb4
-rw-r--r--app/serializers/build_details_entity.rb2
-rw-r--r--app/serializers/cluster_application_entity.rb5
-rw-r--r--app/serializers/cluster_entity.rb1
-rw-r--r--app/serializers/cluster_serializer.rb2
-rw-r--r--app/serializers/concerns/with_pagination.rb22
-rw-r--r--app/serializers/container_tag_entity.rb4
-rw-r--r--app/serializers/environment_serializer.rb12
-rw-r--r--app/serializers/group_child_entity.rb77
-rw-r--r--app/serializers/group_child_serializer.rb51
-rw-r--r--app/serializers/group_entity.rb2
-rw-r--r--app/serializers/group_serializer.rb18
-rw-r--r--app/serializers/issuable_entity.rb8
-rw-r--r--app/serializers/issuable_sidebar_entity.rb16
-rw-r--r--app/serializers/issue_entity.rb5
-rw-r--r--app/serializers/issue_serializer.rb15
-rw-r--r--app/serializers/issue_sidebar_entity.rb3
-rw-r--r--app/serializers/merge_request_basic_entity.rb6
-rw-r--r--app/serializers/merge_request_entity.rb4
-rw-r--r--app/serializers/merge_request_serializer.rb9
-rw-r--r--app/serializers/pipeline_serializer.rb10
-rw-r--r--app/serializers/submodule_entity.rb2
-rw-r--r--app/serializers/time_trackable_entity.rb11
-rw-r--r--app/serializers/tree_entity.rb4
-rw-r--r--app/serializers/tree_root_entity.rb4
-rw-r--r--app/services/access_token_validation_service.rb7
-rw-r--r--app/services/applications/create_service.rb13
-rw-r--r--app/services/auth/container_registry_authentication_service.rb17
-rw-r--r--app/services/base_count_service.rb34
-rw-r--r--app/services/base_renderer.rb7
-rw-r--r--app/services/ci/create_cluster_service.rb15
-rw-r--r--app/services/ci/ensure_stage_service.rb39
-rw-r--r--app/services/ci/fetch_gcp_operation_service.rb17
-rw-r--r--app/services/ci/fetch_kubernetes_token_service.rb1
-rw-r--r--app/services/ci/finalize_cluster_creation_service.rb33
-rw-r--r--app/services/ci/integrate_cluster_service.rb26
-rw-r--r--app/services/ci/pipeline_trigger_service.rb8
-rw-r--r--app/services/ci/provision_cluster_service.rb36
-rw-r--r--app/services/ci/retry_build_service.rb2
-rw-r--r--app/services/ci/update_cluster_service.rb22
-rw-r--r--app/services/clusters/applications/base_helm_service.rb29
-rw-r--r--app/services/clusters/applications/check_installation_progress_service.rb65
-rw-r--r--app/services/clusters/applications/install_service.rb21
-rw-r--r--app/services/clusters/applications/schedule_installation_service.rb22
-rw-r--r--app/services/clusters/create_service.rb29
-rw-r--r--app/services/clusters/gcp/fetch_operation_service.rb16
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb56
-rw-r--r--app/services/clusters/gcp/provision_service.rb47
-rw-r--r--app/services/clusters/gcp/verify_provision_status_service.rb48
-rw-r--r--app/services/clusters/update_service.rb7
-rw-r--r--app/services/compare_service.rb12
-rw-r--r--app/services/delete_merged_branches_service.rb2
-rw-r--r--app/services/events/render_service.rb21
-rw-r--r--app/services/issuable/common_system_notes_service.rb93
-rw-r--r--app/services/issuable/destroy_service.rb9
-rw-r--r--app/services/issuable_base_service.rb126
-rw-r--r--app/services/issues/base_service.rb18
-rw-r--r--app/services/issues/reopen_service.rb1
-rw-r--r--app/services/issues/update_service.rb13
-rw-r--r--app/services/keys/base_service.rb1
-rw-r--r--app/services/labels/promote_service.rb1
-rw-r--r--app/services/merge_requests/add_todo_when_build_fails_service.rb2
-rw-r--r--app/services/merge_requests/base_service.rb28
-rw-r--r--app/services/merge_requests/build_service.rb83
-rw-r--r--app/services/merge_requests/conflicts/list_service.rb4
-rw-r--r--app/services/merge_requests/conflicts/resolve_service.rb48
-rw-r--r--app/services/merge_requests/create_from_issue_service.rb24
-rw-r--r--app/services/merge_requests/merge_service.rb62
-rw-r--r--app/services/merge_requests/refresh_service.rb4
-rw-r--r--app/services/merge_requests/reopen_service.rb1
-rw-r--r--app/services/merge_requests/update_service.rb15
-rw-r--r--app/services/metrics_service.rb3
-rw-r--r--app/services/milestones/promote_service.rb85
-rw-r--r--app/services/notes/render_service.rb21
-rw-r--r--app/services/notification_service.rb2
-rw-r--r--app/services/projects/count_service.rb25
-rw-r--r--app/services/projects/destroy_service.rb2
-rw-r--r--app/services/projects/forks_count_service.rb2
-rw-r--r--app/services/projects/group_links/create_service.rb15
-rw-r--r--app/services/projects/group_links/destroy_service.rb11
-rw-r--r--app/services/projects/hashed_storage/migrate_attachments_service.rb54
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb70
-rw-r--r--app/services/projects/hashed_storage_migration_service.rb62
-rw-r--r--app/services/projects/import_service.rb54
-rw-r--r--app/services/projects/open_issues_count_service.rb2
-rw-r--r--app/services/projects/open_merge_requests_count_service.rb2
-rw-r--r--app/services/projects/transfer_service.rb38
-rw-r--r--app/services/projects/unlink_fork_service.rb18
-rw-r--r--app/services/projects/update_service.rb10
-rw-r--r--app/services/protected_branches/api_create_service.rb29
-rw-r--r--app/services/protected_branches/api_update_service.rb47
-rw-r--r--app/services/protected_branches/legacy_api_create_service.rb29
-rw-r--r--app/services/protected_branches/legacy_api_update_service.rb47
-rw-r--r--app/services/quick_actions/interpret_service.rb18
-rw-r--r--app/services/system_hooks_service.rb50
-rw-r--r--app/services/system_note_service.rb37
-rw-r--r--app/services/todo_service.rb28
-rw-r--r--app/services/users/build_service.rb2
-rw-r--r--app/services/users/keys_count_service.rb27
-rw-r--r--app/services/users/last_push_event_service.rb4
-rw-r--r--app/services/users/migrate_to_ghost_user_service.rb18
-rw-r--r--app/uploaders/file_uploader.rb17
-rw-r--r--app/validators/abstract_path_validator.rb38
-rw-r--r--app/validators/certificate_key_validator.rb1
-rw-r--r--app/validators/certificate_validator.rb1
-rw-r--r--app/validators/cluster_name_validator.rb24
-rw-r--r--app/validators/dynamic_path_validator.rb53
-rw-r--r--app/validators/namespace_path_validator.rb19
-rw-r--r--app/validators/project_path_validator.rb19
-rw-r--r--app/validators/user_path_validator.rb15
-rw-r--r--app/views/admin/appearances/_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_form.html.haml132
-rw-r--r--app/views/admin/background_jobs/show.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml4
-rw-r--r--app/views/admin/groups/_group.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml2
-rw-r--r--app/views/admin/hook_logs/_index.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml33
-rw-r--r--app/views/admin/runners/show.html.haml2
-rw-r--r--app/views/ci/status/_badge.html.haml4
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml8
-rw-r--r--app/views/dashboard/_groups_head.html.haml6
-rw-r--r--app/views/dashboard/_projects_head.html.haml6
-rw-r--r--app/views/dashboard/groups/_empty_state.html.haml7
-rw-r--r--app/views/dashboard/groups/_groups.html.haml9
-rw-r--r--app/views/dashboard/groups/index.html.haml4
-rw-r--r--app/views/dashboard/projects/_blank_state_admin_welcome.html.haml70
-rw-r--r--app/views/dashboard/projects/_blank_state_welcome.html.haml98
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml21
-rw-r--r--app/views/dashboard/todos/_todo.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/devise/sessions/new.html.haml6
-rw-r--r--app/views/devise/shared/_links.erb2
-rw-r--r--app/views/devise/shared/_signin_box.html.haml4
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml4
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml2
-rw-r--r--app/views/discussions/_diff_discussion.html.haml2
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml2
-rw-r--r--app/views/discussions/_discussion.html.haml2
-rw-r--r--app/views/discussions/_notes.html.haml4
-rw-r--r--app/views/discussions/_parallel_diff_discussion.html.haml4
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml22
-rw-r--r--app/views/events/_event.atom.builder7
-rw-r--r--app/views/events/_event_note.atom.haml2
-rw-r--r--app/views/events/event/_note.html.haml2
-rw-r--r--app/views/events/event/_push.html.haml3
-rw-r--r--app/views/explore/groups/_groups.html.haml6
-rw-r--r--app/views/explore/groups/index.html.haml9
-rw-r--r--app/views/groups/_children.html.haml5
-rw-r--r--app/views/groups/_home_panel.html.haml2
-rw-r--r--app/views/groups/_show_nav.html.haml8
-rw-r--r--app/views/groups/edit.html.haml2
-rw-r--r--app/views/groups/issues.html.haml7
-rw-r--r--app/views/groups/merge_requests.html.haml7
-rw-r--r--app/views/groups/milestones/_form.html.haml4
-rw-r--r--app/views/groups/milestones/index.html.haml1
-rw-r--r--app/views/groups/milestones/new.html.haml1
-rw-r--r--app/views/groups/show.html.haml42
-rw-r--r--app/views/groups/subgroups.html.haml21
-rw-r--r--app/views/help/_shortcuts.html.haml4
-rw-r--r--app/views/help/index.html.haml4
-rw-r--r--app/views/help/instance_configuration.html.haml17
-rw-r--r--app/views/help/instance_configuration/_gitlab_ci.html.haml24
-rw-r--r--app/views/help/instance_configuration/_gitlab_pages.html.haml35
-rw-r--r--app/views/help/instance_configuration/_ssh_info.html.haml27
-rw-r--r--app/views/layouts/_flash.html.haml16
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/_mailer.html.haml4
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml4
-rw-r--r--app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml5
-rw-r--r--app/views/notify/_note_email.html.haml11
-rw-r--r--app/views/notify/new_user_email.html.haml2
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml10
-rw-r--r--app/views/notify/pipeline_success_email.html.haml10
-rw-r--r--app/views/peek/views/_gitaly.html.haml7
-rw-r--r--app/views/profiles/accounts/_reset_token.html.haml11
-rw-r--r--app/views/profiles/accounts/show.html.haml16
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml37
-rw-r--r--app/views/projects/_export.html.haml4
-rw-r--r--app/views/projects/_home_panel.html.haml9
-rw-r--r--app/views/projects/_md_preview.html.haml26
-rw-r--r--app/views/projects/_new_project_fields.html.haml41
-rw-r--r--app/views/projects/_project_templates.html.haml30
-rw-r--r--app/views/projects/_readme.html.haml23
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/_header.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml5
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/buttons/_star.html.haml6
-rw-r--r--app/views/projects/clusters/_advanced_settings.html.haml14
-rw-r--r--app/views/projects/clusters/_form.html.haml40
-rw-r--r--app/views/projects/clusters/_header.html.haml2
-rw-r--r--app/views/projects/clusters/login.html.haml2
-rw-r--r--app/views/projects/clusters/new.html.haml17
-rw-r--r--app/views/projects/clusters/new_gcp.html.haml10
-rw-r--r--app/views/projects/clusters/show.html.haml105
-rw-r--r--app/views/projects/commit/_ajax_signature.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml11
-rw-r--r--app/views/projects/commit/_limit_exceeded_message.html.haml8
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml2
-rw-r--r--app/views/projects/commit/branches.html.haml26
-rw-r--r--app/views/projects/commits/_commit.html.haml7
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml4
-rw-r--r--app/views/projects/diffs/_stats.html.haml8
-rw-r--r--app/views/projects/edit.html.haml25
-rw-r--r--app/views/projects/empty.html.haml10
-rw-r--r--app/views/projects/environments/folder.html.haml4
-rw-r--r--app/views/projects/environments/index.html.haml4
-rw-r--r--app/views/projects/environments/show.html.haml17
-rw-r--r--app/views/projects/forks/_fork_button.html.haml26
-rw-r--r--app/views/projects/forks/new.html.haml17
-rw-r--r--app/views/projects/graphs/show.html.haml14
-rw-r--r--app/views/projects/hook_logs/_index.html.haml2
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml4
-rw-r--r--app/views/projects/issues/_new_branch.html.haml63
-rw-r--r--app/views/projects/issues/edit.html.haml7
-rw-r--r--app/views/projects/issues/show.html.haml6
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml18
-rw-r--r--app/views/projects/jobs/show.html.haml6
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml4
-rw-r--r--app/views/projects/merge_requests/index.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml11
-rw-r--r--app/views/projects/new.html.haml178
-rw-r--r--app/views/projects/pipelines/index.html.haml8
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml26
-rw-r--r--app/views/projects/project_members/index.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml4
-rw-r--r--app/views/projects/protected_tags/_create_protected_tag.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml4
-rw-r--r--app/views/projects/services/prometheus/_show.html.haml27
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml16
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml8
-rw-r--r--app/views/projects/tags/index.html.haml14
-rw-r--r--app/views/projects/tags/new.html.haml21
-rw-r--r--app/views/projects/tags/show.html.haml18
-rw-r--r--app/views/projects/tree/_tree_header.html.haml8
-rw-r--r--app/views/projects/tree/_truncated_notice_tree_row.html.haml7
-rw-r--r--app/views/projects/tree/show.html.haml4
-rw-r--r--app/views/projects/wikis/_pages_wiki_page.html.haml2
-rw-r--r--app/views/projects/wikis/empty.html.haml2
-rw-r--r--app/views/projects/wikis/history.html.haml3
-rw-r--r--app/views/projects/wikis/show.html.haml4
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml29
-rw-r--r--app/views/shared/_clone_panel.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml3
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml2
-rw-r--r--app/views/shared/_ref_switcher.html.haml27
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_notifications.html.haml10
-rw-r--r--app/views/shared/groups/_dropdown.html.haml44
-rw-r--r--app/views/shared/groups/_empty_state.html.haml7
-rw-r--r--app/views/shared/groups/_group.html.haml6
-rw-r--r--app/views/shared/groups/_list.html.haml2
-rw-r--r--app/views/shared/groups/_search_form.html.haml4
-rw-r--r--app/views/shared/hook_logs/_content.html.haml2
-rw-r--r--app/views/shared/icons/_add_new_project.svg2
-rw-r--r--app/views/shared/icons/_express.svg7
-rw-r--r--app/views/shared/icons/_icon_autodevops.svg2
-rw-r--r--app/views/shared/icons/_icon_hourglass.svg1
-rw-r--r--app/views/shared/icons/_lightbulb.svg1
-rw-r--r--app/views/shared/icons/_rails.svg7
-rw-r--r--app/views/shared/icons/_spring.svg7
-rw-r--r--app/views/shared/issuable/_filter.html.haml1
-rw-r--r--app/views/shared/issuable/_participants.html.haml18
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml1
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml13
-rw-r--r--app/views/shared/members/_group.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml1
-rw-r--r--app/views/shared/milestones/_milestone.html.haml7
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml16
-rw-r--r--app/views/shared/projects/_dropdown.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/repo/_editable_mode.html.haml2
-rw-r--r--app/views/shared/repo/_repo.html.haml12
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml1
-rw-r--r--app/views/users/_groups.html.haml2
-rw-r--r--app/views/users/show.html.haml3
-rw-r--r--app/workers/cluster_install_app_worker.rb11
-rw-r--r--app/workers/cluster_provision_worker.rb6
-rw-r--r--app/workers/cluster_wait_for_app_installation_worker.rb14
-rw-r--r--app/workers/concerns/cluster_applications.rb9
-rw-r--r--app/workers/concerns/gitlab/github_import/notify_upon_death.rb31
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb54
-rw-r--r--app/workers/concerns/gitlab/github_import/queue.rb16
-rw-r--r--app/workers/concerns/gitlab/github_import/rescheduling_methods.rb40
-rw-r--r--app/workers/concerns/gitlab/github_import/stage_methods.rb30
-rw-r--r--app/workers/concerns/project_start_import.rb9
-rw-r--r--app/workers/create_pipeline_worker.rb16
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb74
-rw-r--r--app/workers/gitlab/github_import/import_diff_note_worker.rb25
-rw-r--r--app/workers/gitlab/github_import/import_issue_worker.rb25
-rw-r--r--app/workers/gitlab/github_import/import_note_worker.rb25
-rw-r--r--app/workers/gitlab/github_import/import_pull_request_worker.rb25
-rw-r--r--app/workers/gitlab/github_import/refresh_import_jid_worker.rb38
-rw-r--r--app/workers/gitlab/github_import/stage/finish_import_worker.rb43
-rw-r--r--app/workers/gitlab/github_import/stage/import_base_data_worker.rb33
-rw-r--r--app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb31
-rw-r--r--app/workers/gitlab/github_import/stage/import_notes_worker.rb27
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb29
-rw-r--r--app/workers/gitlab/github_import/stage/import_repository_worker.rb38
-rw-r--r--app/workers/irker_worker.rb1
-rw-r--r--app/workers/pipeline_schedule_worker.rb2
-rw-r--r--app/workers/project_migrate_hashed_storage_worker.rb26
-rw-r--r--app/workers/repository_fork_worker.rb3
-rw-r--r--app/workers/repository_import_worker.rb14
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb15
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb2
-rw-r--r--app/workers/update_merge_requests_worker.rb18
-rw-r--r--app/workers/wait_for_cluster_creation_worker.rb21
-rwxr-xr-xbin/changelog1
-rw-r--r--changelogs/unreleased/13637-show-account-confirmation-link-in-e-mail-text.yml5
-rw-r--r--changelogs/unreleased/13711-allow-same-period-housekeeping.yml6
-rw-r--r--changelogs/unreleased/14395-upgrade-gitlab-markup.yml5
-rw-r--r--changelogs/unreleased/14553-missing-space-in-log-msg.yml5
-rw-r--r--changelogs/unreleased/15588-fix-empty-dropdown-on-create-new-pipeline-in-case-of-validation-errors.yml5
-rw-r--r--changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml5
-rw-r--r--changelogs/unreleased/18308-escape-characters.yml5
-rw-r--r--changelogs/unreleased/18608-lock-issues.yml4
-rw-r--r--changelogs/unreleased/20049-projects-api-forks.yml5
-rw-r--r--changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml5
-rw-r--r--changelogs/unreleased/21143-customize-branch-name-when-using-create-branch-in-an-issue.yml5
-rw-r--r--changelogs/unreleased/21331-improve-confusing-compare-page.yml5
-rw-r--r--changelogs/unreleased/24121_extract_yet_another_users_finder.yml5
-rw-r--r--changelogs/unreleased/26890-fix-default-branches-sorting.yml5
-rw-r--r--changelogs/unreleased/28377-add-edit-button-to-mobile-file-view.yml5
-rw-r--r--changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml5
-rw-r--r--changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml5
-rw-r--r--changelogs/unreleased/32057_support_ordering_project_notes_in_notes_api.yml5
-rw-r--r--changelogs/unreleased/32098-pipelines-navigation.yml6
-rw-r--r--changelogs/unreleased/32163-protected-branch-form-should-have-sane-defaults-for-dropdowns.yml5
-rw-r--r--changelogs/unreleased/33328-usage-ping-for-gitlab-features-and-components.yml5
-rw-r--r--changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml5
-rw-r--r--changelogs/unreleased/33493-attempt-to-link-saml-users-to-ldap-by-email.yml5
-rw-r--r--changelogs/unreleased/34102-online-view-of-artifacts-fe.yml5
-rw-r--r--changelogs/unreleased/34259-project-denial-of-service-via-gitmodules-fix.yml5
-rw-r--r--changelogs/unreleased/34366-issue-sidebar-don-t-render-participants-in-collapsed-state.yml5
-rw-r--r--changelogs/unreleased/34371-cycle-analitcs-global.yml5
-rw-r--r--changelogs/unreleased/34510-board-issues-sql-speedup.yml5
-rw-r--r--changelogs/unreleased/34600-performance-wiki-pages.yml5
-rw-r--r--changelogs/unreleased/3523-i18n-autodevops.yml5
-rw-r--r--changelogs/unreleased/35290_allow_public_project_apis.yml4
-rw-r--r--changelogs/unreleased/35917_create_services_for_keys.yml4
-rw-r--r--changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml5
-rw-r--r--changelogs/unreleased/36255-metrics-that-do-not-have-a-complete-history-are-not-shown-at-all.yml5
-rw-r--r--changelogs/unreleased/36400-trigger-job.yml5
-rw-r--r--changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml5
-rw-r--r--changelogs/unreleased/36631-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml6
-rw-r--r--changelogs/unreleased/36670-remove-edit-form.yml5
-rw-r--r--changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml5
-rw-r--r--changelogs/unreleased/36829-add-ability-to-verify-gpg-subkeys.yml5
-rw-r--r--changelogs/unreleased/36884-gitaly-admin-version.yml5
-rw-r--r--changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml5
-rw-r--r--changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml4
-rw-r--r--changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml5
-rw-r--r--changelogs/unreleased/37220-es-modules.yml5
-rw-r--r--changelogs/unreleased/37229-mr-widget-status-icon.yml5
-rw-r--r--changelogs/unreleased/37335-counter-active-state.yml5
-rw-r--r--changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml5
-rw-r--r--changelogs/unreleased/37970-ci-sections-tracking.yml5
-rw-r--r--changelogs/unreleased/37970-timestamped-ci.yml5
-rw-r--r--changelogs/unreleased/37999-fix-circuit-breaker.yml5
-rw-r--r--changelogs/unreleased/38031-monitoring-hover-info-is-clipped.yml6
-rw-r--r--changelogs/unreleased/38036-hover-and-legend-data-should-be-linked.yml5
-rw-r--r--changelogs/unreleased/38052-use-simple-api-for-projects.yml5
-rw-r--r--changelogs/unreleased/38075_allow_refernce_integer_labels.yml5
-rw-r--r--changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml5
-rw-r--r--changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml5
-rw-r--r--changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml6
-rw-r--r--changelogs/unreleased/38234-reserve-refs-replace.yml5
-rw-r--r--changelogs/unreleased/38389-allow-merge-without-success.yml6
-rw-r--r--changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml5
-rw-r--r--changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml5
-rw-r--r--changelogs/unreleased/38432-fix-notes-type-for-import.yml6
-rw-r--r--changelogs/unreleased/38502-fix-nav-dropdown-close-animation.yml5
-rw-r--r--changelogs/unreleased/38571-fix-exception-in-raven-report.yml6
-rw-r--r--changelogs/unreleased/38619-fix-comment-delete-confirm-text.yml5
-rw-r--r--changelogs/unreleased/38635-fix-gitlab-check-git-ssh-config.yml5
-rw-r--r--changelogs/unreleased/38696-fix-project-snippets-breadcrumb-link.yml5
-rw-r--r--changelogs/unreleased/38775-scrollable-tabs-on-admin.yml5
-rw-r--r--changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml5
-rw-r--r--changelogs/unreleased/38822-oauth-search-case-insensitive.yml5
-rw-r--r--changelogs/unreleased/38877-disable-autocomplete-in-filtered-search.yml5
-rw-r--r--changelogs/unreleased/38962-automatically-run-a-pipeline-when-auto-devops-is-turned-on-in-project-settings.yml5
-rw-r--r--changelogs/unreleased/39335-add-time-spend-to-milestones.yml5
-rw-r--r--changelogs/unreleased/39436-pages-api-administrative.yml5
-rw-r--r--changelogs/unreleased/39455-clone-dropdown-should-not-have-a-tooltip.yml5
-rw-r--r--changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml5
-rw-r--r--changelogs/unreleased/39601-create-issuable-destroy-service.yml5
-rw-r--r--changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml5
-rw-r--r--changelogs/unreleased/39720-group-milestone-sorting.yml5
-rw-r--r--changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml5
-rw-r--r--changelogs/unreleased/39827-fix-projects-dropdown-overflow.yml5
-rw-r--r--changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml6
-rw-r--r--changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml5
-rw-r--r--changelogs/unreleased/39977-gitlab-shell-default-timeout.yml5
-rw-r--r--changelogs/unreleased/40016-log-header.yml5
-rw-r--r--changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml5
-rw-r--r--changelogs/unreleased/40146_fix_special_charecter_search_in_filenames.yml5
-rw-r--r--changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml5
-rw-r--r--changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml5
-rw-r--r--changelogs/unreleased/40291-ignore-hashed-repos-cleanup-repositories.yml5
-rw-r--r--changelogs/unreleased/40352-ignore-hashed-repos-cleanup-dirs.yml5
-rw-r--r--changelogs/unreleased/40373-fix-issue-note-submit-disabled-on-paste.yml6
-rw-r--r--changelogs/unreleased/40481-bump-jquery-to-2-2-4.yml5
-rw-r--r--changelogs/unreleased/40530-merge-request-generates-wrong-diff-when-branch-and-tag-have-the-same-name.yml5
-rw-r--r--changelogs/unreleased/40561-environment-scope-value-is-not-trimmed.yml5
-rw-r--r--changelogs/unreleased/40568-bump-seed-fu-to-2-3-7.yml5
-rw-r--r--changelogs/unreleased/4080-align-retry-btn.yml5
-rw-r--r--changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml5
-rw-r--r--changelogs/unreleased/add-1000-plus-counters-for-jobs-page.yml5
-rw-r--r--changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml5
-rw-r--r--changelogs/unreleased/add-composite-index-on-merge-requests-merge-commit-sha.yml5
-rw-r--r--changelogs/unreleased/add-labels-template-index.yml5
-rw-r--r--changelogs/unreleased/add-view-replaced-file-link.yml5
-rw-r--r--changelogs/unreleased/add_closed_at_attribute.yml5
-rw-r--r--changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml5
-rw-r--r--changelogs/unreleased/adjusting-tooltips.yml5
-rw-r--r--changelogs/unreleased/an-gitaly-timeouts.yml5
-rw-r--r--changelogs/unreleased/brand_header_change.yml5
-rw-r--r--changelogs/unreleased/breadcrumb-item-links.yml5
-rw-r--r--changelogs/unreleased/breadcrumbs-line-height-padding.yml5
-rw-r--r--changelogs/unreleased/bugfix-graph-friendly-notes-number.yml5
-rw-r--r--changelogs/unreleased/bvl-delete-empty-fork-networks.yml5
-rw-r--r--changelogs/unreleased/bvl-double-fork.yml5
-rw-r--r--changelogs/unreleased/bvl-fork-network-schema.yml5
-rw-r--r--changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml5
-rw-r--r--changelogs/unreleased/bvl-subgroup-in-dropdowns.yml5
-rw-r--r--changelogs/unreleased/change-dashed-border-button-color.yml5
-rw-r--r--changelogs/unreleased/cleanup-issues-schema.yml5
-rw-r--r--changelogs/unreleased/close-issue-by-implements.yml5
-rw-r--r--changelogs/unreleased/commit-row-avatar-align-top.yml5
-rw-r--r--changelogs/unreleased/consistent-tooltip-direction-on-commits.yml5
-rw-r--r--changelogs/unreleased/content-title-link-hover-bg.yml5
-rw-r--r--changelogs/unreleased/declarative-policy-optimisations.yml5
-rw-r--r--changelogs/unreleased/default-values-for-mr-states.yml5
-rw-r--r--changelogs/unreleased/dm-bitbucket-import-truncated-shas.yml6
-rw-r--r--changelogs/unreleased/dm-closing-issue-urls.yml5
-rw-r--r--changelogs/unreleased/dm-copy-parallel-diff.yml5
-rw-r--r--changelogs/unreleased/dm-fix-registry-with-sudo-token.yml5
-rw-r--r--changelogs/unreleased/dm-pat-revoke.yml5
-rw-r--r--changelogs/unreleased/dm-project-search-performance.yml6
-rw-r--r--changelogs/unreleased/dm-search-pattern.yml5
-rw-r--r--changelogs/unreleased/dm-simple-project-avatar-url.yml5
-rw-r--r--changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml5
-rw-r--r--changelogs/unreleased/docs-28814-clarify-artifacts-ref.yml5
-rw-r--r--changelogs/unreleased/docs-38152-bump-recommended-mysql-version.yml5
-rw-r--r--changelogs/unreleased/docs-add-summary-about-project-archiving.yml5
-rw-r--r--changelogs/unreleased/docs-openid-connect.yml5
-rw-r--r--changelogs/unreleased/ee-add-project-repository-storages-index.yml5
-rw-r--r--changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml5
-rw-r--r--changelogs/unreleased/events-atom-feed-author-query.yml5
-rw-r--r--changelogs/unreleased/expose-last-pipeline-details-in-api-for-single-commit.yml5
-rw-r--r--changelogs/unreleased/feature-custom-attributes.yml4
-rw-r--r--changelogs/unreleased/feature-disable-password-authentication.yml5
-rw-r--r--changelogs/unreleased/feature-sm-35954-create-kubernetes-cluster-on-gke-from-k8s-service.yml5
-rw-r--r--changelogs/unreleased/feature-verify_secondary_emails.yml5
-rw-r--r--changelogs/unreleased/feature_add_mermaid.yml5
-rw-r--r--changelogs/unreleased/ff_port_from_ee.yml5
-rw-r--r--changelogs/unreleased/fix-edit-project-service-cancel-button-position.yml5
-rw-r--r--changelogs/unreleased/fix-filter-by-my-reaction.yml5
-rw-r--r--changelogs/unreleased/fix-gpg-case-insensitive.yml5
-rw-r--r--changelogs/unreleased/fix-image-diff-swipe-handle.yml5
-rw-r--r--changelogs/unreleased/fix-import-uploads-hashed-storage.yml5
-rw-r--r--changelogs/unreleased/fix-multi-line-hook-output.yml5
-rw-r--r--changelogs/unreleased/fix-protected-branches-descriptions.yml5
-rw-r--r--changelogs/unreleased/fix-sidebar-with-scrollbars.yml5
-rw-r--r--changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml5
-rw-r--r--changelogs/unreleased/fix-sm-37991-avoid-deactivation-when-pipeline-schedules-execute-a-commit-includes-ci-skip.yml6
-rw-r--r--changelogs/unreleased/fix-todos-last-page.yml5
-rw-r--r--changelogs/unreleased/fix-tooltip-width-issue-board.yml5
-rw-r--r--changelogs/unreleased/fix-update-doorkeeper-openid-connect.yml5
-rw-r--r--changelogs/unreleased/fl-fix-ca-time-component.yml5
-rw-r--r--changelogs/unreleased/fl-upgrade-svg.yml5
-rw-r--r--changelogs/unreleased/force-two-up-view.yml5
-rw-r--r--changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml5
-rw-r--r--changelogs/unreleased/gitaly_feature_flag_metadata.yml5
-rw-r--r--changelogs/unreleased/group-milestones-breadcrumb.yml5
-rw-r--r--changelogs/unreleased/group-new-miletone-breadcrumb.yml5
-rw-r--r--changelogs/unreleased/hash-mr-scroll-load.yml5
-rw-r--r--changelogs/unreleased/hashed-storage-attachments-migration-path.yml5
-rw-r--r--changelogs/unreleased/hashed-storage-migration-path.yml5
-rw-r--r--changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml4
-rw-r--r--changelogs/unreleased/ie-event-polyfill.yml5
-rw-r--r--changelogs/unreleased/import-sources-fix.yml5
-rw-r--r--changelogs/unreleased/improve_sorting_list.yml5
-rw-r--r--changelogs/unreleased/improved-changes-dropdown.yml5
-rw-r--r--changelogs/unreleased/issue_32215.yml5
-rw-r--r--changelogs/unreleased/issue_35873.yml5
-rw-r--r--changelogs/unreleased/issue_39238.yml5
-rw-r--r--changelogs/unreleased/issue_40374.yml5
-rw-r--r--changelogs/unreleased/italicized_emoji.yml5
-rw-r--r--changelogs/unreleased/jk-group-mentions-fix.yml5
-rw-r--r--changelogs/unreleased/jobs-sort-by-id.yml5
-rw-r--r--changelogs/unreleased/lint-changelog-yaml.yml5
-rw-r--r--changelogs/unreleased/mentions-in-comments.yml5
-rw-r--r--changelogs/unreleased/merge-request-notes-performance.yml5
-rw-r--r--changelogs/unreleased/merge-requests-schema-cleanup.yml5
-rw-r--r--changelogs/unreleased/milestone-avatar-issuable-link.yml5
-rw-r--r--changelogs/unreleased/mk-add-user-rate-limits.yml6
-rw-r--r--changelogs/unreleased/mk-clarify-moving-namespaces.yml5
-rw-r--r--changelogs/unreleased/mk-normalize-ldap-user-dns.yml5
-rw-r--r--changelogs/unreleased/mr-side-by-side-breadcrumbs-container.yml5
-rw-r--r--changelogs/unreleased/mr-widget-merged-date-tooltip.yml5
-rw-r--r--changelogs/unreleased/optimise-stuck-ci-jobs-worker.yml5
-rw-r--r--changelogs/unreleased/pawel-update_prometheus_gem_to_well_tested_version.yml5
-rw-r--r--changelogs/unreleased/project-page-clearer.yml5
-rw-r--r--changelogs/unreleased/protected-branches-names.yml5
-rw-r--r--changelogs/unreleased/rc-fix-gh-import-branches-performance.yml5
-rw-r--r--changelogs/unreleased/rd-fix-case-sensative-email-conf-signup.yml5
-rw-r--r--changelogs/unreleased/refactor-animate-js.yml5
-rw-r--r--changelogs/unreleased/refactor-monitoring-service.yml5
-rw-r--r--changelogs/unreleased/remote_user.yml4
-rw-r--r--changelogs/unreleased/remove-temporary-ci-index.yml5
-rw-r--r--changelogs/unreleased/remove-use-key-worker.yml5
-rw-r--r--changelogs/unreleased/remove_repo_prefix_from_api.yml5
-rw-r--r--changelogs/unreleased/replace_emails-feature.yml5
-rw-r--r--changelogs/unreleased/replace_group_links-feature.yml5
-rw-r--r--changelogs/unreleased/replace_milestone-feature.yml5
-rw-r--r--changelogs/unreleased/replace_profile_active_tab-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_archived-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_builds_summary-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_commits_revert-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_issues_award_emoji-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_merge_requests-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_merge_requests_accept-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_merge_requests_revert-feature.yml6
-rw-r--r--changelogs/unreleased/replace_project_service-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_shortcuts-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_snippets-feature.yml5
-rw-r--r--changelogs/unreleased/replace_search-feature.yml5
-rw-r--r--changelogs/unreleased/replace_spinach_wiki-feature.yml5
-rw-r--r--changelogs/unreleased/replace_team_management-feature.yml5
-rw-r--r--changelogs/unreleased/rotated_profile_image.yml5
-rw-r--r--changelogs/unreleased/save-a-query-on-todos-with-no-filters.yml5
-rw-r--r--changelogs/unreleased/sh-fix-username-logging.yml5
-rw-r--r--changelogs/unreleased/sh-show-all-slack-names.yml5
-rw-r--r--changelogs/unreleased/sh-thread-safe-markdown.yml5
-rw-r--r--changelogs/unreleased/skip_confirmation_user_API.yml7
-rw-r--r--changelogs/unreleased/tag-link-size.yml5
-rw-r--r--changelogs/unreleased/tc-geo-read-only-idea.yml5
-rw-r--r--changelogs/unreleased/text-utils.yml5
-rw-r--r--changelogs/unreleased/tm-feature-list-runners-jobs-api.yml5
-rw-r--r--changelogs/unreleased/tm-feature-namespace-by-id-api.yml5
-rw-r--r--changelogs/unreleased/uipolish-fix-2factor-warning.yml5
-rw-r--r--changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml5
-rw-r--r--changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml6
-rw-r--r--changelogs/unreleased/update-merge-worker-metrics.yml5
-rw-r--r--changelogs/unreleased/update-pages-0-6.yml5
-rw-r--r--changelogs/unreleased/use-count_commits-directly.yml5
-rw-r--r--changelogs/unreleased/use-merge-requests-diff-id-column.yml5
-rw-r--r--changelogs/unreleased/valid-branch-name-dash-bug.yml5
-rw-r--r--changelogs/unreleased/voogsgerd-gitlab-ce-daniel-legacy-config.yml5
-rw-r--r--changelogs/unreleased/winh-delete-account-modal.yml5
-rw-r--r--changelogs/unreleased/winh-protected-branch-modal-merged.yml5
-rw-r--r--changelogs/unreleased/winh-sprintf.yml5
-rw-r--r--changelogs/unreleased/winh-subgroups-api.yml5
-rw-r--r--changelogs/unreleased/zj-commit-show-n-1.yml5
-rw-r--r--config/application.rb3
-rw-r--r--config/database.yml.mysql21
-rw-r--r--config/database.yml.postgresql21
-rw-r--r--config/dependency_decisions.yml43
-rw-r--r--config/environments/test.rb1
-rw-r--r--config/gitlab.yml.example14
-rw-r--r--config/initializers/1_settings.rb28
-rw-r--r--config/initializers/7_prometheus_metrics.rb22
-rw-r--r--config/initializers/8_metrics.rb14
-rw-r--r--config/initializers/ar5_batching.rb1
-rw-r--r--config/initializers/batch_loader.rb1
-rw-r--r--config/initializers/devise.rb3
-rw-r--r--config/initializers/gollum.rb28
-rw-r--r--config/initializers/math_lexer.rb2
-rw-r--r--config/initializers/omniauth.rb1
-rw-r--r--config/initializers/peek.rb1
-rw-r--r--config/initializers/plantuml_lexer.rb2
-rw-r--r--config/initializers/postgresql_cte.rb2
-rw-r--r--config/initializers/rack_attack_global.rb61
-rw-r--r--config/karma.config.js9
-rw-r--r--config/locales/doorkeeper.en.yml15
-rw-r--r--config/prometheus/additional_metrics.yml10
-rw-r--r--config/routes.rb28
-rw-r--r--config/routes/ci.rb2
-rw-r--r--config/routes/google_api.rb8
-rw-r--r--config/routes/group.rb81
-rw-r--r--config/routes/help.rb9
-rw-r--r--config/routes/profile.rb1
-rw-r--r--config/routes/project.rb17
-rw-r--r--config/routes/snippets.rb2
-rw-r--r--config/routes/test.rb2
-rw-r--r--config/routes/user.rb12
-rw-r--r--config/sidekiq_queues.yml3
-rw-r--r--config/svg.config.js4
-rw-r--r--config/webpack.config.js9
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb85
-rw-r--r--db/migrate/20150827121444_add_fast_forward_option_to_project.rb6
-rw-r--r--db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb2
-rw-r--r--db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb1
-rw-r--r--db/migrate/20160608195742_add_repository_storage_to_projects.rb2
-rw-r--r--db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb1
-rw-r--r--db/migrate/20160715154212_add_request_access_enabled_to_projects.rb2
-rw-r--r--db/migrate/20160715204316_add_request_access_enabled_to_groups.rb2
-rw-r--r--db/migrate/20160716115711_add_queued_at_to_ci_builds.rb1
-rw-r--r--db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb1
-rw-r--r--db/migrate/20160831223750_remove_features_enabled_from_projects.rb2
-rw-r--r--db/migrate/20160913162434_remove_projects_pushes_since_gc.rb2
-rw-r--r--db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb2
-rw-r--r--db/migrate/20170124193205_add_two_factor_columns_to_users.rb2
-rw-r--r--db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb2
-rw-r--r--db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb2
-rw-r--r--db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb2
-rw-r--r--db/migrate/20170320173259_migrate_assignees.rb1
-rw-r--r--db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb1
-rw-r--r--db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb1
-rw-r--r--db/migrate/20170909150936_add_spent_at_to_timelogs.rb11
-rw-r--r--db/migrate/20170918111708_create_project_custom_attributes.rb15
-rw-r--r--db/migrate/20170918140927_create_group_custom_attributes.rb19
-rw-r--r--db/migrate/20170919211300_remove_temporary_ci_builds_index.rb1
-rw-r--r--db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb38
-rw-r--r--db/migrate/20171012101043_add_circuit_breaker_properties_to_application_settings.rb27
-rw-r--r--db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb78
-rw-r--r--db/migrate/20171013094327_create_new_clusters_architectures.rb68
-rw-r--r--db/migrate/20171017145932_add_new_circuitbreaker_settings_to_application_settings.rb16
-rw-r--r--db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb26
-rw-r--r--db/migrate/20171031100710_create_clusters_kubernetes_helm_apps.rb18
-rw-r--r--db/migrate/20171101130535_add_gitaly_timeout_properties_to_application_settings.rb31
-rw-r--r--db/migrate/20171106101200_create_clusters_kubernetes_ingress_apps.rb21
-rw-r--r--db/migrate/20171106132212_issues_confidential_not_null.rb23
-rw-r--r--db/migrate/20171106133143_rename_application_settings_password_authentication_enabled_to_password_authentication_enabled_for_web.rb15
-rw-r--r--db/migrate/20171106133911_add_password_authentication_enabled_for_git_to_application_settings.rb9
-rw-r--r--db/migrate/20171106135924_issues_milestone_id_foreign_key.rb38
-rw-r--r--db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb45
-rw-r--r--db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb44
-rw-r--r--db/migrate/20171106154015_remove_issues_branch_name.rb13
-rw-r--r--db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb37
-rw-r--r--db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb19
-rw-r--r--db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb43
-rw-r--r--db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb39
-rw-r--r--db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.rb46
-rw-r--r--db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.rb46
-rw-r--r--db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb45
-rw-r--r--db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb39
-rw-r--r--db/migrate/20171115164540_populate_merge_requests_latest_merge_request_diff_id_take_two.rb30
-rw-r--r--db/migrate/20171116135628_add_environment_scope_to_clusters.rb15
-rw-r--r--db/migrate/20171121135738_clean_up_from_merge_request_diffs_and_commits.rb36
-rw-r--r--db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb35
-rw-r--r--db/migrate/20171124125042_add_default_values_to_merge_request_states.rb19
-rw-r--r--db/migrate/20171124125748_populate_missing_merge_request_statuses.rb50
-rw-r--r--db/migrate/20171124132536_make_merge_request_statuses_not_null.rb14
-rw-r--r--db/migrate/limits_to_mysql.rb15
-rw-r--r--db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb1
-rw-r--r--db/post_migrate/20170309171644_reset_relative_position_for_issue.rb1
-rw-r--r--db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb1
-rw-r--r--db/post_migrate/20170406111121_clean_upload_symlinks.rb1
-rw-r--r--db/post_migrate/20170406142253_migrate_user_project_view.rb1
-rw-r--r--db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb1
-rw-r--r--db/post_migrate/20170503004427_update_retried_for_ci_build.rb1
-rw-r--r--db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb1
-rw-r--r--db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb1
-rw-r--r--db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb11
-rw-r--r--db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb1
-rw-r--r--db/post_migrate/20170612071012_move_personal_snippets_files.rb1
-rw-r--r--db/post_migrate/20170613111224_clean_appearance_symlinks.rb1
-rw-r--r--db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb1
-rw-r--r--db/post_migrate/20170927112319_update_notes_type_for_import.rb1
-rw-r--r--db/post_migrate/20171012150314_remove_user_authentication_token.rb20
-rw-r--r--db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb98
-rw-r--r--db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb29
-rw-r--r--db/post_migrate/20171101134435_remove_ref_fetched_from_merge_requests.rb14
-rw-r--r--db/post_migrate/20171106133144_cleanup_application_settings_password_authentication_enabled_rename.rb15
-rw-r--r--db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb19
-rw-r--r--db/post_migrate/20171114104051_remove_empty_fork_networks.rb36
-rw-r--r--db/post_migrate/20171121160421_remove_merge_request_diff_st_commits_and_st_diffs.rb10
-rw-r--r--db/post_migrate/20171124095655_add_index_on_merge_request_diffs_merge_request_id_and_id.rb17
-rw-r--r--db/post_migrate/20171124100152_remove_index_on_merge_request_diffs_merge_request_diff_id.rb17
-rw-r--r--db/post_migrate/20171124150326_reschedule_fork_network_creation.rb27
-rw-r--r--db/schema.rb170
-rw-r--r--doc/README.md6
-rw-r--r--doc/administration/auth/README.md4
-rw-r--r--doc/administration/auth/ldap.md6
-rw-r--r--doc/administration/high_availability/README.md4
-rw-r--r--doc/administration/img/circuitbreaker_config.pngbin0 -> 335073 bytes
-rw-r--r--doc/administration/integration/plantuml.md37
-rw-r--r--doc/administration/logs.md33
-rw-r--r--doc/administration/monitoring/github_imports.md101
-rw-r--r--doc/administration/monitoring/performance/performance_bar.md6
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md4
-rw-r--r--doc/administration/operations/sidekiq_memory_killer.md4
-rw-r--r--doc/administration/raketasks/github_import.md1
-rw-r--r--doc/administration/raketasks/storage.md97
-rw-r--r--doc/administration/repository_storage_paths.md67
-rw-r--r--doc/administration/repository_storage_types.md31
-rw-r--r--doc/administration/repository_storages.md4
-rw-r--r--doc/administration/troubleshooting/debug.md30
-rw-r--r--doc/administration/troubleshooting/sidekiq.md2
-rw-r--r--doc/api/README.md160
-rw-r--r--doc/api/custom_attributes.md27
-rw-r--r--doc/api/environments.md2
-rw-r--r--doc/api/groups.md91
-rw-r--r--doc/api/merge_requests.md28
-rw-r--r--doc/api/namespaces.md52
-rw-r--r--doc/api/notes.md33
-rw-r--r--doc/api/pages_domains.md195
-rw-r--r--doc/api/pipelines.md2
-rw-r--r--doc/api/projects.md6
-rw-r--r--doc/api/protected_branches.md2
-rw-r--r--doc/api/repositories.md2
-rw-r--r--doc/api/runners.md85
-rw-r--r--doc/api/services.md75
-rw-r--r--doc/api/session.md55
-rw-r--r--doc/api/settings.md126
-rw-r--r--doc/api/users.md4
-rw-r--r--doc/articles/index.md1
-rw-r--r--doc/articles/runner_autoscale_aws/index.md410
-rw-r--r--doc/ci/README.md6
-rw-r--r--doc/ci/docker/README.md4
-rw-r--r--doc/ci/docker/using_docker_build.md8
-rw-r--r--doc/ci/docker/using_docker_images.md8
-rw-r--r--doc/ci/enable_or_disable_ci.md6
-rw-r--r--doc/ci/examples/README.md4
-rw-r--r--doc/ci/examples/deployment/composer-npm-deploy.md14
-rw-r--r--doc/ci/examples/php.md4
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md23
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md24
-rw-r--r--doc/ci/examples/test-clojure-application.md12
-rw-r--r--doc/ci/examples/test-phoenix-application.md10
-rw-r--r--doc/ci/git_submodules.md6
-rw-r--r--doc/ci/permissions/README.md2
-rw-r--r--doc/ci/quick_start/README.md2
-rw-r--r--doc/ci/runners/README.md2
-rw-r--r--doc/ci/services/README.md6
-rw-r--r--doc/ci/services/docker-services.md12
-rw-r--r--doc/ci/variables/README.md5
-rw-r--r--doc/ci/yaml/README.md8
-rw-r--r--doc/development/README.md125
-rw-r--r--doc/development/database_debugging.md55
-rw-r--r--doc/development/doc_styleguide.md10
-rw-r--r--doc/development/ee_features.md382
-rw-r--r--doc/development/fe_guide/axios.md68
-rw-r--r--doc/development/fe_guide/dropdowns.md38
-rw-r--r--doc/development/fe_guide/emojis.md27
-rw-r--r--doc/development/fe_guide/icons.md30
-rw-r--r--doc/development/fe_guide/index.md12
-rw-r--r--doc/development/fe_guide/style_guide_js.md30
-rw-r--r--doc/development/fe_guide/testing.md255
-rw-r--r--doc/development/fe_guide/vue.md119
-rw-r--r--doc/development/file_storage.md49
-rw-r--r--doc/development/gitaly.md24
-rw-r--r--doc/development/github_importer.md209
-rw-r--r--doc/development/i18n/externalization.md67
-rw-r--r--doc/development/i18n/translation.md12
-rw-r--r--doc/development/licensing.md8
-rw-r--r--doc/development/limit_ee_conflicts.md6
-rw-r--r--doc/development/migration_style_guide.md38
-rw-r--r--doc/development/profiling.md10
-rw-r--r--doc/development/query_recorder.md46
-rw-r--r--doc/development/rake_tasks.md12
-rw-r--r--doc/development/testing.md567
-rw-r--r--doc/development/testing_guide/best_practices.md303
-rw-r--r--doc/development/testing_guide/ci.md52
-rw-r--r--doc/development/testing_guide/flaky_tests.md74
-rw-r--r--doc/development/testing_guide/frontend_testing.md254
-rw-r--r--doc/development/testing_guide/img/testing_triangle.png (renamed from doc/development/fe_guide/img/testing_triangle.png)bin11836 -> 11836 bytes
-rw-r--r--doc/development/testing_guide/index.md91
-rw-r--r--doc/development/testing_guide/testing_levels.md173
-rw-r--r--doc/development/testing_guide/testing_rake_tasks.md39
-rw-r--r--doc/development/ux_guide/components.md33
-rw-r--r--doc/development/ux_guide/img/modals-general-confimation-dialog.pngbin0 -> 51205 bytes
-rw-r--r--doc/development/ux_guide/img/modals-layout-for-modals.pngbin0 -> 68203 bytes
-rw-r--r--doc/development/ux_guide/img/modals-special-confimation-dialog.pngbin0 -> 89978 bytes
-rw-r--r--doc/development/ux_guide/img/modals-three-buttons.pngbin0 -> 54927 bytes
-rw-r--r--doc/development/ux_guide/resources.md6
-rw-r--r--doc/development/ux_guide/users.md75
-rw-r--r--doc/development/what_requires_downtime.md2
-rw-r--r--doc/gitlab-basics/README.md4
-rw-r--r--doc/gitlab-basics/add-merge-request.md23
-rw-r--r--doc/gitlab-basics/create-project.md2
-rw-r--r--doc/gitlab-basics/img/create_new_project_info.pngbin82725 -> 75470 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_new.pngbin2234 -> 0 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_select_branch.pngbin20332 -> 16668 bytes
-rw-r--r--doc/gitlab-basics/img/project_navbar.pngbin3259 -> 0 bytes
-rw-r--r--doc/install/README.md4
-rw-r--r--doc/install/installation.md14
-rw-r--r--doc/install/kubernetes/gitlab_chart.md6
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md10
-rw-r--r--doc/install/kubernetes/index.md14
-rw-r--r--doc/install/relative_url.md16
-rw-r--r--doc/install/requirements.md12
-rw-r--r--doc/integration/README.md4
-rw-r--r--doc/integration/external-issue-tracker.md1
-rw-r--r--doc/integration/google.md125
-rw-r--r--doc/integration/saml.md11
-rw-r--r--doc/integration/trello_power_up.md2
-rw-r--r--doc/intro/README.md4
-rw-r--r--doc/legal/README.md4
-rw-r--r--doc/legal/corporate_contributor_license_agreement.md31
-rw-r--r--doc/legal/individual_contributor_license_agreement.md27
-rw-r--r--doc/migrate_ci_to_ce/README.md9
-rw-r--r--doc/policy/maintenance.md86
-rw-r--r--doc/raketasks/README.md4
-rw-r--r--doc/raketasks/backup_restore.md123
-rw-r--r--doc/raketasks/import.md47
-rw-r--r--doc/raketasks/user_management.md15
-rw-r--r--doc/security/README.md4
-rw-r--r--doc/ssh/README.md8
-rw-r--r--doc/system_hooks/system_hooks.md72
-rw-r--r--doc/topics/autodevops/img/auto_devops_settings.pngbin0 -> 67845 bytes
-rw-r--r--doc/topics/autodevops/index.md16
-rw-r--r--doc/university/README.md16
-rw-r--r--doc/university/bookclub/booklist.md4
-rw-r--r--doc/university/bookclub/index.md4
-rw-r--r--doc/university/glossary/README.md157
-rw-r--r--doc/university/high-availability/aws/README.md4
-rw-r--r--doc/university/process/README.md6
-rw-r--r--doc/university/support/README.md6
-rw-r--r--doc/university/training/end-user/README.md4
-rw-r--r--doc/university/training/gitlab_flow.md4
-rw-r--r--doc/university/training/index.md4
-rw-r--r--doc/university/training/topics/additional_resources.md6
-rw-r--r--doc/university/training/topics/agile_git.md4
-rw-r--r--doc/university/training/topics/bisect.md4
-rw-r--r--doc/university/training/topics/cherry_picking.md4
-rw-r--r--doc/university/training/topics/env_setup.md4
-rw-r--r--doc/university/training/topics/explore_gitlab.md4
-rw-r--r--doc/university/training/topics/feature_branching.md4
-rw-r--r--doc/university/training/topics/getting_started.md4
-rw-r--r--doc/university/training/topics/git_add.md4
-rw-r--r--doc/university/training/topics/git_intro.md4
-rw-r--r--doc/university/training/topics/git_log.md8
-rw-r--r--doc/university/training/topics/gitlab_flow.md4
-rw-r--r--doc/university/training/topics/merge_conflicts.md4
-rw-r--r--doc/university/training/topics/merge_requests.md4
-rw-r--r--doc/university/training/topics/rollback_commits.md4
-rw-r--r--doc/university/training/topics/stash.md4
-rw-r--r--doc/university/training/topics/subtree.md8
-rw-r--r--doc/university/training/topics/tags.md4
-rw-r--r--doc/university/training/topics/unstage.md4
-rw-r--r--doc/university/training/user_training.md4
-rw-r--r--doc/update/10.0-to-10.1.md10
-rw-r--r--doc/update/10.1-to-10.2.md360
-rw-r--r--doc/update/2.6-to-3.0.md4
-rw-r--r--doc/update/2.9-to-3.0.md4
-rw-r--r--doc/update/3.0-to-3.1.md4
-rw-r--r--doc/update/3.1-to-4.0.md4
-rw-r--r--doc/update/4.0-to-4.1.md4
-rw-r--r--doc/update/4.1-to-4.2.md4
-rw-r--r--doc/update/4.2-to-5.0.md4
-rw-r--r--doc/update/5.0-to-5.1.md4
-rw-r--r--doc/update/5.1-to-5.2.md4
-rw-r--r--doc/update/5.1-to-5.4.md4
-rw-r--r--doc/update/5.1-to-6.0.md4
-rw-r--r--doc/update/5.2-to-5.3.md4
-rw-r--r--doc/update/5.3-to-5.4.md4
-rw-r--r--doc/update/5.4-to-6.0.md4
-rw-r--r--doc/update/6.0-to-6.1.md4
-rw-r--r--doc/update/6.1-to-6.2.md4
-rw-r--r--doc/update/6.2-to-6.3.md4
-rw-r--r--doc/update/6.3-to-6.4.md4
-rw-r--r--doc/update/6.4-to-6.5.md4
-rw-r--r--doc/update/6.5-to-6.6.md4
-rw-r--r--doc/update/6.6-to-6.7.md4
-rw-r--r--doc/update/6.7-to-6.8.md4
-rw-r--r--doc/update/6.8-to-6.9.md4
-rw-r--r--doc/update/6.9-to-7.0.md4
-rw-r--r--doc/update/6.x-or-7.x-to-7.14.md4
-rw-r--r--doc/update/7.0-to-7.1.md4
-rw-r--r--doc/update/7.1-to-7.2.md4
-rw-r--r--doc/update/7.10-to-7.11.md4
-rw-r--r--doc/update/7.11-to-7.12.md4
-rw-r--r--doc/update/7.12-to-7.13.md4
-rw-r--r--doc/update/7.13-to-7.14.md4
-rw-r--r--doc/update/7.14-to-8.0.md4
-rw-r--r--doc/update/7.2-to-7.3.md4
-rw-r--r--doc/update/7.3-to-7.4.md4
-rw-r--r--doc/update/7.4-to-7.5.md4
-rw-r--r--doc/update/7.5-to-7.6.md4
-rw-r--r--doc/update/7.6-to-7.7.md4
-rw-r--r--doc/update/7.7-to-7.8.md4
-rw-r--r--doc/update/7.8-to-7.9.md4
-rw-r--r--doc/update/7.9-to-7.10.md4
-rw-r--r--doc/update/8.0-to-8.1.md4
-rw-r--r--doc/update/8.1-to-8.2.md4
-rw-r--r--doc/update/8.10-to-8.11.md4
-rw-r--r--doc/update/8.11-to-8.12.md4
-rw-r--r--doc/update/8.12-to-8.13.md4
-rw-r--r--doc/update/8.13-to-8.14.md4
-rw-r--r--doc/update/8.14-to-8.15.md4
-rw-r--r--doc/update/8.15-to-8.16.md4
-rw-r--r--doc/update/8.16-to-8.17.md4
-rw-r--r--doc/update/8.17-to-9.0.md4
-rw-r--r--doc/update/8.2-to-8.3.md4
-rw-r--r--doc/update/8.3-to-8.4.md4
-rw-r--r--doc/update/8.4-to-8.5.md4
-rw-r--r--doc/update/8.5-to-8.6.md4
-rw-r--r--doc/update/8.6-to-8.7.md4
-rw-r--r--doc/update/8.7-to-8.8.md4
-rw-r--r--doc/update/8.8-to-8.9.md4
-rw-r--r--doc/update/8.9-to-8.10.md4
-rw-r--r--doc/update/9.0-to-9.1.md4
-rw-r--r--doc/update/9.1-to-9.2.md4
-rw-r--r--doc/update/9.2-to-9.3.md4
-rw-r--r--doc/update/9.3-to-9.4.md4
-rw-r--r--doc/update/9.4-to-9.5.md4
-rw-r--r--doc/update/9.5-to-10.0.md4
-rw-r--r--doc/update/patch_versions.md4
-rw-r--r--doc/update/upgrader.md4
-rw-r--r--doc/user/discussions/img/image_resolved_discussion.pngbin0 -> 48234 bytes
-rw-r--r--doc/user/discussions/img/onion_skin_view.pngbin0 -> 45053 bytes
-rw-r--r--doc/user/discussions/img/start_image_discussion.gifbin0 -> 146627 bytes
-rw-r--r--doc/user/discussions/img/swipe_view.pngbin0 -> 16483 bytes
-rw-r--r--doc/user/discussions/img/two_up_view.pngbin0 -> 61759 bytes
-rw-r--r--doc/user/discussions/index.md74
-rw-r--r--doc/user/markdown.md32
-rw-r--r--doc/user/permissions.md8
-rw-r--r--doc/user/profile/index.md2
-rw-r--r--doc/user/profile/personal_access_tokens.md12
-rw-r--r--doc/user/profile/preferences.md3
-rw-r--r--doc/user/project/clusters/index.md113
-rw-r--r--doc/user/project/container_registry.md3
-rw-r--r--doc/user/project/img/label_priority_sort_order.pngbin0 -> 101667 bytes
-rw-r--r--doc/user/project/img/labels_filter_by_priority.pngbin38717 -> 0 bytes
-rw-r--r--doc/user/project/img/priority_sort_order.pngbin0 -> 102242 bytes
-rw-r--r--doc/user/project/import/github.md29
-rw-r--r--doc/user/project/import/index.md2
-rw-r--r--doc/user/project/index.md2
-rw-r--r--doc/user/project/integrations/custom_issue_tracker.md20
-rw-r--r--doc/user/project/integrations/img/webhook_logs.pngbin24066 -> 132319 bytes
-rw-r--r--doc/user/project/integrations/project_services.md1
-rw-r--r--doc/user/project/integrations/prometheus_library/kubernetes.md4
-rw-r--r--doc/user/project/integrations/webhooks.md128
-rw-r--r--doc/user/project/issue_board.md26
-rw-r--r--doc/user/project/issues/automatic_issue_closing.md16
-rw-r--r--doc/user/project/labels.md29
-rw-r--r--doc/user/project/members/index.md2
-rw-r--r--doc/user/project/merge_requests/index.md1
-rw-r--r--doc/user/project/milestones/index.md3
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md2
-rw-r--r--doc/user/project/pages/getting_started_part_one.md2
-rw-r--r--doc/user/project/pages/introduction.md8
-rw-r--r--doc/user/project/pipelines/job_artifacts.md6
-rw-r--r--doc/user/project/pipelines/schedules.md4
-rw-r--r--doc/user/project/pipelines/settings.md4
-rw-r--r--doc/user/project/quick_actions.md2
-rw-r--r--doc/user/project/repository/branches/index.md2
-rw-r--r--doc/user/project/repository/web_editor.md2
-rw-r--r--doc/user/project/settings/import_export.md3
-rw-r--r--doc/workflow/README.md4
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--doc/workflow/importing/README.md2
-rw-r--r--doc/workflow/importing/import_projects_from_bitbucket.md2
-rw-r--r--doc/workflow/importing/import_projects_from_fogbugz.md2
-rw-r--r--doc/workflow/importing/import_projects_from_gitea.md2
-rw-r--r--doc/workflow/importing/import_projects_from_github.md2
-rw-r--r--doc/workflow/importing/import_projects_from_gitlab_com.md2
-rw-r--r--doc/workflow/importing/migrating_from_svn.md2
-rw-r--r--doc/workflow/shortcuts.md2
-rw-r--r--features/explore/groups.feature12
-rw-r--r--features/explore/projects.feature144
-rw-r--r--features/steps/explore/projects.rb145
-rw-r--r--features/steps/profile/notifications.rb2
-rw-r--r--features/steps/project/commits/branches.rb8
-rw-r--r--features/steps/project/commits/commits.rb2
-rw-r--r--features/steps/project/issues/filter_labels.rb6
-rw-r--r--features/steps/project/issues/issues.rb6
-rw-r--r--features/steps/project/issues/labels.rb2
-rw-r--r--features/steps/project/issues/milestones.rb3
-rw-r--r--features/steps/project/source/browse_files.rb14
-rw-r--r--features/steps/shared/diff_note.rb9
-rw-r--r--features/steps/shared/note.rb2
-rw-r--r--features/steps/shared/paths.rb15
-rw-r--r--features/steps/shared/project.rb18
-rw-r--r--features/support/capybara.rb29
-rw-r--r--features/support/capybara_helpers.rb10
-rw-r--r--fixtures/emojis/aliases.json4
-rw-r--r--fixtures/emojis/digests.json18
-rwxr-xr-xfixtures/emojis/generate_aliases.rb18
-rw-r--r--fixtures/emojis/index.json33
-rw-r--r--lib/additional_email_headers_interceptor.rb6
-rw-r--r--lib/api/api.rb14
-rw-r--r--lib/api/api_guard.rb130
-rw-r--r--lib/api/branches.rb35
-rw-r--r--lib/api/commits.rb18
-rw-r--r--lib/api/entities.rb80
-rw-r--r--lib/api/groups.rb67
-rw-r--r--lib/api/helpers.rb109
-rw-r--r--lib/api/helpers/custom_validators.rb1
-rw-r--r--lib/api/helpers/internal_helpers.rb20
-rw-r--r--lib/api/helpers/runner.rb1
-rw-r--r--lib/api/internal.rb4
-rw-r--r--lib/api/issues.rb16
-rw-r--r--lib/api/jobs.rb2
-rw-r--r--lib/api/merge_requests.rb8
-rw-r--r--lib/api/namespaces.rb10
-rw-r--r--lib/api/notes.rb9
-rw-r--r--lib/api/pages_domains.rb137
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/api/protected_branches.rb4
-rw-r--r--lib/api/repositories.rb4
-rw-r--r--lib/api/runners.rb27
-rw-r--r--lib/api/services.rb31
-rw-r--r--lib/api/session.rb20
-rw-r--r--lib/api/settings.rb16
-rw-r--r--lib/api/snippets.rb1
-rw-r--r--lib/api/users.rb7
-rw-r--r--lib/api/v3/branches.rb6
-rw-r--r--lib/api/v3/builds.rb4
-rw-r--r--lib/api/v3/commits.rb18
-rw-r--r--lib/api/v3/entities.rb4
-rw-r--r--lib/api/v3/repositories.rb6
-rw-r--r--lib/api/v3/runners.rb1
-rw-r--r--lib/api/v3/services.rb20
-rw-r--r--lib/api/v3/settings.rb8
-rw-r--r--lib/api/v3/snippets.rb2
-rw-r--r--lib/backup/manager.rb80
-rw-r--r--lib/backup/repository.rb24
-rw-r--r--lib/banzai.rb4
-rw-r--r--lib/banzai/filter/absolute_link_filter.rb34
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb33
-rw-r--r--lib/banzai/filter/mermaid_filter.rb20
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb2
-rw-r--r--lib/banzai/filter/reference_filter.rb6
-rw-r--r--lib/banzai/filter/sanitization_filter.rb14
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb35
-rw-r--r--lib/banzai/filter/user_reference_filter.rb57
-rw-r--r--lib/banzai/note_renderer.rb21
-rw-r--r--lib/banzai/object_renderer.rb8
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/banzai/pipeline/post_process_pipeline.rb3
-rw-r--r--lib/banzai/querying.rb2
-rw-r--r--lib/banzai/reference_parser/user_parser.rb1
-rw-r--r--lib/banzai/renderer.rb13
-rw-r--r--lib/banzai/request_store_reference_cache.rb27
-rw-r--r--lib/constraints/group_url_constrainer.rb2
-rw-r--r--lib/constraints/project_url_constrainer.rb2
-rw-r--r--lib/constraints/user_url_constrainer.rb2
-rw-r--r--lib/declarative_policy.rb2
-rw-r--r--lib/declarative_policy/base.rb2
-rw-r--r--lib/declarative_policy/cache.rb2
-rw-r--r--lib/declarative_policy/rule.rb5
-rw-r--r--lib/declarative_policy/runner.rb1
-rw-r--r--lib/feature.rb14
-rw-r--r--lib/file_size_validator.rb1
-rw-r--r--lib/github/client.rb54
-rw-r--r--lib/github/collection.rb29
-rw-r--r--lib/github/error.rb3
-rw-r--r--lib/github/import.rb376
-rw-r--r--lib/github/import/issue.rb13
-rw-r--r--lib/github/import/legacy_diff_note.rb12
-rw-r--r--lib/github/import/merge_request.rb13
-rw-r--r--lib/github/import/note.rb13
-rw-r--r--lib/github/rate_limit.rb27
-rw-r--r--lib/github/repositories.rb19
-rw-r--r--lib/github/representation/base.rb30
-rw-r--r--lib/github/representation/branch.rb55
-rw-r--r--lib/github/representation/comment.rb42
-rw-r--r--lib/github/representation/issuable.rb37
-rw-r--r--lib/github/representation/issue.rb27
-rw-r--r--lib/github/representation/label.rb13
-rw-r--r--lib/github/representation/milestone.rb25
-rw-r--r--lib/github/representation/pull_request.rb71
-rw-r--r--lib/github/representation/release.rb17
-rw-r--r--lib/github/representation/repo.rb6
-rw-r--r--lib/github/representation/user.rb15
-rw-r--r--lib/github/response.rb25
-rw-r--r--lib/github/user.rb24
-rw-r--r--lib/gitlab/access.rb6
-rw-r--r--lib/gitlab/auth.rb66
-rw-r--r--lib/gitlab/auth/request_authenticator.rb25
-rw-r--r--lib/gitlab/auth/user_auth_finders.rb109
-rw-r--r--lib/gitlab/background_migration/.rubocop.yml52
-rw-r--r--lib/gitlab/background_migration/create_fork_network_memberships_range.rb21
-rw-r--r--lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb4
-rw-r--r--lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb4
-rw-r--r--lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb7
-rw-r--r--lib/gitlab/background_migration/migrate_build_stage_id_reference.rb3
-rw-r--r--lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb4
-rw-r--r--lib/gitlab/background_migration/migrate_stage_status.rb4
-rw-r--r--lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb4
-rw-r--r--lib/gitlab/background_migration/move_personal_snippet_files.rb4
-rw-r--r--lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb7
-rw-r--r--lib/gitlab/background_migration/populate_fork_networks_range.rb82
-rw-r--r--lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb33
-rw-r--r--lib/gitlab/bare_repository_import/importer.rb101
-rw-r--r--lib/gitlab/bare_repository_import/repository.rb42
-rw-r--r--lib/gitlab/bare_repository_importer.rb97
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb6
-rw-r--r--lib/gitlab/changes_list.rb1
-rw-r--r--lib/gitlab/checks/change_access.rb12
-rw-r--r--lib/gitlab/checks/lfs_integrity.rb27
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb1
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb3
-rw-r--r--lib/gitlab/ci/build/image.rb1
-rw-r--r--lib/gitlab/ci/config/entry/image.rb1
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb1
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb2
-rw-r--r--lib/gitlab/ci/status/build/failed_allowed.rb2
-rw-r--r--lib/gitlab/ci/status/build/play.rb2
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb2
-rw-r--r--lib/gitlab/ci/status/build/stop.rb2
-rw-r--r--lib/gitlab/ci/status/canceled.rb2
-rw-r--r--lib/gitlab/ci/status/created.rb2
-rw-r--r--lib/gitlab/ci/status/failed.rb2
-rw-r--r--lib/gitlab/ci/status/manual.rb2
-rw-r--r--lib/gitlab/ci/status/pending.rb2
-rw-r--r--lib/gitlab/ci/status/running.rb2
-rw-r--r--lib/gitlab/ci/status/skipped.rb2
-rw-r--r--lib/gitlab/ci/status/success.rb2
-rw-r--r--lib/gitlab/ci/status/success_warning.rb2
-rw-r--r--lib/gitlab/conflict/file.rb88
-rw-r--r--lib/gitlab/conflict/file_collection.rb68
-rw-r--r--lib/gitlab/conflict/parser.rb74
-rw-r--r--lib/gitlab/conflict/resolution_error.rb5
-rw-r--r--lib/gitlab/cycle_analytics/plan_event_fetcher.rb8
-rw-r--r--lib/gitlab/daemon.rb3
-rw-r--r--lib/gitlab/database.rb33
-rw-r--r--lib/gitlab/database/grant.rb30
-rw-r--r--lib/gitlab/database/migration_helpers.rb9
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb12
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb8
-rw-r--r--lib/gitlab/diff/file.rb20
-rw-r--r--lib/gitlab/diff/file_collection/base.rb5
-rw-r--r--lib/gitlab/diff/inline_diff.rb1
-rw-r--r--lib/gitlab/diff/line_code.rb9
-rw-r--r--lib/gitlab/diff/parser.rb5
-rw-r--r--lib/gitlab/diff/position.rb7
-rw-r--r--lib/gitlab/ee_compat_check.rb87
-rw-r--r--lib/gitlab/email/handler/unsubscribe_handler.rb1
-rw-r--r--lib/gitlab/encoding_helper.rb11
-rw-r--r--lib/gitlab/file_detector.rb28
-rw-r--r--lib/gitlab/fogbugz_import/client.rb1
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb2
-rw-r--r--lib/gitlab/gcp/model.rb13
-rw-r--r--lib/gitlab/git.rb4
-rw-r--r--lib/gitlab/git/blob.rb60
-rw-r--r--lib/gitlab/git/branch.rb8
-rw-r--r--lib/gitlab/git/commit.rb3
-rw-r--r--lib/gitlab/git/conflict/file.rb86
-rw-r--r--lib/gitlab/git/conflict/parser.rb91
-rw-r--r--lib/gitlab/git/conflict/resolver.rb91
-rw-r--r--lib/gitlab/git/env.rb17
-rw-r--r--lib/gitlab/git/hooks_service.rb2
-rw-r--r--lib/gitlab/git/lfs_changes.rb33
-rw-r--r--lib/gitlab/git/operation_service.rb22
-rw-r--r--lib/gitlab/git/popen.rb72
-rw-r--r--lib/gitlab/git/remote_repository.rb82
-rw-r--r--lib/gitlab/git/repository.rb240
-rw-r--r--lib/gitlab/git/repository_mirroring.rb104
-rw-r--r--lib/gitlab/git/rev_list.rb60
-rw-r--r--lib/gitlab/git/storage.rb1
-rw-r--r--lib/gitlab/git/storage/circuit_breaker.rb55
-rw-r--r--lib/gitlab/git/storage/circuit_breaker_settings.rb37
-rw-r--r--lib/gitlab/git/storage/forked_storage_check.rb13
-rw-r--r--lib/gitlab/git/storage/health.rb20
-rw-r--r--lib/gitlab/git/storage/null_circuit_breaker.rb13
-rw-r--r--lib/gitlab/git/user.rb12
-rw-r--r--lib/gitlab/git/wiki.rb208
-rw-r--r--lib/gitlab/git/wiki_file.rb3
-rw-r--r--lib/gitlab/git_access.rb4
-rw-r--r--lib/gitlab/gitaly_client.rb115
-rw-r--r--lib/gitlab/gitaly_client/attributes_bag.rb31
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb32
-rw-r--r--lib/gitlab/gitaly_client/diff.rb16
-rw-r--r--lib/gitlab/gitaly_client/diff_stitcher.rb2
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb56
-rw-r--r--lib/gitlab/gitaly_client/queue_enumerator.rb28
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb21
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb33
-rw-r--r--lib/gitlab/gitaly_client/util.rb20
-rw-r--r--lib/gitlab/gitaly_client/wiki_file.rb9
-rw-r--r--lib/gitlab/gitaly_client/wiki_page.rb25
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb170
-rw-r--r--lib/gitlab/github_import.rb38
-rw-r--r--lib/gitlab/github_import/base_formatter.rb26
-rw-r--r--lib/gitlab/github_import/branch_formatter.rb37
-rw-r--r--lib/gitlab/github_import/bulk_importing.rb25
-rw-r--r--lib/gitlab/github_import/caching.rb151
-rw-r--r--lib/gitlab/github_import/client.rb263
-rw-r--r--lib/gitlab/github_import/comment_formatter.rb69
-rw-r--r--lib/gitlab/github_import/importer.rb329
-rw-r--r--lib/gitlab/github_import/importer/diff_note_importer.rb63
-rw-r--r--lib/gitlab/github_import/importer/diff_notes_importer.rb31
-rw-r--r--lib/gitlab/github_import/importer/issue_and_label_links_importer.rb25
-rw-r--r--lib/gitlab/github_import/importer/issue_importer.rb81
-rw-r--r--lib/gitlab/github_import/importer/issues_importer.rb35
-rw-r--r--lib/gitlab/github_import/importer/label_links_importer.rb52
-rw-r--r--lib/gitlab/github_import/importer/labels_importer.rb55
-rw-r--r--lib/gitlab/github_import/importer/milestones_importer.rb58
-rw-r--r--lib/gitlab/github_import/importer/note_importer.rb54
-rw-r--r--lib/gitlab/github_import/importer/notes_importer.rb31
-rw-r--r--lib/gitlab/github_import/importer/pull_request_importer.rb91
-rw-r--r--lib/gitlab/github_import/importer/pull_requests_importer.rb83
-rw-r--r--lib/gitlab/github_import/importer/releases_importer.rb55
-rw-r--r--lib/gitlab/github_import/importer/repository_importer.rb83
-rw-r--r--lib/gitlab/github_import/issuable_finder.rb81
-rw-r--r--lib/gitlab/github_import/issuable_formatter.rb66
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb32
-rw-r--r--lib/gitlab/github_import/label_finder.rb37
-rw-r--r--lib/gitlab/github_import/label_formatter.rb37
-rw-r--r--lib/gitlab/github_import/markdown_text.rb30
-rw-r--r--lib/gitlab/github_import/milestone_finder.rb40
-rw-r--r--lib/gitlab/github_import/milestone_formatter.rb40
-rw-r--r--lib/gitlab/github_import/page_counter.rb31
-rw-r--r--lib/gitlab/github_import/parallel_importer.rb48
-rw-r--r--lib/gitlab/github_import/parallel_scheduling.rb162
-rw-r--r--lib/gitlab/github_import/project_creator.rb52
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb90
-rw-r--r--lib/gitlab/github_import/rate_limit_error.rb9
-rw-r--r--lib/gitlab/github_import/release_formatter.rb27
-rw-r--r--lib/gitlab/github_import/representation.rb25
-rw-r--r--lib/gitlab/github_import/representation/diff_note.rb87
-rw-r--r--lib/gitlab/github_import/representation/expose_attribute.rb26
-rw-r--r--lib/gitlab/github_import/representation/issue.rb80
-rw-r--r--lib/gitlab/github_import/representation/note.rb70
-rw-r--r--lib/gitlab/github_import/representation/pull_request.rb114
-rw-r--r--lib/gitlab/github_import/representation/to_hash.rb31
-rw-r--r--lib/gitlab/github_import/representation/user.rb34
-rw-r--r--lib/gitlab/github_import/sequential_importer.rb50
-rw-r--r--lib/gitlab/github_import/user_finder.rb164
-rw-r--r--lib/gitlab/github_import/user_formatter.rb45
-rw-r--r--lib/gitlab/github_import/wiki_formatter.rb19
-rw-r--r--lib/gitlab/gitlab_import/client.rb1
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/gitlab/group_hierarchy.rb30
-rw-r--r--lib/gitlab/hook_data/issuable_builder.rb56
-rw-r--r--lib/gitlab/hook_data/issue_builder.rb55
-rw-r--r--lib/gitlab/hook_data/merge_request_builder.rb62
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml6
-rw-r--r--lib/gitlab/import_export/importer.rb4
-rw-r--r--lib/gitlab/import_export/merge_request_parser.rb4
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb4
-rw-r--r--lib/gitlab/import_export/relation_factory.rb60
-rw-r--r--lib/gitlab/import_export/uploads_saver.rb3
-rw-r--r--lib/gitlab/import_sources.rb4
-rw-r--r--lib/gitlab/issuable_metadata.rb8
-rw-r--r--lib/gitlab/job_waiter.rb8
-rw-r--r--lib/gitlab/kubernetes/helm.rb96
-rw-r--r--lib/gitlab/kubernetes/namespace.rb30
-rw-r--r--lib/gitlab/kubernetes/pod.rb12
-rw-r--r--lib/gitlab/ldap/auth_hash.rb2
-rw-r--r--lib/gitlab/ldap/authentication.rb1
-rw-r--r--lib/gitlab/ldap/user.rb6
-rw-r--r--lib/gitlab/legacy_github_import/base_formatter.rb26
-rw-r--r--lib/gitlab/legacy_github_import/branch_formatter.rb37
-rw-r--r--lib/gitlab/legacy_github_import/client.rb148
-rw-r--r--lib/gitlab/legacy_github_import/comment_formatter.rb69
-rw-r--r--lib/gitlab/legacy_github_import/importer.rb334
-rw-r--r--lib/gitlab/legacy_github_import/issuable_formatter.rb66
-rw-r--r--lib/gitlab/legacy_github_import/issue_formatter.rb32
-rw-r--r--lib/gitlab/legacy_github_import/label_formatter.rb37
-rw-r--r--lib/gitlab/legacy_github_import/milestone_formatter.rb40
-rw-r--r--lib/gitlab/legacy_github_import/project_creator.rb52
-rw-r--r--lib/gitlab/legacy_github_import/pull_request_formatter.rb90
-rw-r--r--lib/gitlab/legacy_github_import/release_formatter.rb27
-rw-r--r--lib/gitlab/legacy_github_import/user_formatter.rb45
-rw-r--r--lib/gitlab/legacy_github_import/wiki_formatter.rb19
-rw-r--r--lib/gitlab/lfs_token.rb4
-rw-r--r--lib/gitlab/logger.rb12
-rw-r--r--lib/gitlab/metrics/background_transaction.rb14
-rw-r--r--lib/gitlab/metrics/base_sampler.rb63
-rw-r--r--lib/gitlab/metrics/influx_db.rb31
-rw-r--r--lib/gitlab/metrics/influx_sampler.rb101
-rw-r--r--lib/gitlab/metrics/instrumentation.rb11
-rw-r--r--lib/gitlab/metrics/method_call.rb45
-rw-r--r--lib/gitlab/metrics/prometheus.rb30
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb67
-rw-r--r--lib/gitlab/metrics/samplers/base_sampler.rb64
-rw-r--r--lib/gitlab/metrics/samplers/influx_sampler.rb103
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb111
-rw-r--r--lib/gitlab/metrics/samplers/unicorn_sampler.rb50
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb2
-rw-r--r--lib/gitlab/metrics/subscribers/action_view.rb14
-rw-r--r--lib/gitlab/metrics/subscribers/active_record.rb15
-rw-r--r--lib/gitlab/metrics/subscribers/rails_cache.rb42
-rw-r--r--lib/gitlab/metrics/system.rb4
-rw-r--r--lib/gitlab/metrics/transaction.rb117
-rw-r--r--lib/gitlab/metrics/unicorn_sampler.rb48
-rw-r--r--lib/gitlab/metrics/web_transaction.rb82
-rw-r--r--lib/gitlab/middleware/go.rb15
-rw-r--r--lib/gitlab/middleware/rails_queue_duration.rb13
-rw-r--r--lib/gitlab/middleware/read_only.rb19
-rw-r--r--lib/gitlab/multi_collection_paginator.rb63
-rw-r--r--lib/gitlab/o_auth/user.rb4
-rw-r--r--lib/gitlab/optimistic_locking.rb1
-rw-r--r--lib/gitlab/path_regex.rb20
-rw-r--r--lib/gitlab/performance_bar/peek_query_tracker.rb4
-rw-r--r--lib/gitlab/project_search_results.rb7
-rw-r--r--lib/gitlab/project_template.rb12
-rw-r--r--lib/gitlab/prometheus/queries/query_additional_metrics.rb2
-rw-r--r--lib/gitlab/quick_actions/spend_time_and_date_separator.rb54
-rw-r--r--lib/gitlab/regex.rb2
-rw-r--r--lib/gitlab/routing.rb19
-rw-r--r--lib/gitlab/saml/auth_hash.rb2
-rw-r--r--lib/gitlab/saml/user.rb1
-rw-r--r--lib/gitlab/search_results.rb2
-rw-r--r--lib/gitlab/shell.rb4
-rw-r--r--lib/gitlab/shell_adapter.rb2
-rw-r--r--lib/gitlab/sherlock/transaction.rb4
-rw-r--r--lib/gitlab/sidekiq_middleware/memory_killer.rb41
-rw-r--r--lib/gitlab/sidekiq_status.rb7
-rw-r--r--lib/gitlab/sql/pattern.rb25
-rw-r--r--lib/gitlab/sql/union.rb6
-rw-r--r--lib/gitlab/string_range_marker.rb1
-rw-r--r--lib/gitlab/template/finders/repo_template_finder.rb1
-rw-r--r--lib/gitlab/testing/request_blocker_middleware.rb12
-rw-r--r--lib/gitlab/testing/request_inspector_middleware.rb71
-rw-r--r--lib/gitlab/url_blocker.rb4
-rw-r--r--lib/gitlab/url_sanitizer.rb1
-rw-r--r--lib/gitlab/usage_data.rb6
-rw-r--r--lib/gitlab/utils/merge_hash.rb117
-rw-r--r--lib/gitlab/utils/strong_memoize.rb31
-rw-r--r--lib/gitlab/visibility_level.rb1
-rw-r--r--lib/gitlab/workhorse.rb52
-rw-r--r--lib/google_api/cloud_platform/client.rb1
-rw-r--r--lib/haml_lint/inline_javascript.rb1
-rw-r--r--lib/milestone_array.rb40
-rw-r--r--lib/omni_auth/strategies/bitbucket.rb4
-rw-r--r--lib/peek/views/gitaly.rb34
-rw-r--r--lib/rouge/lexers/math.rb9
-rw-r--r--lib/rouge/lexers/plantuml.rb9
-rw-r--r--lib/system_check/app/git_user_default_ssh_config_check.rb4
-rw-r--r--lib/system_check/app/ruby_version_check.rb2
-rw-r--r--lib/system_check/simple_executor.rb1
-rw-r--r--lib/tasks/brakeman.rake2
-rw-r--r--lib/tasks/gemojione.rake31
-rw-r--r--lib/tasks/gitlab/backup.rake39
-rw-r--r--lib/tasks/gitlab/cleanup.rake12
-rw-r--r--lib/tasks/gitlab/dev.rake5
-rw-r--r--lib/tasks/gitlab/gitaly.rake17
-rw-r--r--lib/tasks/gitlab/import.rake14
-rw-r--r--lib/tasks/gitlab/sidekiq.rake47
-rw-r--r--lib/tasks/gitlab/storage.rake85
-rw-r--r--lib/tasks/gitlab/users.rake11
-rw-r--r--lib/tasks/import.rake38
-rw-r--r--lib/tasks/tokens.rake12
-rw-r--r--locale/bg/gitlab.po830
-rw-r--r--locale/de/gitlab.po834
-rw-r--r--locale/eo/gitlab.po826
-rw-r--r--locale/es/gitlab.po824
-rw-r--r--locale/fr/gitlab.po1196
-rw-r--r--locale/gitlab.pot290
-rw-r--r--locale/it/gitlab.po824
-rw-r--r--locale/ja/gitlab.po820
-rw-r--r--locale/ko/gitlab.po820
-rw-r--r--locale/nl_NL/gitlab.po842
-rw-r--r--locale/pt_BR/gitlab.po1264
-rw-r--r--locale/ru/gitlab.po1310
-rw-r--r--locale/uk/gitlab.po1094
-rw-r--r--locale/zh_CN/gitlab.po1068
-rw-r--r--locale/zh_HK/gitlab.po820
-rw-r--r--locale/zh_TW/gitlab.po1070
-rw-r--r--package.json46
-rw-r--r--qa/.gitignore1
-rw-r--r--qa/Dockerfile7
-rwxr-xr-xqa/bin/qa2
-rw-r--r--qa/qa.rb36
-rw-r--r--qa/qa/git/repository.rb2
-rw-r--r--qa/qa/page/admin/menu.rb7
-rw-r--r--qa/qa/page/admin/settings.rb18
-rw-r--r--qa/qa/page/base.rb19
-rw-r--r--qa/qa/page/dashboard/groups.rb23
-rw-r--r--qa/qa/page/dashboard/projects.rb11
-rw-r--r--qa/qa/page/group/new.rb23
-rw-r--r--qa/qa/page/group/show.rb22
-rw-r--r--qa/qa/page/main/entry.rb22
-rw-r--r--qa/qa/page/main/login.rb19
-rw-r--r--qa/qa/page/main/menu.rb5
-rw-r--r--qa/qa/page/main/oauth.rb15
-rw-r--r--qa/qa/page/mattermost/login.rb19
-rw-r--r--qa/qa/page/mattermost/main.rb11
-rw-r--r--qa/qa/page/project/show.rb4
-rw-r--r--qa/qa/runtime/namespace.rb6
-rw-r--r--qa/qa/runtime/scenario.rb28
-rw-r--r--qa/qa/scenario/bootable.rb45
-rw-r--r--qa/qa/scenario/entrypoint.rb35
-rw-r--r--qa/qa/scenario/gitlab/admin/hashed_storage.rb25
-rw-r--r--qa/qa/scenario/gitlab/group/create.rb27
-rw-r--r--qa/qa/scenario/gitlab/project/create.rb18
-rw-r--r--qa/qa/scenario/gitlab/sandbox/prepare.rb28
-rw-r--r--qa/qa/scenario/test/instance.rb17
-rw-r--r--qa/qa/scenario/test/integration/mattermost.rb21
-rw-r--r--qa/qa/shell/omnibus.rb39
-rw-r--r--qa/qa/specs/config.rb11
-rw-r--r--qa/qa/specs/features/login/standard_spec.rb5
-rw-r--r--qa/qa/specs/features/mattermost/group_create_spec.rb17
-rw-r--r--qa/qa/specs/features/mattermost/login_spec.rb24
-rw-r--r--qa/qa/specs/features/project/create_spec.rb5
-rw-r--r--qa/qa/specs/features/repository/clone_spec.rb5
-rw-r--r--qa/qa/specs/features/repository/push_spec.rb7
-rw-r--r--qa/qa/specs/runner.rb19
-rw-r--r--qa/spec/runtime/scenario_spec.rb27
-rw-r--r--qa/spec/scenario/bootable_spec.rb24
-rw-r--r--qa/spec/scenario/entrypoint_spec.rb44
-rw-r--r--rubocop/cop/line_break_after_guard_clauses.rb100
-rw-r--r--rubocop/cop/migration/add_column_with_default_to_large_table.rb55
-rw-r--r--rubocop/cop/migration/datetime.rb20
-rw-r--r--rubocop/cop/migration/update_large_table.rb57
-rw-r--r--rubocop/cop/rspec/env_assignment.rb58
-rw-r--r--rubocop/cop/rspec/verbose_include_metadata.rb74
-rw-r--r--rubocop/rubocop.rb15
-rw-r--r--rubocop/spec_helpers.rb12
-rw-r--r--scripts/create_mysql_user.sh8
-rw-r--r--scripts/create_postgres_user.sh8
-rw-r--r--scripts/prepare_build.sh21
-rwxr-xr-xscripts/static-analysis23
-rwxr-xr-xscripts/trigger-build-docs37
-rwxr-xr-xscripts/trigger-build-omnibus107
-rw-r--r--spec/bin/changelog_spec.rb2
-rw-r--r--spec/controllers/admin/hooks_controller_spec.rb2
-rw-r--r--spec/controllers/admin/impersonations_controller_spec.rb6
-rw-r--r--spec/controllers/admin/projects_controller_spec.rb2
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb14
-rw-r--r--spec/controllers/admin/services_controller_spec.rb6
-rw-r--r--spec/controllers/admin/spam_logs_controller_spec.rb8
-rw-r--r--spec/controllers/admin/users_controller_spec.rb4
-rw-r--r--spec/controllers/application_controller_spec.rb121
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb10
-rw-r--r--spec/controllers/boards/issues_controller_spec.rb28
-rw-r--r--spec/controllers/boards/lists_controller_spec.rb32
-rw-r--r--spec/controllers/concerns/group_tree_spec.rb89
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb107
-rw-r--r--spec/controllers/concerns/lfs_request_spec.rb50
-rw-r--r--spec/controllers/dashboard/groups_controller_spec.rb23
-rw-r--r--spec/controllers/dashboard/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb18
-rw-r--r--spec/controllers/explore/groups_controller_spec.rb23
-rw-r--r--spec/controllers/groups/children_controller_spec.rb297
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb18
-rw-r--r--spec/controllers/groups/labels_controller_spec.rb2
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb14
-rw-r--r--spec/controllers/groups/settings/ci_cd_controller_spec.rb2
-rw-r--r--spec/controllers/groups/variables_controller_spec.rb2
-rw-r--r--spec/controllers/groups_controller_spec.rb139
-rw-r--r--spec/controllers/health_check_controller_spec.rb8
-rw-r--r--spec/controllers/help_controller_spec.rb2
-rw-r--r--spec/controllers/import/github_controller_spec.rb4
-rw-r--r--spec/controllers/invites_controller_spec.rb4
-rw-r--r--spec/controllers/metrics_controller_spec.rb14
-rw-r--r--spec/controllers/notification_settings_controller_spec.rb4
-rw-r--r--spec/controllers/oauth/applications_controller_spec.rb4
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb6
-rw-r--r--spec/controllers/passwords_controller_spec.rb14
-rw-r--r--spec/controllers/profiles/accounts_controller_spec.rb6
-rw-r--r--spec/controllers/profiles_controller_spec.rb44
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb8
-rw-r--r--spec/controllers/projects/badges_controller_spec.rb4
-rw-r--r--spec/controllers/projects/blame_controller_spec.rb2
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb4
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb6
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb68
-rw-r--r--spec/controllers/projects/clusters/applications_controller_spec.rb85
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb490
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb44
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb31
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb8
-rw-r--r--spec/controllers/projects/deployments_controller_spec.rb4
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb12
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb26
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb4
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb165
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb71
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb18
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/conflicts_controller_spec.rb24
-rw-r--r--spec/controllers/projects/merge_requests/creations_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb8
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb78
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb30
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb79
-rw-r--r--spec/controllers/projects/pages_controller_spec.rb10
-rw-r--r--spec/controllers/projects/pages_domains_controller_spec.rb12
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb16
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb50
-rw-r--r--spec/controllers/projects/pipelines_settings_controller_spec.rb34
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb16
-rw-r--r--spec/controllers/projects/prometheus_controller_spec.rb6
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb10
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb14
-rw-r--r--spec/controllers/projects/registry/repositories_controller_spec.rb12
-rw-r--r--spec/controllers/projects/registry/tags_controller_spec.rb6
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb2
-rw-r--r--spec/controllers/projects/runners_controller_spec.rb8
-rw-r--r--spec/controllers/projects/services_controller_spec.rb2
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb2
-rw-r--r--spec/controllers/projects/settings/integrations_controller_spec.rb2
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb2
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb18
-rw-r--r--spec/controllers/projects/todos_controller_spec.rb20
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb2
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb22
-rw-r--r--spec/controllers/projects/variables_controller_spec.rb2
-rw-r--r--spec/controllers/projects_controller_spec.rb75
-rw-r--r--spec/controllers/registrations_controller_spec.rb3
-rw-r--r--spec/controllers/sent_notifications_controller_spec.rb2
-rw-r--r--spec/controllers/sessions_controller_spec.rb4
-rw-r--r--spec/controllers/snippets/notes_controller_spec.rb20
-rw-r--r--spec/controllers/snippets_controller_spec.rb26
-rw-r--r--spec/controllers/uploads_controller_spec.rb40
-rw-r--r--spec/controllers/users_controller_spec.rb12
-rw-r--r--spec/factories/ci/builds.rb2
-rw-r--r--spec/factories/clusters/applications/helm.rb35
-rw-r--r--spec/factories/clusters/applications/ingress.rb35
-rw-r--r--spec/factories/clusters/cluster.rb39
-rw-r--r--spec/factories/clusters/platforms/kubernetes.rb20
-rw-r--r--spec/factories/clusters/providers/gcp.rb32
-rw-r--r--spec/factories/commit_statuses.rb1
-rw-r--r--spec/factories/fork_network_members.rb8
-rw-r--r--spec/factories/gcp/cluster.rb38
-rw-r--r--spec/factories/group_custom_attributes.rb7
-rw-r--r--spec/factories/instance_configuration.rb5
-rw-r--r--spec/factories/merge_requests.rb10
-rw-r--r--spec/factories/notes.rb1
-rw-r--r--spec/factories/project_custom_attributes.rb7
-rw-r--r--spec/factories/services.rb2
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb2
-rw-r--r--spec/features/admin/admin_broadcast_messages_spec.rb2
-rw-r--r--spec/features/admin/admin_disables_two_factor_spec.rb4
-rw-r--r--spec/features/admin/admin_groups_spec.rb12
-rw-r--r--spec/features/admin/admin_health_check_spec.rb6
-rw-r--r--spec/features/admin/admin_hooks_spec.rb6
-rw-r--r--spec/features/admin/admin_labels_spec.rb4
-rw-r--r--spec/features/admin/admin_projects_spec.rb8
-rw-r--r--spec/features/admin/admin_settings_spec.rb25
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb6
-rw-r--r--spec/features/admin/admin_users_spec.rb31
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb4
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb6
-rw-r--r--spec/features/atom/dashboard_spec.rb6
-rw-r--r--spec/features/atom/issues_spec.rb6
-rw-r--r--spec/features/atom/users_spec.rb6
-rw-r--r--spec/features/auto_deploy_spec.rb88
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb2
-rw-r--r--spec/features/boards/boards_spec.rb24
-rw-r--r--spec/features/boards/keyboard_shortcut_spec.rb2
-rw-r--r--spec/features/boards/new_issue_spec.rb2
-rw-r--r--spec/features/boards/sidebar_spec.rb52
-rw-r--r--spec/features/calendar_spec.rb6
-rw-r--r--spec/features/ci_lint_spec.rb3
-rw-r--r--spec/features/commits_spec.rb29
-rw-r--r--spec/features/container_registry_spec.rb4
-rw-r--r--spec/features/copy_as_gfm_spec.rb6
-rw-r--r--spec/features/cycle_analytics_spec.rb2
-rw-r--r--spec/features/dashboard/active_tab_spec.rb2
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb2
-rw-r--r--spec/features/dashboard/group_spec.rb10
-rw-r--r--spec/features/dashboard/groups_list_spec.rb103
-rw-r--r--spec/features/dashboard/issues_spec.rb20
-rw-r--r--spec/features/dashboard/label_filter_spec.rb2
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb8
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb2
-rw-r--r--spec/features/dashboard/projects_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_filtering_spec.rb2
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb24
-rw-r--r--spec/features/discussion_comments/commit_spec.rb2
-rw-r--r--spec/features/discussion_comments/snippets_spec.rb2
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb2
-rw-r--r--spec/features/explore/groups_list_spec.rb13
-rw-r--r--spec/features/explore/new_menu_spec.rb10
-rw-r--r--spec/features/explore/user_explores_projects_spec.rb72
-rw-r--r--spec/features/gitlab_flavored_markdown_spec.rb2
-rw-r--r--spec/features/group_variables_spec.rb2
-rw-r--r--spec/features/groups/labels/subscription_spec.rb2
-rw-r--r--spec/features/groups/members/manage_members.rb6
-rw-r--r--spec/features/groups/milestone_spec.rb28
-rw-r--r--spec/features/groups/milestones_sorting_spec.rb51
-rw-r--r--spec/features/groups/show_spec.rb31
-rw-r--r--spec/features/groups_spec.rb31
-rw-r--r--spec/features/issuables/default_sort_order_spec.rb18
-rw-r--r--spec/features/issuables/shortcuts_issuable_spec.rb46
-rw-r--r--spec/features/issuables/user_sees_sidebar_spec.rb4
-rw-r--r--spec/features/issues/award_emoji_spec.rb18
-rw-r--r--spec/features/issues/award_spec.rb2
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb4
-rw-r--r--spec/features/issues/create_branch_merge_request_spec.rb93
-rw-r--r--spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb2
-rw-r--r--spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb7
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb8
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb8
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb8
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb7
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/recent_searches_spec.rb26
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb5
-rw-r--r--spec/features/issues/form_spec.rb49
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb51
-rw-r--r--spec/features/issues/issue_detail_spec.rb9
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb18
-rw-r--r--spec/features/issues/markdown_toolbar_spec.rb4
-rw-r--r--spec/features/issues/move_spec.rb12
-rw-r--r--spec/features/issues/spam_issues_spec.rb2
-rw-r--r--spec/features/issues/todo_spec.rb2
-rw-r--r--spec/features/issues/update_issues_spec.rb2
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb248
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb6
-rw-r--r--spec/features/issues_spec.rb150
-rw-r--r--spec/features/login_spec.rb8
-rw-r--r--spec/features/logout_spec.rb22
-rw-r--r--spec/features/markdown_spec.rb6
-rw-r--r--spec/features/merge_requests/assign_issues_spec.rb2
-rw-r--r--spec/features/merge_requests/award_spec.rb2
-rw-r--r--spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb2
-rw-r--r--spec/features/merge_requests/cherry_pick_spec.rb2
-rw-r--r--spec/features/merge_requests/closes_issues_spec.rb2
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb11
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb24
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb7
-rw-r--r--spec/features/merge_requests/deleted_source_branch_spec.rb2
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb22
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb53
-rw-r--r--spec/features/merge_requests/diffs_spec.rb20
-rw-r--r--spec/features/merge_requests/edit_mr_spec.rb5
-rw-r--r--spec/features/merge_requests/filter_by_labels_spec.rb16
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb8
-rw-r--r--spec/features/merge_requests/filter_merge_requests_spec.rb16
-rw-r--r--spec/features/merge_requests/form_spec.rb2
-rw-r--r--spec/features/merge_requests/image_diff_notes.rb2
-rw-r--r--spec/features/merge_requests/merge_commit_message_toggle_spec.rb2
-rw-r--r--spec/features/merge_requests/mini_pipeline_graph_spec.rb4
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb6
-rw-r--r--spec/features/merge_requests/pipelines_spec.rb2
-rw-r--r--spec/features/merge_requests/resolve_outdated_diff_discussions.rb2
-rw-r--r--spec/features/merge_requests/target_branch_spec.rb2
-rw-r--r--spec/features/merge_requests/toggle_whitespace_changes_spec.rb2
-rw-r--r--spec/features/merge_requests/toggler_behavior_spec.rb2
-rw-r--r--spec/features/merge_requests/update_merge_requests_spec.rb8
-rw-r--r--spec/features/merge_requests/user_posts_diff_notes_spec.rb9
-rw-r--r--spec/features/merge_requests/user_posts_notes_spec.rb2
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb2
-rw-r--r--spec/features/merge_requests/versions_spec.rb10
-rw-r--r--spec/features/merge_requests/widget_deployments_spec.rb4
-rw-r--r--spec/features/merge_requests/widget_spec.rb2
-rw-r--r--spec/features/milestone_spec.rb29
-rw-r--r--spec/features/profile_spec.rb37
-rw-r--r--spec/features/profiles/chat_names_spec.rb4
-rw-r--r--spec/features/profiles/keys_spec.rb2
-rw-r--r--spec/features/profiles/oauth_applications_spec.rb6
-rw-r--r--spec/features/profiles/password_spec.rb9
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb8
-rw-r--r--spec/features/profiles/user_changes_notified_of_own_activity_spec.rb2
-rw-r--r--spec/features/profiles/user_visits_notifications_tab_spec.rb4
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb2
-rw-r--r--spec/features/projects/artifacts/download_spec.rb2
-rw-r--r--spec/features/projects/artifacts/file_spec.rb1
-rw-r--r--spec/features/projects/badges/coverage_spec.rb2
-rw-r--r--spec/features/projects/badges/list_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_line_permalink_updater_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb12
-rw-r--r--spec/features/projects/blobs/edit_spec.rb3
-rw-r--r--spec/features/projects/blobs/shortcuts_blob_spec.rb2
-rw-r--r--spec/features/projects/branches_spec.rb6
-rw-r--r--spec/features/projects/clusters/interchangeability_spec.rb16
-rw-r--r--spec/features/projects/clusters_spec.rb111
-rw-r--r--spec/features/projects/commit/builds_spec.rb3
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb2
-rw-r--r--spec/features/projects/commit/diff_notes_spec.rb4
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb7
-rw-r--r--spec/features/projects/compare_spec.rb2
-rw-r--r--spec/features/projects/deploy_keys_spec.rb2
-rw-r--r--spec/features/projects/developer_views_empty_project_instructions_spec.rb4
-rw-r--r--spec/features/projects/edit_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_spec.rb59
-rw-r--r--spec/features/projects/environments/environments_spec.rb72
-rw-r--r--spec/features/projects/features_visibility_spec.rb8
-rw-r--r--spec/features/projects/files/browse_files_spec.rb2
-rw-r--r--spec/features/projects/files/dockerfile_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/edit_file_soft_wrap_spec.rb28
-rw-r--r--spec/features/projects/files/find_file_keyboard_spec.rb2
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb2
-rw-r--r--spec/features/projects/files/template_type_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/undo_template_spec.rb2
-rw-r--r--spec/features/projects/fork_spec.rb17
-rw-r--r--spec/features/projects/gfm_autocomplete_load_spec.rb2
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb4
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb22
-rw-r--r--spec/features/projects/import_export/namespace_export_file_spec.rb4
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin679559 -> 343232 bytes
-rw-r--r--spec/features/projects/issuable_templates_spec.rb8
-rw-r--r--spec/features/projects/issues/list_spec.rb20
-rw-r--r--spec/features/projects/issues/user_views_issues_spec.rb56
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb4
-rw-r--r--spec/features/projects/jobs_spec.rb44
-rw-r--r--spec/features/projects/labels/subscription_spec.rb2
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb10
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb2
-rw-r--r--spec/features/projects/members/groups_with_access_list_spec.rb5
-rw-r--r--spec/features/projects/members/list_spec.rb16
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb6
-rw-r--r--spec/features/projects/members/share_with_group_spec.rb6
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_diff_spec.rb11
-rw-r--r--spec/features/projects/merge_requests/user_edits_merge_request_spec.rb5
-rw-r--r--spec/features/projects/merge_requests/user_manages_subscription_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb2
-rw-r--r--spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb143
-rw-r--r--spec/features/projects/new_project_spec.rb37
-rw-r--r--spec/features/projects/no_password_spec.rb2
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb26
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb68
-rw-r--r--spec/features/projects/project_settings_spec.rb10
-rw-r--r--spec/features/projects/ref_switcher_spec.rb37
-rw-r--r--spec/features/projects/services/user_activates_jira_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb8
-rw-r--r--spec/features/projects/services/user_activates_packagist_spec.rb24
-rw-r--r--spec/features/projects/services/user_views_services_spec.rb1
-rw-r--r--spec/features/projects/settings/forked_project_settings_spec.rb40
-rw-r--r--spec/features/projects/settings/integration_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb6
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb128
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb5
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb2
-rw-r--r--spec/features/projects/show_project_spec.rb2
-rw-r--r--spec/features/projects/snippets/create_snippet_spec.rb4
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb37
-rw-r--r--spec/features/projects/tree/create_file_spec.rb37
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb46
-rw-r--r--spec/features/projects/user_browses_files_spec.rb13
-rw-r--r--spec/features/projects/user_creates_directory_spec.rb2
-rw-r--r--spec/features/projects/user_creates_files_spec.rb16
-rw-r--r--spec/features/projects/user_creates_project_spec.rb33
-rw-r--r--spec/features/projects/user_deletes_files_spec.rb4
-rw-r--r--spec/features/projects/user_edits_files_spec.rb19
-rw-r--r--spec/features/projects/user_interacts_with_stars_spec.rb2
-rw-r--r--spec/features/projects/user_replaces_files_spec.rb4
-rw-r--r--spec/features/projects/user_transfers_a_project_spec.rb49
-rw-r--r--spec/features/projects/user_uploads_files_spec.rb4
-rw-r--r--spec/features/projects/user_views_details_spec.rb151
-rw-r--r--spec/features/projects/view_on_env_spec.rb2
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb9
-rw-r--r--spec/features/projects/wiki/shortcuts_spec.rb4
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb4
-rw-r--r--spec/features/projects/wiki/user_git_access_wiki_page_spec.rb9
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb7
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb13
-rw-r--r--spec/features/projects_spec.rb11
-rw-r--r--spec/features/protected_branches_spec.rb8
-rw-r--r--spec/features/protected_tags_spec.rb2
-rw-r--r--spec/features/raven_js_spec.rb6
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb4
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb8
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb6
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb6
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb4
-rw-r--r--spec/features/search/user_uses_search_filters_spec.rb6
-rw-r--r--spec/features/security/project/internal_access_spec.rb15
-rw-r--r--spec/features/security/project/private_access_spec.rb15
-rw-r--r--spec/features/security/project/public_access_spec.rb15
-rw-r--r--spec/features/snippets/internal_snippet_spec.rb2
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb7
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb16
-rw-r--r--spec/features/tags/master_creates_tag_spec.rb4
-rw-r--r--spec/features/tags/master_deletes_tag_spec.rb8
-rw-r--r--spec/features/task_lists_spec.rb10
-rw-r--r--spec/features/triggers_spec.rb24
-rw-r--r--spec/features/u2f_spec.rb24
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb40
-rw-r--r--spec/features/users/snippets_spec.rb2
-rw-r--r--spec/features/users_spec.rb3
-rw-r--r--spec/features/variables_spec.rb4
-rw-r--r--spec/finders/admin/projects_finder_spec.rb2
-rw-r--r--spec/finders/autocomplete_users_finder_spec.rb15
-rw-r--r--spec/finders/branches_finder_spec.rb9
-rw-r--r--spec/finders/group_descendants_finder_spec.rb166
-rw-r--r--spec/finders/runner_jobs_finder_spec.rb39
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json33
-rw-r--r--spec/fixtures/api/schemas/entities/issue.json44
-rw-r--r--spec/fixtures/api/schemas/entities/issue_sidebar.json21
-rw-r--r--spec/fixtures/api/schemas/entities/label.json26
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_basic.json4
-rw-r--r--spec/fixtures/api/schemas/issue.json29
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_requests.json1
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json18
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json20
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domains.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/login.json6
-rw-r--r--spec/fixtures/api/schemas/registry/tag.json5
-rw-r--r--spec/fixtures/clusters/sample_cert.pem33
-rw-r--r--spec/fixtures/markdown.md.erb34
-rw-r--r--spec/fixtures/pages.tar.gzbin1795 -> 1884 bytes
-rw-r--r--spec/fixtures/pages.zipbin1851 -> 2338 bytes
-rw-r--r--spec/fixtures/ssh_host_example_key.pub1
-rw-r--r--spec/helpers/application_helper_spec.rb81
-rw-r--r--spec/helpers/auto_devops_helper_spec.rb100
-rw-r--r--spec/helpers/button_helper_spec.rb57
-rw-r--r--spec/helpers/ci_status_helper_spec.rb12
-rw-r--r--spec/helpers/events_helper_spec.rb90
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb26
-rw-r--r--spec/helpers/groups_helper_spec.rb19
-rw-r--r--spec/helpers/icons_helper_spec.rb28
-rw-r--r--spec/helpers/instance_configuration_helper_spec.rb51
-rw-r--r--spec/helpers/issuables_helper_spec.rb33
-rw-r--r--spec/helpers/labels_helper_spec.rb2
-rw-r--r--spec/helpers/markup_helper_spec.rb151
-rw-r--r--spec/helpers/namespaces_helper_spec.rb25
-rw-r--r--spec/helpers/projects_helper_spec.rb33
-rw-r--r--spec/helpers/search_helper_spec.rb4
-rw-r--r--spec/helpers/tree_helper_spec.rb32
-rw-r--r--spec/initializers/8_metrics_spec.rb5
-rw-r--r--spec/initializers/settings_spec.rb20
-rw-r--r--spec/javascripts/awards_handler_spec.js5
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js31
-rw-r--r--spec/javascripts/behaviors/copy_as_gfm_spec.js47
-rw-r--r--spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js19
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js5
-rw-r--r--spec/javascripts/blob/blob_file_dropzone_spec.js1
-rw-r--r--spec/javascripts/boards/board_card_spec.js29
-rw-r--r--spec/javascripts/boards/issue_spec.js13
-rw-r--r--spec/javascripts/build_spec.js306
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js257
-rw-r--r--spec/javascripts/clusters/components/application_row_spec.js237
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js42
-rw-r--r--spec/javascripts/clusters/services/mock_data.js50
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js89
-rw-r--r--spec/javascripts/clusters_spec.js79
-rw-r--r--spec/javascripts/commits_spec.js108
-rw-r--r--spec/javascripts/copy_as_gfm_spec.js49
-rw-r--r--spec/javascripts/datetime_utility_spec.js22
-rw-r--r--spec/javascripts/droplab/drop_down_spec.js8
-rw-r--r--spec/javascripts/droplab/hook_spec.js2
-rw-r--r--spec/javascripts/emoji_spec.js19
-rw-r--r--spec/javascripts/environments/emtpy_state_spec.js57
-rw-r--r--spec/javascripts/environments/environment_spec.js292
-rw-r--r--spec/javascripts/environments/environment_table_spec.js31
-rw-r--r--spec/javascripts/environments/environments_app_spec.js318
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js157
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js19
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js23
-rw-r--r--spec/javascripts/fixtures/clusters.rb2
-rw-r--r--spec/javascripts/fixtures/environments/element.html.haml1
-rw-r--r--spec/javascripts/fixtures/environments/environments.html.haml9
-rw-r--r--spec/javascripts/fixtures/environments/environments_folder_view.html.haml7
-rw-r--r--spec/javascripts/fixtures/pipelines.html.haml8
-rw-r--r--spec/javascripts/fixtures/search_autocomplete.html.haml1
-rw-r--r--spec/javascripts/flash_spec.js290
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js22
-rw-r--r--spec/javascripts/gl_field_errors_spec.js180
-rw-r--r--spec/javascripts/gl_form_spec.js13
-rw-r--r--spec/javascripts/groups/components/app_spec.js443
-rw-r--r--spec/javascripts/groups/components/group_folder_spec.js66
-rw-r--r--spec/javascripts/groups/components/group_item_spec.js177
-rw-r--r--spec/javascripts/groups/components/groups_spec.js70
-rw-r--r--spec/javascripts/groups/components/item_actions_spec.js110
-rw-r--r--spec/javascripts/groups/components/item_caret_spec.js40
-rw-r--r--spec/javascripts/groups/components/item_stats_spec.js159
-rw-r--r--spec/javascripts/groups/components/item_type_icon_spec.js54
-rw-r--r--spec/javascripts/groups/group_item_spec.js102
-rw-r--r--spec/javascripts/groups/groups_spec.js99
-rw-r--r--spec/javascripts/groups/mock_data.js470
-rw-r--r--spec/javascripts/groups/service/groups_service_spec.js42
-rw-r--r--spec/javascripts/groups/store/groups_store_spec.js110
-rw-r--r--spec/javascripts/header_spec.js74
-rw-r--r--spec/javascripts/helpers/set_timeout_promise_helper.js3
-rw-r--r--spec/javascripts/helpers/vue_mount_component_helper.js10
-rw-r--r--spec/javascripts/integrations/integration_settings_form_spec.js8
-rw-r--r--spec/javascripts/issuable_context_spec.js34
-rw-r--r--spec/javascripts/issuable_spec.js102
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js67
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js99
-rw-r--r--spec/javascripts/issue_show/components/edit_actions_spec.js9
-rw-r--r--spec/javascripts/issue_show/components/title_spec.js39
-rw-r--r--spec/javascripts/issue_spec.js34
-rw-r--r--spec/javascripts/job_spec.js305
-rw-r--r--spec/javascripts/jobs/header_spec.js7
-rw-r--r--spec/javascripts/jobs/job_details_mediator_spec.js20
-rw-r--r--spec/javascripts/jobs/mock_data.js2
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js6
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js73
-rw-r--r--spec/javascripts/lib/utils/datefix_spec.js27
-rw-r--r--spec/javascripts/lib/utils/number_utility_spec.js27
-rw-r--r--spec/javascripts/lib/utils/poll_spec.js6
-rw-r--r--spec/javascripts/lib/utils/text_markdown_spec.js62
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js120
-rw-r--r--spec/javascripts/merge_request_notes_spec.js16
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js10
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js12
-rw-r--r--spec/javascripts/monitoring/graph/legend_spec.js2
-rw-r--r--spec/javascripts/monitoring/graph_path_spec.js19
-rw-r--r--spec/javascripts/monitoring/mock_data.js15
-rw-r--r--spec/javascripts/monitoring/utils/multiple_time_series_spec.js2
-rw-r--r--spec/javascripts/namespace_select_spec.js65
-rw-r--r--spec/javascripts/new_branch_spec.js3
-rw-r--r--spec/javascripts/notes/components/issue_comment_form_spec.js25
-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_spec.js65
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/dropdown_action_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/mock_data.js12
-rw-r--r--spec/javascripts/pipelines/graph/stage_column_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/navigation_tabs_spec.js127
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js137
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js1
-rw-r--r--spec/javascripts/pipelines/pipelines_table_spec.js3
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_item_spec.js12
-rw-r--r--spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js12
-rw-r--r--spec/javascripts/registry/mock_data.js8
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js33
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_item_spec.js53
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_spec.js72
-rw-r--r--spec/javascripts/repo/components/new_branch_form_spec.js114
-rw-r--r--spec/javascripts/repo/components/new_dropdown/index_spec.js71
-rw-r--r--spec/javascripts/repo/components/new_dropdown/modal_spec.js198
-rw-r--r--spec/javascripts/repo/components/new_dropdown/upload_spec.js103
-rw-r--r--spec/javascripts/repo/components/repo_commit_section_spec.js219
-rw-r--r--spec/javascripts/repo/components/repo_edit_button_spec.js86
-rw-r--r--spec/javascripts/repo/components/repo_editor_spec.js69
-rw-r--r--spec/javascripts/repo/components/repo_file_buttons_spec.js90
-rw-r--r--spec/javascripts/repo/components/repo_file_options_spec.js33
-rw-r--r--spec/javascripts/repo/components/repo_file_spec.js152
-rw-r--r--spec/javascripts/repo/components/repo_loading_file_spec.js59
-rw-r--r--spec/javascripts/repo/components/repo_prev_directory_spec.js52
-rw-r--r--spec/javascripts/repo/components/repo_preview_spec.js32
-rw-r--r--spec/javascripts/repo/components/repo_sidebar_spec.js165
-rw-r--r--spec/javascripts/repo/components/repo_spec.js35
-rw-r--r--spec/javascripts/repo/components/repo_tab_spec.js104
-rw-r--r--spec/javascripts/repo/components/repo_tabs_spec.js46
-rw-r--r--spec/javascripts/repo/helpers.js15
-rw-r--r--spec/javascripts/repo/lib/common/disposable_spec.js44
-rw-r--r--spec/javascripts/repo/lib/common/model_manager_spec.js81
-rw-r--r--spec/javascripts/repo/lib/common/model_spec.js84
-rw-r--r--spec/javascripts/repo/lib/decorations/controller_spec.js120
-rw-r--r--spec/javascripts/repo/lib/diff/controller_spec.js176
-rw-r--r--spec/javascripts/repo/lib/diff/diff_spec.js80
-rw-r--r--spec/javascripts/repo/lib/editor_options_spec.js7
-rw-r--r--spec/javascripts/repo/lib/editor_spec.js128
-rw-r--r--spec/javascripts/repo/services/repo_service_spec.js171
-rw-r--r--spec/javascripts/repo/stores/actions/branch_spec.js38
-rw-r--r--spec/javascripts/repo/stores/actions/file_spec.js417
-rw-r--r--spec/javascripts/repo/stores/actions/tree_spec.js469
-rw-r--r--spec/javascripts/repo/stores/actions_spec.js419
-rw-r--r--spec/javascripts/repo/stores/getters_spec.js146
-rw-r--r--spec/javascripts/repo/stores/mutations/branch_spec.js18
-rw-r--r--spec/javascripts/repo/stores/mutations/file_spec.js131
-rw-r--r--spec/javascripts/repo/stores/mutations/tree_spec.js71
-rw-r--r--spec/javascripts/repo/stores/mutations_spec.js117
-rw-r--r--spec/javascripts/repo/stores/utils_spec.js102
-rw-r--r--spec/javascripts/search_autocomplete_spec.js61
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js6
-rw-r--r--spec/javascripts/shortcuts_spec.js11
-rw-r--r--spec/javascripts/sidebar/mock_data.js2
-rw-r--r--spec/javascripts/sidebar/participants_spec.js174
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js17
-rw-r--r--spec/javascripts/sidebar/sidebar_service_spec.js55
-rw-r--r--spec/javascripts/sidebar/sidebar_store_spec.js93
-rw-r--r--spec/javascripts/sidebar/sidebar_subscriptions_spec.js36
-rw-r--r--spec/javascripts/sidebar/subscriptions_spec.js42
-rw-r--r--spec/javascripts/smart_interval_spec.js243
-rw-r--r--spec/javascripts/test_bundle.js40
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js109
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js53
-rw-r--r--spec/javascripts/u2f/register_spec.js120
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js179
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js46
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js1
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js60
-rw-r--r--spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js47
-rw-r--r--spec/javascripts/vue_shared/ci_action_icons_spec.js27
-rw-r--r--spec/javascripts/vue_shared/ci_status_icon_spec.js27
-rw-r--r--spec/javascripts/vue_shared/components/ci_badge_link_spec.js41
-rw-r--r--spec/javascripts/vue_shared/components/icon_spec.js54
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_warning_spec.js6
-rw-r--r--spec/javascripts/vue_shared/components/loading_button_spec.js109
-rw-r--r--spec/javascripts/vue_shared/components/markdown/field_spec.js41
-rw-r--r--spec/javascripts/vue_shared/components/markdown/header_spec.js10
-rw-r--r--spec/javascripts/vue_shared/components/markdown/toolbar_spec.js37
-rw-r--r--spec/javascripts/vue_shared/components/navigation_tabs_spec.js61
-rw-r--r--spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js39
-rw-r--r--spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js25
-rw-r--r--spec/javascripts/vue_shared/components/notes/system_note_spec.js57
-rw-r--r--spec/javascripts/vue_shared/components/pikaday_spec.js29
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js35
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js91
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js117
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js32
-rw-r--r--spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js49
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js84
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js89
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js (renamed from spec/javascripts/vue_shared/components/user_avatar_svg_spec.js)0
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar_image_spec.js54
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar_link_spec.js50
-rw-r--r--spec/javascripts/zen_mode_spec.js149
-rw-r--r--spec/lib/additional_email_headers_interceptor_spec.rb21
-rw-r--r--spec/lib/api/helpers_spec.rb109
-rw-r--r--spec/lib/banzai/commit_renderer_spec.rb2
-rw-r--r--spec/lib/banzai/filter/absolute_link_filter_spec.rb58
-rw-r--r--spec/lib/banzai/filter/gollum_tags_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb70
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/mermaid_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb45
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb5
-rw-r--r--spec/lib/banzai/filter/snippet_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb33
-rw-r--r--spec/lib/banzai/note_renderer_spec.rb24
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb4
-rw-r--r--spec/lib/banzai/renderer_spec.rb2
-rw-r--r--spec/lib/container_registry/path_spec.rb18
-rw-r--r--spec/lib/feature_spec.rb41
-rw-r--r--spec/lib/github/client_spec.rb34
-rw-r--r--spec/lib/github/import/legacy_diff_note_spec.rb9
-rw-r--r--spec/lib/github/import/note_spec.rb9
-rw-r--r--spec/lib/gitlab/app_logger_spec.rb12
-rw-r--r--spec/lib/gitlab/auth/request_authenticator_spec.rb67
-rw-r--r--spec/lib/gitlab/auth/user_auth_finders_spec.rb194
-rw-r--r--spec/lib/gitlab/auth_spec.rb66
-rw-r--r--spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb9
-rw-r--r--spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb37
-rw-r--r--spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb33
-rw-r--r--spec/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id_spec.rb62
-rw-r--r--spec/lib/gitlab/backup/manager_spec.rb4
-rw-r--r--spec/lib/gitlab/bare_repository_import/importer_spec.rb168
-rw-r--r--spec/lib/gitlab/bare_repository_import/repository_spec.rb51
-rw-r--r--spec/lib/gitlab/bare_repository_importer_spec.rb100
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb33
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb46
-rw-r--r--spec/lib/gitlab/checks/force_push_spec.rb2
-rw-r--r--spec/lib/gitlab/checks/lfs_integrity_spec.rb74
-rw-r--r--spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/cron_parser_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/status/build/cancelable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/retryable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/stop_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/canceled_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/created_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/failed_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/manual_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/pending_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/running_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/skipped_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/success_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/success_warning_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb28
-rw-r--r--spec/lib/gitlab/conflict/file_collection_spec.rb2
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb25
-rw-r--r--spec/lib/gitlab/conflict/parser_spec.rb222
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb2
-rw-r--r--spec/lib/gitlab/database/grant_spec.rb22
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb4
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb2
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb18
-rw-r--r--spec/lib/gitlab/database_spec.rb42
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb8
-rw-r--r--spec/lib/gitlab/diff/parser_spec.rb17
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb57
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb4
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb17
-rw-r--r--spec/lib/gitlab/fake_application_settings_spec.rb10
-rw-r--r--spec/lib/gitlab/file_detector_spec.rb12
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb59
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb32
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb6
-rw-r--r--spec/lib/gitlab/git/conflict/parser_spec.rb224
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb1
-rw-r--r--spec/lib/gitlab/git/env_spec.rb42
-rw-r--r--spec/lib/gitlab/git/hooks_service_spec.rb28
-rw-r--r--spec/lib/gitlab/git/lfs_changes_spec.rb48
-rw-r--r--spec/lib/gitlab/git/popen_spec.rb149
-rw-r--r--spec/lib/gitlab/git/remote_repository_spec.rb99
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb462
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb123
-rw-r--r--spec/lib/gitlab/git/storage/circuit_breaker_spec.rb315
-rw-r--r--spec/lib/gitlab/git/storage/forked_storage_check_spec.rb15
-rw-r--r--spec/lib/gitlab/git/storage/health_spec.rb30
-rw-r--r--spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb17
-rw-r--r--spec/lib/gitlab/git/tag_spec.rb2
-rw-r--r--spec/lib/gitlab/git/user_spec.rb33
-rw-r--r--spec/lib/gitlab/git_access_spec.rb4
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb36
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb17
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb11
-rw-r--r--spec/lib/gitlab/gitaly_client/util_spec.rb22
-rw-r--r--spec/lib/gitlab/gitaly_client/wiki_service_spec.rb88
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb16
-rw-r--r--spec/lib/gitlab/github_import/branch_formatter_spec.rb70
-rw-r--r--spec/lib/gitlab/github_import/bulk_importing_spec.rb62
-rw-r--r--spec/lib/gitlab/github_import/caching_spec.rb117
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb389
-rw-r--r--spec/lib/gitlab/github_import/comment_formatter_spec.rb96
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb152
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb119
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb27
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_importer_spec.rb201
-rw-r--r--spec/lib/gitlab/github_import/importer/issues_importer_spec.rb111
-rw-r--r--spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb82
-rw-r--r--spec/lib/gitlab/github_import/importer/labels_importer_spec.rb107
-rw-r--r--spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb120
-rw-r--r--spec/lib/gitlab/github_import/importer/note_importer_spec.rb151
-rw-r--r--spec/lib/gitlab/github_import/importer/notes_importer_spec.rb116
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb221
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb272
-rw-r--r--spec/lib/gitlab/github_import/importer/releases_importer_spec.rb125
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb227
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb309
-rw-r--r--spec/lib/gitlab/github_import/issuable_finder_spec.rb38
-rw-r--r--spec/lib/gitlab/github_import/issuable_formatter_spec.rb21
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb195
-rw-r--r--spec/lib/gitlab/github_import/label_finder_spec.rb61
-rw-r--r--spec/lib/gitlab/github_import/label_formatter_spec.rb34
-rw-r--r--spec/lib/gitlab/github_import/markdown_text_spec.rb28
-rw-r--r--spec/lib/gitlab/github_import/milestone_finder_spec.rb57
-rw-r--r--spec/lib/gitlab/github_import/milestone_formatter_spec.rb96
-rw-r--r--spec/lib/gitlab/github_import/page_counter_spec.rb32
-rw-r--r--spec/lib/gitlab/github_import/parallel_importer_spec.rb40
-rw-r--r--spec/lib/gitlab/github_import/parallel_scheduling_spec.rb296
-rw-r--r--spec/lib/gitlab/github_import/project_creator_spec.rb80
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb347
-rw-r--r--spec/lib/gitlab/github_import/release_formatter_spec.rb54
-rw-r--r--spec/lib/gitlab/github_import/representation/diff_note_spec.rb164
-rw-r--r--spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb19
-rw-r--r--spec/lib/gitlab/github_import/representation/issue_spec.rb182
-rw-r--r--spec/lib/gitlab/github_import/representation/note_spec.rb107
-rw-r--r--spec/lib/gitlab/github_import/representation/pull_request_spec.rb288
-rw-r--r--spec/lib/gitlab/github_import/representation/to_hash_spec.rb37
-rw-r--r--spec/lib/gitlab/github_import/representation/user_spec.rb33
-rw-r--r--spec/lib/gitlab/github_import/representation_spec.rb17
-rw-r--r--spec/lib/gitlab/github_import/sequential_importer_spec.rb37
-rw-r--r--spec/lib/gitlab/github_import/user_finder_spec.rb333
-rw-r--r--spec/lib/gitlab/github_import/user_formatter_spec.rb39
-rw-r--r--spec/lib/gitlab/github_import/wiki_formatter_spec.rb23
-rw-r--r--spec/lib/gitlab/github_import_spec.rb79
-rw-r--r--spec/lib/gitlab/group_hierarchy_spec.rb28
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb2
-rw-r--r--spec/lib/gitlab/hook_data/issuable_builder_spec.rb110
-rw-r--r--spec/lib/gitlab/hook_data/issue_builder_spec.rb45
-rw-r--r--spec/lib/gitlab/hook_data/merge_request_builder_spec.rb61
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml12
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/merge_request_parser_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/project.group.json188
-rw-r--r--spec/lib/gitlab/import_export/project.json763
-rw-r--r--spec/lib/gitlab/import_export/project.light.json51
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb166
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb17
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml57
-rw-r--r--spec/lib/gitlab/import_export/uploads_restorer_spec.rb55
-rw-r--r--spec/lib/gitlab/import_export/uploads_saver_spec.rb61
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb4
-rw-r--r--spec/lib/gitlab/issuable_metadata_spec.rb12
-rw-r--r--spec/lib/gitlab/kubernetes/helm_spec.rb100
-rw-r--r--spec/lib/gitlab/kubernetes/namespace_spec.rb66
-rw-r--r--spec/lib/gitlab/ldap/authentication_spec.rb4
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb8
-rw-r--r--spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb70
-rw-r--r--spec/lib/gitlab/legacy_github_import/client_spec.rb97
-rw-r--r--spec/lib/gitlab/legacy_github_import/comment_formatter_spec.rb96
-rw-r--r--spec/lib/gitlab/legacy_github_import/importer_spec.rb309
-rw-r--r--spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb21
-rw-r--r--spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb195
-rw-r--r--spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb34
-rw-r--r--spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb96
-rw-r--r--spec/lib/gitlab/legacy_github_import/project_creator_spec.rb80
-rw-r--r--spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb347
-rw-r--r--spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb54
-rw-r--r--spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb39
-rw-r--r--spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb23
-rw-r--r--spec/lib/gitlab/metrics/background_transaction_spec.rb19
-rw-r--r--spec/lib/gitlab/metrics/influx_sampler_spec.rb150
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb3
-rw-r--r--spec/lib/gitlab/metrics/method_call_spec.rb59
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb84
-rw-r--r--spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb150
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb90
-rw-r--r--spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb108
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/subscribers/action_view_spec.rb11
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb17
-rw-r--r--spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb89
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb203
-rw-r--r--spec/lib/gitlab/metrics/unicorn_sampler_spec.rb108
-rw-r--r--spec/lib/gitlab/metrics/web_transaction_spec.rb252
-rw-r--r--spec/lib/gitlab/metrics_spec.rb43
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb144
-rw-r--r--spec/lib/gitlab/middleware/rails_queue_duration_spec.rb12
-rw-r--r--spec/lib/gitlab/middleware/read_only_spec.rb42
-rw-r--r--spec/lib/gitlab/multi_collection_paginator_spec.rb46
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb11
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb65
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb9
-rw-r--r--spec/lib/gitlab/project_template_spec.rb8
-rw-r--r--spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb81
-rw-r--r--spec/lib/gitlab/saml/auth_hash_spec.rb40
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb34
-rw-r--r--spec/lib/gitlab/shell_spec.rb4
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb63
-rw-r--r--spec/lib/gitlab/sidekiq_status_spec.rb12
-rw-r--r--spec/lib/gitlab/sql/pattern_spec.rb30
-rw-r--r--spec/lib/gitlab/sql/union_spec.rb7
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb16
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb6
-rw-r--r--spec/lib/gitlab/utils/merge_hash_spec.rb33
-rw-r--r--spec/lib/gitlab/utils/strong_memoize_spec.rb52
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb88
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb2
-rw-r--r--spec/lib/milestone_array_spec.rb34
-rw-r--r--spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb8
-rw-r--r--spec/mailers/notify_spec.rb511
-rw-r--r--spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb166
-rw-r--r--spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb25
-rw-r--r--spec/migrations/migrate_user_project_view_spec.rb7
-rw-r--r--spec/migrations/remove_empty_fork_networks_spec.rb24
-rw-r--r--spec/migrations/schedule_merge_request_diff_migrations_spec.rb19
-rw-r--r--spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb19
-rw-r--r--spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb64
-rw-r--r--spec/migrations/update_upload_paths_to_system_spec.rb4
-rw-r--r--spec/models/application_setting_spec.rb126
-rw-r--r--spec/models/blob_spec.rb17
-rw-r--r--spec/models/blob_viewer/readme_spec.rb2
-rw-r--r--spec/models/ci/artifact_blob_spec.rb7
-rw-r--r--spec/models/ci/build_spec.rb49
-rw-r--r--spec/models/ci/pipeline_spec.rb145
-rw-r--r--spec/models/ci/runner_spec.rb2
-rw-r--r--spec/models/clusters/applications/helm_spec.rb102
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb108
-rw-r--r--spec/models/clusters/cluster_spec.rb201
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb266
-rw-r--r--spec/models/clusters/project_spec.rb6
-rw-r--r--spec/models/clusters/providers/gcp_spec.rb183
-rw-r--r--spec/models/commit_collection_spec.rb59
-rw-r--r--spec/models/commit_spec.rb11
-rw-r--r--spec/models/commit_status_spec.rb73
-rw-r--r--spec/models/concerns/avatarable_spec.rb44
-rw-r--r--spec/models/concerns/group_descendant_spec.rb166
-rw-r--r--spec/models/concerns/has_variable_spec.rb18
-rw-r--r--spec/models/concerns/ignorable_column_spec.rb12
-rw-r--r--spec/models/concerns/issuable_spec.rb101
-rw-r--r--spec/models/concerns/loaded_in_group_list_spec.rb49
-rw-r--r--spec/models/concerns/manual_inverse_association_spec.rb51
-rw-r--r--spec/models/concerns/milestoneish_spec.rb17
-rw-r--r--spec/models/concerns/routable_spec.rb1
-rw-r--r--spec/models/concerns/subscribable_spec.rb6
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb2
-rw-r--r--spec/models/diff_note_spec.rb8
-rw-r--r--spec/models/diff_viewer/base_spec.rb22
-rw-r--r--spec/models/diff_viewer/server_side_spec.rb9
-rw-r--r--spec/models/email_spec.rb8
-rw-r--r--spec/models/environment_spec.rb77
-rw-r--r--spec/models/fork_network_member_spec.rb18
-rw-r--r--spec/models/fork_network_spec.rb10
-rw-r--r--spec/models/gcp/cluster_spec.rb240
-rw-r--r--spec/models/group_custom_attribute_spec.rb16
-rw-r--r--spec/models/group_spec.rb58
-rw-r--r--spec/models/identity_spec.rb24
-rw-r--r--spec/models/instance_configuration_spec.rb109
-rw-r--r--spec/models/issue_spec.rb34
-rw-r--r--spec/models/key_spec.rb23
-rw-r--r--spec/models/member_spec.rb4
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb83
-rw-r--r--spec/models/merge_request_diff_spec.rb80
-rw-r--r--spec/models/merge_request_spec.rb165
-rw-r--r--spec/models/milestone_spec.rb2
-rw-r--r--spec/models/namespace_spec.rb28
-rw-r--r--spec/models/note_spec.rb31
-rw-r--r--spec/models/project_custom_attribute_spec.rb16
-rw-r--r--spec/models/project_group_link_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/issue_message_spec.rb13
-rw-r--r--spec/models/project_services/flowdock_service_spec.rb1
-rw-r--r--spec/models/project_services/jira_service_spec.rb14
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb49
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb8
-rw-r--r--spec/models/project_services/packagist_service_spec.rb46
-rw-r--r--spec/models/project_spec.rb275
-rw-r--r--spec/models/project_wiki_spec.rb193
-rw-r--r--spec/models/repository_spec.rb165
-rw-r--r--spec/models/sent_notification_spec.rb122
-rw-r--r--spec/models/snippet_spec.rb2
-rw-r--r--spec/models/user_spec.rb164
-rw-r--r--spec/models/wiki_page_spec.rb4
-rw-r--r--spec/policies/ci/build_policy_spec.rb77
-rw-r--r--spec/policies/clusters/cluster_policy_spec.rb28
-rw-r--r--spec/policies/gcp/cluster_policy_spec.rb28
-rw-r--r--spec/policies/group_policy_spec.rb13
-rw-r--r--spec/policies/namespace_policy_spec.rb38
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb34
-rw-r--r--spec/presenters/gcp/cluster_presenter_spec.rb35
-rw-r--r--spec/requests/api/access_requests_spec.rb28
-rw-r--r--spec/requests/api/award_emoji_spec.rb54
-rw-r--r--spec/requests/api/boards_spec.rb34
-rw-r--r--spec/requests/api/branches_spec.rb36
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb38
-rw-r--r--spec/requests/api/circuit_breakers_spec.rb10
-rw-r--r--spec/requests/api/commit_statuses_spec.rb38
-rw-r--r--spec/requests/api/commits_spec.rb32
-rw-r--r--spec/requests/api/deploy_keys_spec.rb30
-rw-r--r--spec/requests/api/deployments_spec.rb8
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb16
-rw-r--r--spec/requests/api/environments_spec.rb34
-rw-r--r--spec/requests/api/events_spec.rb24
-rw-r--r--spec/requests/api/features_spec.rb34
-rw-r--r--spec/requests/api/files_spec.rb44
-rw-r--r--spec/requests/api/group_variables_spec.rb40
-rw-r--r--spec/requests/api/groups_spec.rb266
-rw-r--r--spec/requests/api/helpers_spec.rb468
-rw-r--r--spec/requests/api/internal_spec.rb134
-rw-r--r--spec/requests/api/issues_spec.rb160
-rw-r--r--spec/requests/api/jobs_spec.rb113
-rw-r--r--spec/requests/api/keys_spec.rb6
-rw-r--r--spec/requests/api/labels_spec.rb64
-rw-r--r--spec/requests/api/lint_spec.rb8
-rw-r--r--spec/requests/api/members_spec.rb44
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb10
-rw-r--r--spec/requests/api/merge_requests_spec.rb80
-rw-r--r--spec/requests/api/namespaces_spec.rb135
-rw-r--r--spec/requests/api/notes_spec.rb216
-rw-r--r--spec/requests/api/notification_settings_spec.rb16
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb8
-rw-r--r--spec/requests/api/pages_domains_spec.rb487
-rw-r--r--spec/requests/api/pipeline_schedules_spec.rb72
-rw-r--r--spec/requests/api/pipelines_spec.rb64
-rw-r--r--spec/requests/api/project_hooks_spec.rb38
-rw-r--r--spec/requests/api/project_snippets_spec.rb34
-rw-r--r--spec/requests/api/projects_spec.rb269
-rw-r--r--spec/requests/api/repositories_spec.rb24
-rw-r--r--spec/requests/api/runner_spec.rb138
-rw-r--r--spec/requests/api/runners_spec.rb232
-rw-r--r--spec/requests/api/services_spec.rb45
-rw-r--r--spec/requests/api/session_spec.rb107
-rw-r--r--spec/requests/api/settings_spec.rb19
-rw-r--r--spec/requests/api/sidekiq_metrics_spec.rb8
-rw-r--r--spec/requests/api/snippets_spec.rb38
-rw-r--r--spec/requests/api/system_hooks_spec.rb20
-rw-r--r--spec/requests/api/templates_spec.rb12
-rw-r--r--spec/requests/api/todos_spec.rb12
-rw-r--r--spec/requests/api/triggers_spec.rb64
-rw-r--r--spec/requests/api/users_spec.rb379
-rw-r--r--spec/requests/api/v3/award_emoji_spec.rb54
-rw-r--r--spec/requests/api/v3/boards_spec.rb16
-rw-r--r--spec/requests/api/v3/branches_spec.rb24
-rw-r--r--spec/requests/api/v3/broadcast_messages_spec.rb6
-rw-r--r--spec/requests/api/v3/builds_spec.rb60
-rw-r--r--spec/requests/api/v3/commits_spec.rb82
-rw-r--r--spec/requests/api/v3/deploy_keys_spec.rb26
-rw-r--r--spec/requests/api/v3/deployments_spec.rb8
-rw-r--r--spec/requests/api/v3/environments_spec.rb28
-rw-r--r--spec/requests/api/v3/files_spec.rb28
-rw-r--r--spec/requests/api/v3/groups_spec.rb114
-rw-r--r--spec/requests/api/v3/issues_spec.rb236
-rw-r--r--spec/requests/api/v3/labels_spec.rb24
-rw-r--r--spec/requests/api/v3/members_spec.rb44
-rw-r--r--spec/requests/api/v3/merge_request_diffs_spec.rb4
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb124
-rw-r--r--spec/requests/api/v3/milestones_spec.rb46
-rw-r--r--spec/requests/api/v3/notes_spec.rb88
-rw-r--r--spec/requests/api/v3/pipelines_spec.rb26
-rw-r--r--spec/requests/api/v3/project_hooks_spec.rb40
-rw-r--r--spec/requests/api/v3/project_snippets_spec.rb26
-rw-r--r--spec/requests/api/v3/projects_spec.rb226
-rw-r--r--spec/requests/api/v3/repositories_spec.rb35
-rw-r--r--spec/requests/api/v3/runners_spec.rb28
-rw-r--r--spec/requests/api/v3/services_spec.rb2
-rw-r--r--spec/requests/api/v3/settings_spec.rb12
-rw-r--r--spec/requests/api/v3/snippets_spec.rb28
-rw-r--r--spec/requests/api/v3/system_hooks_spec.rb10
-rw-r--r--spec/requests/api/v3/tags_spec.rb10
-rw-r--r--spec/requests/api/v3/templates_spec.rb12
-rw-r--r--spec/requests/api/v3/triggers_spec.rb48
-rw-r--r--spec/requests/api/v3/users_spec.rb52
-rw-r--r--spec/requests/api/variables_spec.rb40
-rw-r--r--spec/requests/api/wikis_spec.rb22
-rw-r--r--spec/requests/git_http_spec.rb90
-rw-r--r--spec/requests/jwt_controller_spec.rb22
-rw-r--r--spec/requests/lfs_http_spec.rb153
-rw-r--r--spec/requests/openid_connect_spec.rb15
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb6
-rw-r--r--spec/requests/rack_attack_global_spec.rb362
-rw-r--r--spec/routing/group_routing_spec.rb133
-rw-r--r--spec/routing/project_routing_spec.rb19
-rw-r--r--spec/routing/routing_spec.rb47
-rw-r--r--spec/rubocop/cop/line_break_after_guard_clauses_spec.rb160
-rw-r--r--spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb44
-rw-r--r--spec/rubocop/cop/migration/datetime_spec.rb26
-rw-r--r--spec/rubocop/cop/migration/update_large_table_spec.rb69
-rw-r--r--spec/rubocop/cop/rspec/env_assignment_spec.rb59
-rw-r--r--spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb53
-rw-r--r--spec/serializers/cluster_application_entity_spec.rb30
-rw-r--r--spec/serializers/cluster_entity_spec.rb51
-rw-r--r--spec/serializers/cluster_serializer_spec.rb21
-rw-r--r--spec/serializers/container_tag_entity_spec.rb2
-rw-r--r--spec/serializers/group_child_entity_spec.rb101
-rw-r--r--spec/serializers/group_child_serializer_spec.rb110
-rw-r--r--spec/serializers/issue_entity_spec.rb20
-rw-r--r--spec/serializers/issue_serializer_spec.rb27
-rw-r--r--spec/serializers/merge_request_basic_serializer_spec.rb10
-rw-r--r--spec/serializers/merge_request_entity_spec.rb13
-rw-r--r--spec/serializers/merge_request_serializer_spec.rb12
-rw-r--r--spec/serializers/pipeline_details_entity_spec.rb2
-rw-r--r--spec/services/applications/create_service_spec.rb13
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb50
-rw-r--r--spec/services/base_count_service_spec.rb80
-rw-r--r--spec/services/ci/create_cluster_service_spec.rb47
-rw-r--r--spec/services/ci/fetch_gcp_operation_service_spec.rb36
-rw-r--r--spec/services/ci/finalize_cluster_creation_service_spec.rb61
-rw-r--r--spec/services/ci/integrate_cluster_service_spec.rb42
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb24
-rw-r--r--spec/services/ci/provision_cluster_service_spec.rb85
-rw-r--r--spec/services/ci/retry_build_service_spec.rb3
-rw-r--r--spec/services/ci/update_cluster_service_spec.rb37
-rw-r--r--spec/services/clusters/applications/check_installation_progress_service_spec.rb91
-rw-r--r--spec/services/clusters/applications/install_service_spec.rb60
-rw-r--r--spec/services/clusters/applications/schedule_installation_service_spec.rb55
-rw-r--r--spec/services/clusters/create_service_spec.rb64
-rw-r--r--spec/services/clusters/gcp/fetch_operation_service_spec.rb43
-rw-r--r--spec/services/clusters/gcp/finalize_creation_service_spec.rb111
-rw-r--r--spec/services/clusters/gcp/provision_service_spec.rb69
-rw-r--r--spec/services/clusters/gcp/verify_provision_status_service_spec.rb107
-rw-r--r--spec/services/clusters/update_service_spec.rb59
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb8
-rw-r--r--spec/services/events/render_service_spec.rb37
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb90
-rw-r--r--spec/services/issuable/destroy_service_spec.rb38
-rw-r--r--spec/services/issues/update_service_spec.rb24
-rw-r--r--spec/services/merge_requests/build_service_spec.rb32
-rw-r--r--spec/services/merge_requests/conflicts/list_service_spec.rb2
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb39
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb24
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb94
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb8
-rw-r--r--spec/services/merge_requests/update_service_spec.rb17
-rw-r--r--spec/services/milestones/destroy_service_spec.rb4
-rw-r--r--spec/services/milestones/promote_service_spec.rb113
-rw-r--r--spec/services/notes/render_service_spec.rb31
-rw-r--r--spec/services/notification_service_spec.rb14
-rw-r--r--spec/services/projects/destroy_service_spec.rb17
-rw-r--r--spec/services/projects/group_links/create_service_spec.rb22
-rw-r--r--spec/services/projects/group_links/destroy_service_spec.rb16
-rw-r--r--spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb63
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb76
-rw-r--r--spec/services/projects/hashed_storage_migration_service_spec.rb66
-rw-r--r--spec/services/projects/import_service_spec.rb83
-rw-r--r--spec/services/projects/transfer_service_spec.rb32
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb18
-rw-r--r--spec/services/projects/update_pages_service_spec.rb5
-rw-r--r--spec/services/projects/update_service_spec.rb270
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb56
-rw-r--r--spec/services/search/global_service_spec.rb4
-rw-r--r--spec/services/system_hooks_service_spec.rb44
-rw-r--r--spec/services/system_note_service_spec.rb88
-rw-r--r--spec/services/todo_service_spec.rb12
-rw-r--r--spec/services/users/keys_count_service_spec.rb66
-rw-r--r--spec/services/users/last_push_event_service_spec.rb1
-rw-r--r--spec/spec_helper.rb5
-rw-r--r--spec/support/api/issues_resolving_discussions_shared_examples.rb2
-rw-r--r--spec/support/api/members_shared_examples.rb2
-rw-r--r--spec/support/api/milestones_shared_examples.rb66
-rw-r--r--spec/support/api/scopes/read_user_shared_examples.rb18
-rw-r--r--spec/support/api/time_tracking_shared_examples.rb18
-rw-r--r--spec/support/api/v3/time_tracking_shared_examples.rb18
-rw-r--r--spec/support/api_helpers.rb28
-rw-r--r--spec/support/bare_repo_operations.rb60
-rw-r--r--spec/support/board_helpers.rb16
-rw-r--r--spec/support/capybara.rb44
-rw-r--r--spec/support/capybara_helpers.rb2
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb36
-rw-r--r--spec/support/cookie_helper.rb17
-rw-r--r--spec/support/cycle_analytics_helpers.rb1
-rw-r--r--spec/support/email_helpers.rb14
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb56
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb10
-rw-r--r--spec/support/features/reportable_note_shared_examples.rb2
-rw-r--r--spec/support/fixture_helpers.rb1
-rwxr-xr-xspec/support/generate-seed-repo-rb1
-rw-r--r--spec/support/gitaly.rb9
-rw-r--r--spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535bin0 -> 304 bytes
-rw-r--r--spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cfbin0 -> 597 bytes
-rw-r--r--spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb162
-rw-r--r--spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31ebin0 -> 185 bytes
-rw-r--r--spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3fbin0 -> 642 bytes
-rw-r--r--spec/support/gitlab_stubs/session.json4
-rw-r--r--spec/support/gitlab_stubs/user.json6
-rw-r--r--spec/support/google_api/cloud_platform_helpers.rb119
-rw-r--r--spec/support/helpers/merge_request_diff_helpers.rb2
-rw-r--r--spec/support/helpers/note_interaction_helpers.rb2
-rw-r--r--spec/support/input_helper.rb7
-rw-r--r--spec/support/inspect_requests.rb17
-rw-r--r--spec/support/jira_service_helper.rb2
-rw-r--r--spec/support/kubernetes_helpers.rb37
-rw-r--r--spec/support/ldap_helpers.rb5
-rw-r--r--spec/support/legacy_path_redirect_shared_examples.rb13
-rw-r--r--spec/support/live_debugger.rb17
-rw-r--r--spec/support/login_helpers.rb30
-rw-r--r--spec/support/matchers/access_matchers_for_controller.rb2
-rw-r--r--spec/support/matchers/be_a_binary_string.rb9
-rw-r--r--spec/support/matchers/have_gitlab_http_status.rb6
-rw-r--r--spec/support/matchers/security_header_matcher.rb5
-rw-r--r--spec/support/mobile_helpers.rb2
-rw-r--r--spec/support/project_forks_helper.rb2
-rw-r--r--spec/support/project_hook_data_shared_example.rb42
-rw-r--r--spec/support/prometheus/additional_metrics_shared_examples.rb28
-rw-r--r--spec/support/protected_tags/access_control_ce_shared_examples.rb4
-rw-r--r--spec/support/query_recorder.rb18
-rw-r--r--spec/support/quick_actions_helpers.rb2
-rw-r--r--spec/support/redis_without_keys.rb8
-rw-r--r--spec/support/select2_helper.rb1
-rw-r--r--spec/support/selection_helper.rb6
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce.rb10
-rw-r--r--spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb57
-rw-r--r--spec/support/shared_examples/models/project_hook_data_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/requests/api/status_shared_examples.rb7
-rw-r--r--spec/support/slack_mattermost_notifications_shared_examples.rb3
-rw-r--r--spec/support/stub_configuration.rb8
-rw-r--r--spec/support/test_env.rb2
-rw-r--r--spec/support/time_tracking_shared_examples.rb2
-rw-r--r--spec/support/unique_ip_check_shared_examples.rb8
-rw-r--r--spec/support/update_invalid_issuable.rb27
-rw-r--r--spec/support/wait_for_requests.rb36
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb215
-rw-r--r--spec/tasks/gitlab/cleanup_rake_spec.rb67
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb26
-rw-r--r--spec/tasks/gitlab/ldap_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/users_rake_spec.rb38
-rw-r--r--spec/tasks/tokens_spec.rb6
-rw-r--r--spec/unicorn/unicorn_spec.rb22
-rw-r--r--spec/uploaders/file_uploader_spec.rb78
-rw-r--r--spec/validators/dynamic_path_validator_spec.rb97
-rw-r--r--spec/validators/namespace_path_validator_spec.rb38
-rw-r--r--spec/validators/project_path_validator_spec.rb38
-rw-r--r--spec/validators/user_path_validator_spec.rb38
-rw-r--r--spec/views/help/index.html.haml_spec.rb8
-rw-r--r--spec/views/help/instance_configuration.html.haml_spec.rb29
-rw-r--r--spec/views/projects/commit/branches.html.haml_spec.rb109
-rw-r--r--spec/views/projects/jobs/show.html.haml_spec.rb25
-rw-r--r--spec/views/projects/pipelines_settings/_show.html.haml_spec.rb2
-rw-r--r--spec/views/shared/issuable/_participants.html.haml.rb26
-rw-r--r--spec/workers/cluster_provision_worker_spec.rb19
-rw-r--r--spec/workers/concerns/gitlab/github_import/notify_upon_death_spec.rb49
-rw-r--r--spec/workers/concerns/gitlab/github_import/object_importer_spec.rb70
-rw-r--r--spec/workers/concerns/gitlab/github_import/queue_spec.rb12
-rw-r--r--spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb110
-rw-r--r--spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb77
-rw-r--r--spec/workers/create_pipeline_worker_spec.rb36
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/advance_stage_worker_spec.rb115
-rw-r--r--spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb42
-rw-r--r--spec/workers/gitlab/github_import/import_issue_worker_spec.rb45
-rw-r--r--spec/workers/gitlab/github_import/import_note_worker_spec.rb40
-rw-r--r--spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb51
-rw-r--r--spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb95
-rw-r--r--spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb32
-rw-r--r--spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb30
-rw-r--r--spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb32
-rw-r--r--spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb29
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb32
-rw-r--r--spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb49
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb31
-rw-r--r--spec/workers/project_migrate_hashed_storage_worker_spec.rb52
-rw-r--r--spec/workers/reactive_caching_worker_spec.rb25
-rw-r--r--spec/workers/repository_fork_worker_spec.rb22
-rw-r--r--spec/workers/repository_import_worker_spec.rb40
-rw-r--r--spec/workers/stuck_ci_jobs_worker_spec.rb10
-rw-r--r--spec/workers/stuck_merge_jobs_worker_spec.rb9
-rw-r--r--spec/workers/update_merge_requests_worker_spec.rb12
-rw-r--r--spec/workers/wait_for_cluster_creation_worker_spec.rb61
-rw-r--r--vendor/assets/javascripts/autosize.js243
-rw-r--r--vendor/assets/javascripts/clipboard.js621
-rw-r--r--vendor/assets/javascripts/fuzzaldrin-plus.js1161
-rw-r--r--vendor/assets/javascripts/latinise.js11
-rw-r--r--vendor/assets/javascripts/peek.js28
-rw-r--r--vendor/gitignore/Android.gitignore5
-rw-r--r--vendor/gitignore/Autotools.gitignore9
-rw-r--r--vendor/gitignore/Composer.gitignore2
-rw-r--r--vendor/gitignore/Elixir.gitignore2
-rw-r--r--vendor/gitignore/ExtJs.gitignore2
-rw-r--r--vendor/gitignore/Global/Matlab.gitignore2
-rw-r--r--vendor/gitignore/Global/Windows.gitignore2
-rw-r--r--vendor/gitignore/Global/Xcode.gitignore18
-rw-r--r--vendor/gitignore/Global/macOS.gitignore2
-rw-r--r--vendor/gitignore/Joomla.gitignore2
-rw-r--r--vendor/gitignore/OCaml.gitignore3
-rw-r--r--vendor/gitignore/Perl.gitignore2
-rw-r--r--vendor/gitignore/Python.gitignore5
-rw-r--r--vendor/gitignore/Qt.gitignore2
-rw-r--r--vendor/gitignore/TeX.gitignore1
-rw-r--r--vendor/gitignore/Terraform.gitignore12
-rw-r--r--vendor/gitignore/Umbraco.gitignore4
-rw-r--r--vendor/gitignore/VisualStudio.gitignore12
-rw-r--r--vendor/gitignore/ZendFramework.gitignore1
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml4
-rw-r--r--vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Go.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Maven.gitlab-ci.yml7
-rw-r--r--vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml6
-rw-r--r--vendor/gitlab-ci-yml/Python.gitlab-ci.yml32
-rw-r--r--vendor/gitlab-ci-yml/Rust.gitlab-ci.yml6
-rw-r--r--vendor/gitlab-ci-yml/dotNET.gitlab-ci.yml86
-rw-r--r--vendor/licenses.csv206
-rw-r--r--vendor/project_templates/express.tar.gzbin5648 -> 5651 bytes
-rw-r--r--vendor/project_templates/rails.tar.gzbin25063 -> 25065 bytes
-rw-r--r--vendor/project_templates/spring.tar.gzbin50838 -> 50845 bytes
-rw-r--r--yarn.lock1305
3293 files changed, 95843 insertions, 41135 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index fb170515c1d..60a2b5d5b5b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,7 +1,7 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-phantomjs-2.1-node-8.x-yarn-1.0-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-chrome-62.0-node-8.x-yarn-1.2-postgresql-9.6"
.default-cache: &default-cache
- key: "ruby-233-with-yarn"
+ key: "ruby-235-with-yarn"
paths:
- vendor/ruby
- .yarn-cache/
@@ -23,7 +23,6 @@ variables:
SIMPLECOV: "true"
GIT_DEPTH: "20"
GIT_SUBMODULE_STRATEGY: "none"
- PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
@@ -49,11 +48,11 @@ stages:
- gitlab-org
.tests-metadata-state: &tests-metadata-state
- services: []
+ <<: *dedicated-runner
variables:
- SETUP_DB: "false"
- USE_BUNDLE_INSTALL: "false"
TESTS_METADATA_S3_BUCKET: "gitlab-ce-cache"
+ before_script:
+ - source scripts/utils.sh
artifacts:
expire_in: 31d
paths:
@@ -80,6 +79,7 @@ stages:
.rspec-metadata: &rspec-metadata
<<: *dedicated-runner
<<: *pull-cache
+ <<: *except-docs
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
@@ -109,16 +109,15 @@ stages:
.rspec-metadata-pg: &rspec-metadata-pg
<<: *rspec-metadata
<<: *use-pg
- <<: *except-docs
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
<<: *use-mysql
- <<: *except-docs
.spinach-metadata: &spinach-metadata
<<: *dedicated-runner
<<: *pull-cache
+ <<: *except-docs
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
@@ -141,12 +140,10 @@ stages:
.spinach-metadata-pg: &spinach-metadata-pg
<<: *spinach-metadata
<<: *use-pg
- <<: *except-docs
.spinach-metadata-mysql: &spinach-metadata-mysql
<<: *spinach-metadata
<<: *use-mysql
- <<: *except-docs
.only-canonical-masters: &only-canonical-masters
only:
@@ -155,14 +152,12 @@ stages:
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
+##
# Trigger a package build in omnibus-gitlab repository
-build-package:
- image: ruby:2.3-alpine
+#
+package-qa:
+ image: ruby:2.4-alpine
before_script: []
- services: []
- variables:
- SETUP_DB: "false"
- USE_BUNDLE_INSTALL: "false"
stage: build
cache: {}
when: manual
@@ -183,13 +178,9 @@ build-package:
- apk add --update openssl
- wget https://gitlab.com/gitlab-org/gitlab-ce/raw/master/scripts/trigger-build-docs
- chmod 755 trigger-build-docs
- services: []
cache: {}
dependencies: []
- artifacts: {}
variables:
- SETUP_DB: "false"
- USE_BUNDLE_INSTALL: "false"
GIT_STRATEGY: none
when: manual
only:
@@ -204,7 +195,7 @@ review-docs-deploy:
name: review-docs/$CI_COMMIT_REF_NAME
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
- url: http://preview-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
+ url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
script:
- ./trigger-build-docs deploy
@@ -222,7 +213,6 @@ review-docs-cleanup:
# Retrieve knapsack and rspec_flaky reports
retrieve-tests-metadata:
<<: *tests-metadata-state
- <<: *dedicated-runner
<<: *except-docs
stage: prepare
cache:
@@ -240,7 +230,6 @@ retrieve-tests-metadata:
update-tests-metadata:
<<: *tests-metadata-state
- <<: *dedicated-runner
<<: *only-canonical-masters
stage: post-test
cache:
@@ -269,7 +258,8 @@ flaky-examples-check:
USE_BUNDLE_INSTALL: "false"
NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json
stage: post-test
- allow_failure: yes
+ allow_failure: true
+ retry: 0
only:
- branches
except:
@@ -304,69 +294,69 @@ setup-test-env:
- public/assets
- tmp/tests
-rspec-pg 0 25: *rspec-metadata-pg
-rspec-pg 1 25: *rspec-metadata-pg
-rspec-pg 2 25: *rspec-metadata-pg
-rspec-pg 3 25: *rspec-metadata-pg
-rspec-pg 4 25: *rspec-metadata-pg
-rspec-pg 5 25: *rspec-metadata-pg
-rspec-pg 6 25: *rspec-metadata-pg
-rspec-pg 7 25: *rspec-metadata-pg
-rspec-pg 8 25: *rspec-metadata-pg
-rspec-pg 9 25: *rspec-metadata-pg
-rspec-pg 10 25: *rspec-metadata-pg
-rspec-pg 11 25: *rspec-metadata-pg
-rspec-pg 12 25: *rspec-metadata-pg
-rspec-pg 13 25: *rspec-metadata-pg
-rspec-pg 14 25: *rspec-metadata-pg
-rspec-pg 15 25: *rspec-metadata-pg
-rspec-pg 16 25: *rspec-metadata-pg
-rspec-pg 17 25: *rspec-metadata-pg
-rspec-pg 18 25: *rspec-metadata-pg
-rspec-pg 19 25: *rspec-metadata-pg
-rspec-pg 20 25: *rspec-metadata-pg
-rspec-pg 21 25: *rspec-metadata-pg
-rspec-pg 22 25: *rspec-metadata-pg
-rspec-pg 23 25: *rspec-metadata-pg
-rspec-pg 24 25: *rspec-metadata-pg
-
-rspec-mysql 0 25: *rspec-metadata-mysql
-rspec-mysql 1 25: *rspec-metadata-mysql
-rspec-mysql 2 25: *rspec-metadata-mysql
-rspec-mysql 3 25: *rspec-metadata-mysql
-rspec-mysql 4 25: *rspec-metadata-mysql
-rspec-mysql 5 25: *rspec-metadata-mysql
-rspec-mysql 6 25: *rspec-metadata-mysql
-rspec-mysql 7 25: *rspec-metadata-mysql
-rspec-mysql 8 25: *rspec-metadata-mysql
-rspec-mysql 9 25: *rspec-metadata-mysql
-rspec-mysql 10 25: *rspec-metadata-mysql
-rspec-mysql 11 25: *rspec-metadata-mysql
-rspec-mysql 12 25: *rspec-metadata-mysql
-rspec-mysql 13 25: *rspec-metadata-mysql
-rspec-mysql 14 25: *rspec-metadata-mysql
-rspec-mysql 15 25: *rspec-metadata-mysql
-rspec-mysql 16 25: *rspec-metadata-mysql
-rspec-mysql 17 25: *rspec-metadata-mysql
-rspec-mysql 18 25: *rspec-metadata-mysql
-rspec-mysql 19 25: *rspec-metadata-mysql
-rspec-mysql 20 25: *rspec-metadata-mysql
-rspec-mysql 21 25: *rspec-metadata-mysql
-rspec-mysql 22 25: *rspec-metadata-mysql
-rspec-mysql 23 25: *rspec-metadata-mysql
-rspec-mysql 24 25: *rspec-metadata-mysql
-
-spinach-pg 0 5: *spinach-metadata-pg
-spinach-pg 1 5: *spinach-metadata-pg
-spinach-pg 2 5: *spinach-metadata-pg
-spinach-pg 3 5: *spinach-metadata-pg
-spinach-pg 4 5: *spinach-metadata-pg
-
-spinach-mysql 0 5: *spinach-metadata-mysql
-spinach-mysql 1 5: *spinach-metadata-mysql
-spinach-mysql 2 5: *spinach-metadata-mysql
-spinach-mysql 3 5: *spinach-metadata-mysql
-spinach-mysql 4 5: *spinach-metadata-mysql
+rspec-pg 0 26: *rspec-metadata-pg
+rspec-pg 1 26: *rspec-metadata-pg
+rspec-pg 2 26: *rspec-metadata-pg
+rspec-pg 3 26: *rspec-metadata-pg
+rspec-pg 4 26: *rspec-metadata-pg
+rspec-pg 5 26: *rspec-metadata-pg
+rspec-pg 6 26: *rspec-metadata-pg
+rspec-pg 7 26: *rspec-metadata-pg
+rspec-pg 8 26: *rspec-metadata-pg
+rspec-pg 9 26: *rspec-metadata-pg
+rspec-pg 10 26: *rspec-metadata-pg
+rspec-pg 11 26: *rspec-metadata-pg
+rspec-pg 12 26: *rspec-metadata-pg
+rspec-pg 13 26: *rspec-metadata-pg
+rspec-pg 14 26: *rspec-metadata-pg
+rspec-pg 15 26: *rspec-metadata-pg
+rspec-pg 16 26: *rspec-metadata-pg
+rspec-pg 17 26: *rspec-metadata-pg
+rspec-pg 18 26: *rspec-metadata-pg
+rspec-pg 19 26: *rspec-metadata-pg
+rspec-pg 20 26: *rspec-metadata-pg
+rspec-pg 21 26: *rspec-metadata-pg
+rspec-pg 22 26: *rspec-metadata-pg
+rspec-pg 23 26: *rspec-metadata-pg
+rspec-pg 24 26: *rspec-metadata-pg
+rspec-pg 25 26: *rspec-metadata-pg
+
+rspec-mysql 0 26: *rspec-metadata-mysql
+rspec-mysql 1 26: *rspec-metadata-mysql
+rspec-mysql 2 26: *rspec-metadata-mysql
+rspec-mysql 3 26: *rspec-metadata-mysql
+rspec-mysql 4 26: *rspec-metadata-mysql
+rspec-mysql 5 26: *rspec-metadata-mysql
+rspec-mysql 6 26: *rspec-metadata-mysql
+rspec-mysql 7 26: *rspec-metadata-mysql
+rspec-mysql 8 26: *rspec-metadata-mysql
+rspec-mysql 9 26: *rspec-metadata-mysql
+rspec-mysql 10 26: *rspec-metadata-mysql
+rspec-mysql 11 26: *rspec-metadata-mysql
+rspec-mysql 12 26: *rspec-metadata-mysql
+rspec-mysql 13 26: *rspec-metadata-mysql
+rspec-mysql 14 26: *rspec-metadata-mysql
+rspec-mysql 15 26: *rspec-metadata-mysql
+rspec-mysql 16 26: *rspec-metadata-mysql
+rspec-mysql 17 26: *rspec-metadata-mysql
+rspec-mysql 18 26: *rspec-metadata-mysql
+rspec-mysql 19 26: *rspec-metadata-mysql
+rspec-mysql 20 26: *rspec-metadata-mysql
+rspec-mysql 21 26: *rspec-metadata-mysql
+rspec-mysql 22 26: *rspec-metadata-mysql
+rspec-mysql 23 26: *rspec-metadata-mysql
+rspec-mysql 24 26: *rspec-metadata-mysql
+rspec-mysql 25 26: *rspec-metadata-mysql
+
+spinach-pg 0 4: *spinach-metadata-pg
+spinach-pg 1 4: *spinach-metadata-pg
+spinach-pg 2 4: *spinach-metadata-pg
+spinach-pg 3 4: *spinach-metadata-pg
+
+spinach-mysql 0 4: *spinach-metadata-mysql
+spinach-mysql 1 4: *spinach-metadata-mysql
+spinach-mysql 2 4: *spinach-metadata-mysql
+spinach-mysql 3 4: *spinach-metadata-mysql
# Static analysis jobs
.ruby-static-analysis: &ruby-static-analysis
@@ -428,14 +418,10 @@ ee_compat_check:
- /^[\d-]+-stable(-ee)?/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
- allow_failure: yes
- cache:
- key: "ee_compat_check_repo"
- paths:
- - ee_compat_check/ee-repo/
+ retry: 0
artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
- when: on_failure
+ when: always
expire_in: 10d
paths:
- ee_compat_check/patches/*.patch
@@ -464,8 +450,9 @@ db:migrate:reset-mysql:
stage: test
variables:
SETUP_DB: "false"
+ CREATE_DB_USER: "true"
script:
- - git fetch origin v8.14.10
+ - git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD
- bundle install $BUNDLE_INSTALL_FLAGS
- cp config/gitlab.yml.example config/gitlab.yml
@@ -489,7 +476,7 @@ migration:path-mysql:
<<: *pull-cache
stage: test
script:
- - bundle exec rake db:rollback STEP=120
+ - bundle exec rake db:rollback STEP=119
- bundle exec rake db:migrate
db:rollback-pg:
@@ -508,6 +495,7 @@ db:rollback-mysql:
variables:
SIZE: "1"
SETUP_DB: "false"
+ CREATE_DB_USER: "true"
script:
- git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git
@@ -543,7 +531,6 @@ gitlab:assets:compile:
NODE_ENV: "production"
RAILS_ENV: "production"
SETUP_DB: "false"
- USE_DB: "false"
SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true"
NO_COMPRESSION: "true"
@@ -561,7 +548,6 @@ karma:
<<: *dedicated-runner
<<: *except-docs
<<: *pull-cache
- image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6"
stage: test
variables:
BABEL_ENV: "coverage"
@@ -579,7 +565,7 @@ karma:
- chrome_debug.log
- coverage-javascript/
-codeclimate:
+codequality:
<<: *except-docs
<<: *pull-cache
before_script: []
@@ -599,6 +585,16 @@ codeclimate:
artifacts:
paths: [codeclimate.json]
+qa:internal:
+ stage: test
+ variables:
+ SETUP_DB: "false"
+ services: []
+ script:
+ - cd qa/
+ - bundle install
+ - bundle exec rspec
+
coverage:
<<: *dedicated-runner
<<: *except-docs
diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md
index 1278061a410..5b55eb1374b 100644
--- a/.gitlab/issue_templates/Feature Proposal.md
+++ b/.gitlab/issue_templates/Feature Proposal.md
@@ -1,22 +1,3 @@
-Please read this!
-
-Before opening a new issue, make sure to search for keywords in the issues
-filtered by the "feature proposal" label:
-
-For the Community Edition issue tracker:
-
-- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal
-
-For the Enterprise Edition issue tracker:
-
-- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=feature+proposal
-
-and verify the issue you're about to submit isn't a duplicate.
-
-Please remove this notice if you're confident your issue isn't a duplicate.
-
-------
-
### Description
(Include problem, use cases, benefits, and/or goals)
@@ -25,26 +6,4 @@ Please remove this notice if you're confident your issue isn't a duplicate.
### Links / references
-### Documentation blurb
-
-#### Overview
-
-What is it?
-Why should someone use this feature?
-What is the underlying (business) problem?
-How do you use this feature?
-
-#### Use cases
-
-Who is this for? Provide one or more use cases.
-
-### Feature checklist
-
-Make sure these are completed before closing the issue,
-with a link to the relevant commit.
-
-- [ ] [Feature assurance](https://about.gitlab.com/handbook/product/#feature-assurance)
-- [ ] Documentation
-- [ ] Added to [features.yml](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml)
-
-/label ~"feature proposal" \ No newline at end of file
+/label ~"feature proposal"
diff --git a/.gitlab/merge_request_templates/Database Changes.md b/.gitlab/merge_request_templates/Database Changes.md
index 2a5c8267872..8302b3b30c7 100644
--- a/.gitlab/merge_request_templates/Database Changes.md
+++ b/.gitlab/merge_request_templates/Database Changes.md
@@ -1,20 +1,5 @@
-Remove this section and replace it with a description of your MR. Also follow the
-checklist below and check off any tasks that are done. If a certain task can not
-be done you should explain so in the MR body. You are free to remove any
-sections that do not apply to your MR.
-
-When gathering statistics (e.g. the output of `EXPLAIN ANALYZE`) you should make
-sure your database has enough data. Having around 10 000 rows in the tables
-being queries should provide a reasonable estimate of how a query will behave.
-Also make sure that PostgreSQL uses the following settings:
-
-* `random_page_cost`: `1`
-* `work_mem`: `16MB`
-* `maintenance_work_mem`: at least `64MB`
-* `shared_buffers`: at least `256MB`
-
-If you have access to GitLab.com's staging environment you should also run your
-measurements there, and include the results in this MR.
+Add a description of your merge request here. Merge requests without an adequate
+description will not be reviewed until one is added.
## Database Checklist
@@ -23,34 +8,23 @@ When adding migrations:
- [ ] Updated `db/schema.rb`
- [ ] Added a `down` method so the migration can be reverted
- [ ] Added the output of the migration(s) to the MR body
-- [ ] Added the execution time of the migration(s) to the MR body
-- [ ] Added tests for the migration in `spec/migrations` if necessary (e.g. when
- migrating data)
-- [ ] Made sure the migration won't interfere with a running GitLab cluster,
- for example by disabling transactions for long running migrations
+- [ ] Added tests for the migration in `spec/migrations` if necessary (e.g. when migrating data)
When adding or modifying queries to improve performance:
-- [ ] Included the raw SQL queries of the relevant queries
-- [ ] Included the output of `EXPLAIN ANALYZE` and execution timings of the
- relevant queries
-- [ ] Added tests for the relevant changes
-
-When adding indexes:
-
-- [ ] Described the need for these indexes in the MR body
-- [ ] Made sure existing indexes can not be reused instead
+- [ ] Included data that shows the performance improvement, preferably in the form of a benchmark
+- [ ] Included the output of `EXPLAIN (ANALYZE, BUFFERS)` of the relevant queries
When adding foreign keys to existing tables:
-- [ ] Included a migration to remove orphaned rows in the source table
+- [ ] Included a migration to remove orphaned rows in the source table before adding the foreign key
- [ ] Removed any instances of `dependent: ...` that may no longer be necessary
When adding tables:
-- [ ] Ordered columns based on their type sizes in descending order
-- [ ] Added foreign keys if necessary
-- [ ] Added indexes if necessary
+- [ ] Ordered columns based on the [Ordering Table Columns](https://docs.gitlab.com/ee/development/ordering_table_columns.html#ordering-table-columns) guidelines
+- [ ] Added foreign keys to any columns pointing to data in other tables
+- [ ] Added indexes for fields that are used in statements such as WHERE, ORDER BY, GROUP BY, and JOINs
When removing columns, tables, indexes or other structures:
@@ -64,8 +38,6 @@ When removing columns, tables, indexes or other structures:
- [ ] API support added
- [ ] Tests added for this feature/bug
- Review
- - [ ] Has been reviewed by UX
- - [ ] Has been reviewed by Frontend
- [ ] Has been reviewed by Backend
- [ ] Has been reviewed by Database
- [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
diff --git a/.gitlab/route-map.yml b/.gitlab/route-map.yml
new file mode 100644
index 00000000000..0b37dc68f8b
--- /dev/null
+++ b/.gitlab/route-map.yml
@@ -0,0 +1,3 @@
+# Documentation
+- source: /doc/(.+?)\.md/ # doc/administration/build_artifacts.md
+ public: '\1.html' # doc/administration/build_artifacts.html
diff --git a/.nvmrc b/.nvmrc
index 72906051c5c..f7ee06693c1 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-7.5 \ No newline at end of file
+9.0.0
diff --git a/.rubocop.yml b/.rubocop.yml
index dbeb1880d39..c427f219a0d 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -624,7 +624,7 @@ Style/PredicateName:
# branches, and conditions.
Metrics/AbcSize:
Enabled: true
- Max: 55.25
+ Max: 54.28
# This cop checks if the length of a block exceeds some maximum value.
Metrics/BlockLength:
@@ -665,7 +665,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity:
Enabled: true
- Max: 15
+ Max: 14
# Lint ########################################################################
diff --git a/.ruby-version b/.ruby-version
index 0bee604df76..cc6c9a491e0 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.3.3
+2.3.5
diff --git a/.scss-lint.yml b/.scss-lint.yml
index 73f8d27f78c..dcd4cac780a 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -14,7 +14,7 @@ linters:
# Whether or not to prefer `border: 0` over `border: none`.
BorderZero:
- enabled: false
+ enabled: true
# Reports when you define a rule set using a selector with chained classes
# (a.k.a. adjoining classes).
@@ -112,7 +112,7 @@ linters:
# Reports when you define the same selector twice in a single sheet.
MergeableSelector:
- enabled: false
+ enabled: true
# Functions, mixins, variables, and placeholders should be declared
# with all lowercase letters and hyphens instead of underscores.
@@ -121,7 +121,8 @@ linters:
# Avoid nesting selectors too deeply.
NestingDepth:
- enabled: false
+ enabled: true
+ max_depth: 6
# Always use placeholder selectors in @extend.
PlaceholderInExtend:
@@ -240,7 +241,7 @@ linters:
# Numeric values should not contain unnecessary fractional portions.
UnnecessaryMantissa:
- enabled: false
+ enabled: true
# Do not use parent selector references (&) when they would otherwise
# be unnecessary.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index efd32d44890..92dd4d7610f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,494 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.2.2 (2017-11-23)
+
+### Fixed (5 changes)
+
+- Label addition/removal are not going to be redacted wrongfully in the API. !15080
+- Fix bitbucket wiki import with hashed storage enabled. !15490
+- Impersonation no longer gets stuck on password change. !15497
+- Fix blank states using old css.
+- Fix promoting milestone updating all issuables without milestone.
+
+### Performance (3 changes)
+
+- Update Issue Boards to fetch the notification subscription status asynchronously.
+- Update composite pipelines index to include "id".
+- Use arrays in Pipeline#latest_builds_with_artifacts.
+
+### Other (2 changes)
+
+- Don't move repositories and attachments for projects using hashed storage. !15479
+- Add logs for monitoring the merge process.
+
+
+## 10.2.1 (2017-11-22)
+
+### Fixed (1 change)
+
+- Force disable Prometheus metrics.
+
+
+## 10.2.0 (2017-11-22)
+
+### Security (4 changes)
+
+- Upgrade Ruby to 2.3.5 to include security patches. !15099
+- Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization.
+- Convert private tokens to Personal Access Tokens with sudo scope.
+- Remove private tokens from web interface and API.
+
+### Removed (5 changes)
+
+- Remove help text from group issues page and group merge requests page. !14963
+- Remove overzealous tooltips in projects page tabs. !15017
+- Stop merge requests from fetching their refs when the data is already available. !15129
+- Remove update merge request worker tagging.
+- Remove Session API now that private tokens are removed from user API endpoints.
+
+### Fixed (75 changes, 18 of them are from the community)
+
+- Fix 404 errors in API caused when the branch name had a dot. !14462 (gvieira37)
+- Remove unnecessary alt-texts from pipeline emails. !14602 (gernberg)
+- Renders 404 in commits controller if no commits are found for a given path. !14610 (Guilherme Vieira)
+- Cleanup data-page attribute after each Karma test. !14742
+- Removed extra border radius from .file-editor and .file-holder when editing a file. !14803 (Rachel Pipkin)
+- Add support for markdown preview to group milestones. !14806 (Vitaliy @blackst0ne Klachkov)
+- Fixed 'Removed source branch' checkbox in merge widget being ignored. !14832
+- Fix unnecessary ajax requests in admin broadcast message form. !14853
+- Make NamespaceSelect change URL when filtering. !14888
+- Get true failure from evalulate_script by checking for element beforehand. !14898
+- Fix SAML error 500 when no groups are defined for user. !14913
+- Fix 500 errors caused by empty diffs in some discussions. !14945 (Alexander Popov)
+- Fix the atom feed for group events. !14974
+- Hides pipeline duration in commit box when it is zero (nil). !14979 (gvieira37)
+- Add new diff discussions on MR diffs tab in "realtime". !14981
+- Returns a ssh url for go-get=1. !14990 (gvieira37)
+- Case insensitive search for branches. !14995 (George Andrinopoulos)
+- Fixes 404 error to 'Issues assigned to me' and 'Issues I've created' when issues are disabled. !15021 (Jacopo Beschi @jacopo-beschi)
+- Update the groups API documentation. !15024 (Robert Schilling)
+- Validate username/pw for Jiraservice, require them in the API. !15025 (Robert Schilling)
+- Update Merge Request polling so there is only one request at a time. !15032
+- Use project select dropdown not only as a combobutton. !15043
+- Remove create MR button from issues when MRs are disabled. !15071 (George Andrinopoulos)
+- Tighten up whitelisting of certain Geo routes. !15082
+- Allow to disable the Performance Bar. !15084
+- Refresh open Issue and Merge Request project counter caches when re-opening. !15085 (Rob Ede @robjtede)
+- Fix markdown form tabs toggling preview mode from double clicking write mode button. !15119
+- Fix cancel button not working while uploading on the new issue page. !15137
+- Fix webhooks recent deliveries. !15146 (Alexander Randa (@randaalex))
+- Fix issues with forked projects of which the source was deleted. !15150
+- Fix GPG signature popup info in Safari and Firefox. !15228
+- Fix GFM reference links for closed milestones. !15234 (Vitaliy @blackst0ne Klachkov)
+- When deleting merged branches, ignore protected tags. !15252
+- Revert a regression on runners sorting (!15134). !15341 (Takuya Noguchi)
+- Don't use JS to delete memberships from projects and groups. !15344
+- Don't try to create fork network memberships for forks with a missing source. !15366
+- Fix gitlab:backup rake for hashed storage based repositories. !15400
+- Fix issue where clicking a GPG verification badge would scroll to the top of the page. !15407
+- Update container repository path reference and allow using double underscore. !15417
+- Fix crash when navigating to second page of the group dashbaord when there are projects and groups on the first page. !15456
+- Fix flash errors showing up on a non configured prometheus integration. !35652
+- Fix timezone bug in Pikaday and upgrade Pikaday version.
+- Fix arguments Import/Export error importing project merge requests.
+- Moves mini graph of pipeline to the end of sentence in MR widget. Cleans HTML and tests.
+- Fix user autocomplete in subgroups.
+- Fixed user profile activity tab being off-screen on mobile.
+- Fix diff parser so it tolerates to diff special markers in the content.
+- Fix a migration that adds merge_requests_ff_only_enabled column to MR table.
+- Don't create build failed todos when the job is automatically retried.
+- Render 404 when polling commit notes without having permissions.
+- Show error message when fast-forward merge is not possible.
+- Prevents position update for image diff notes.
+- Mobile-friendly table on Admin Runners. (Takuya Noguchi)
+- Decreases z-index of select2 to a lower number of our navigation bar.
+- Fix broken Members link when relative URL root paths are used.
+- Avoid regenerating the ref path for the environment.
+- Memoize GitLab logger to reduce open file descriptors.
+- Fix hashed storage with project transfers to another namespace.
+- Fix bad type checking to prevent 0 count badge to be shown.
+- Fix problem with issuable header wrapping when content is too long.
+- Move retry button in job page to sidebar.
+- Formats bytes to human reabale number in registry table.
+- Fix commit pipeline showing wrong status.
+- Include link to issue in reopen message for Slack and Mattermost notifications.
+- Fix double border UI bug on pipelines/environments table and pagination.
+- Remove native title tooltip in pipeline jobs dropdown in Safari.
+- Fix namespacing for MergeWhenPipelineSucceedsService in MR API.
+- Prevent error when authorizing an admin-created OAauth application without a set owner.
+- Always return full avatar URL for private/internal groups/projects when asset host is set.
+- Make sure group and project creation is blocked for new users that are external by default.
+- Make sure NotesActions#noteable returns a Noteable in the update action.
+- Reallow project paths ending in periods.
+- Only set Auto-Submitted header once for emails on push.
+- Fix overlap of right-sidebar and main content when creating a Wiki page.
+- Enables scroll to bottom once user has scrolled back to bottom in job log.
+
+### Changed (21 changes, 7 of them are from the community)
+
+- Added possibility to enter past date in /spend command to log time in the past. !3044 (g3dinua, LockiStrike)
+- Add Prometheus equivalent of all InfluxDB metrics. !13891
+- Show collapsible project lists. !14055
+- Make Prometheus metrics endpoint return empty response when metrics are disabled. !14490
+- Support custom attributes on groups and projects. !14593 (Markus Koller)
+- Avoid fetching all branches for branch existence checks. !14778
+- Update participants and subscriptions button in issuable sidebar to be async. !14836
+- Replace WikiPage::CreateService calls with wiki_page factory in specs. !14850 (Jacopo Beschi @jacopo-beschi)
+- Add lazy option to UserAvatarImage. !14895
+- Add readme only option as project view. !14900
+- Todos spelled correctly on Todos list page. !15015
+- Support uml:: and captions in reStructuredText. !15120 (Markus Koller)
+- Add system hooks user_rename and group_rename. !15123
+- Change tags order in refs dropdown. !15235 (Vitaliy @blackst0ne Klachkov)
+- Change default cluster size to n1-default-2. !39649 (Fabio Busatto)
+- Change 'Sign Out' route from a DELETE to a GET. !39708 (Joe Marty)
+- Change background color of nav sidebar to match other gl sidebars.
+- Update i18n section in FE docs for marking and interpolation.
+- Add a count of changes to the merge requests API.
+- Improve GitLab Import rake task to work with Hashed Storage and Subgroups.
+- 14830 Move GitLab export option to top of import list when creating a new project.
+
+### Performance (14 changes)
+
+- Improve branch listing page performance. !14729
+- Improve DashboardController#activity.json performance. !14985
+- Add a latest_merge_request_diff_id column to merge_requests. !15035
+- Improve performance of the /projects/:id/repository/branches API endpoint. !15215
+- Ensure merge requests with lots of version don't time out when searching for pipelines.
+- Speed up issues list APIs.
+- Remove Filesystem check metrics that use too much CPU to handle requests.
+- Disable Unicorn sampling in Sidekiq since there are no Unicorn sockets to monitor.
+- Truncate tree to max 1,000 items and display notice to users.
+- Add Performance improvement as category on the changelog.
+- Cache commits fetched from the repository.
+- Cache the number of user SSH keys.
+- Optimise getting the pipeline status of commits.
+- Improve performance of commits list by fully using DB index when getting commit note counts.
+
+### Added (26 changes, 10 of them are from the community)
+
+- Expose duration in Job entity. !13644 (Mehdi Lahmam (@mehlah))
+- Prevent git push when LFS objects are missing. !13837
+- Automatic configuration settings page. !13850 (Francisco Lopez)
+- Add API endpoints for Pages Domains. !13917 (Travis Miller)
+- Include the changes in issuable webhook payloads. !14308
+- Add Packagist project service. !14493 (Matt Coleman)
+- Add sort runners on admin runners. !14661 (Takuya Noguchi)
+- Repo Editor: Add option to start a new MR directly from comit section. !14665
+- Issue JWT token with registry:catalog:* scope when requested by GitLab admin. !14751 (Vratislav Kalenda)
+- Support show-all-refs for git over HTTP. !14834
+- Add loading button for new UX paradigm. !14883
+- Get Project Branch API shows an helpful error message on invalid refname. !14884 (Jacopo Beschi @jacopo-beschi)
+- Refactor have_http_status into have_gitlab_http_status. !14958 (Jacopo Beschi @jacopo-beschi)
+- Suggest to rename the remote for existing repository instructions. !14970 (helmo42)
+- Adds project_id to pipeline hook data. !15044 (Jacopo Beschi @jacopo-beschi)
+- Hashed Storage support for Attachments. !15068
+- Add metric tagging for sidekiq workers. !15111
+- Expose project visibility as CI variable - CI_PROJECT_VISIBILITY. !15193
+- Allow multiple queries in a single Prometheus graph to support additional environments (Canary, Staging, et al.). !15201
+- Allow promoting project milestones to group milestones.
+- Added submodule support in multi-file editor.
+- Add applications section to GKE clusters page to easily install Helm Tiller, Ingress.
+- Allow files to uploaded in the multi-file editor.
+- Add Ingress to available Cluster applications.
+- Adds typescript support.
+- Add sudo scope for OAuth and Personal Access Tokens to be used by admins to impersonate other users on the API.
+
+### Other (18 changes, 8 of them are from the community)
+
+- Decrease Perceived Complexity threshold to 14. !14231 (Maxim Rydkin)
+- Replace the 'features/explore/projects.feature' spinach test with an rspec analog. !14755 (Vitaliy @blackst0ne Klachkov)
+- While displaying a commit, do not show list of related branches if there are thousands of branches. !14812
+- Removed d3.js from the graph and users bundles and used the common_d3 bundle instead. !14826
+- Make contributors page translatable. !14915
+- Decrease ABC threshold to 54.28. !14920 (Maxim Rydkin)
+- Clarify system_hook triggers in documentation. !14957 (Joe Marty)
+- Free up some reserved group names. !15052
+- Bump carrierwave to 1.2.1. !15072 (Takuya Noguchi)
+- Enable NestingDepth (level 6) on scss-lint. !15073 (Takuya Noguchi)
+- Enable BorderZero rule in scss-lint. !15168 (Takuya Noguchi)
+- Internationalized tags page. !38589
+- Moves placeholders components into shared folder with documentation. Makes them easier to reuse in MR and Snippets comments.
+- Reorganize welcome page for new users.
+- Refactor GroupLinksController. (15121)
+- Remove filter icon from search bar.
+- Use title as placeholder instead of issue title for reusability.
+- Add Gitaly metrics to the performance bar.
+
+
+## 10.1.4 (2017-11-14)
+
+### Fixed (4 changes)
+
+- Don't try to create fork network memberships for forks with a missing source. !15366
+- Formats bytes to human reabale number in registry table.
+- Prevent error when authorizing an admin-created OAauth application without a set owner.
+- Prevents position update for image diff notes.
+
+
+## 10.1.3 (2017-11-10)
+
+- [SECURITY] Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization.
+- [FIXED] Fix cancel button not working while uploading on the new issue page. !15137
+- [FIXED] Fix webhooks recent deliveries. !15146 (Alexander Randa (@randaalex))
+- [FIXED] Fix issues with forked projects of which the source was deleted. !15150
+- [FIXED] Fix GPG signature popup info in Safari and Firefox. !15228
+- [FIXED] Make sure group and project creation is blocked for new users that are external by default.
+- [FIXED] Fix arguments Import/Export error importing project merge requests.
+- [FIXED] Fix diff parser so it tolerates to diff special markers in the content.
+- [FIXED] Fix a migration that adds merge_requests_ff_only_enabled column to MR table.
+- [FIXED] Render 404 when polling commit notes without having permissions.
+- [FIXED] Show error message when fast-forward merge is not possible.
+- [FIXED] Avoid regenerating the ref path for the environment.
+- [PERFORMANCE] Remove Filesystem check metrics that use too much CPU to handle requests.
+
+## 10.1.2 (2017-11-08)
+
+- [SECURITY] Add X-Content-Type-Options header in API responses to make it more difficult to find other vulnerabilities.
+- [SECURITY] Properly translate IP addresses written in decimal, octal, or other formats in SSRF protections in project imports.
+- [FIXED] Fix TRIGGER checks for MySQL.
+
+## 10.1.1 (2017-10-31)
+
+- [FIXED] Auto Devops kubernetes default namespace is now correctly built out of gitlab project group-name. !14642 (Mircea Danila Dumitrescu)
+- [FIXED] Forbid the usage of `Redis#keys`. !14889
+- [FIXED] Make the circuitbreaker more robust by adding higher thresholds, and multiple access attempts. !14933
+- [FIXED] Only cache last push event for existing projects when pushing to a fork. !14989
+- [FIXED] Fix bug preventing secondary emails from being confirmed. !15010
+- [FIXED] Fix broken wiki pages that link to a wiki file. !15019
+- [FIXED] Don't rename paths that were freed up when upgrading. !15029
+- [FIXED] Fix bitbucket login. !15051
+- [FIXED] Update gitaly in Gitlab 10.1 to 0.43.1 for temp file cleanup. !15055
+- [FIXED] Use the correct visibility attribute for projects in system hooks. !15065
+- [FIXED] Normalize LDAP DN when looking up identity.
+- [FIXED] Adds callback functions for initial request in clusters page.
+- [FIXED] Fix missing Import/Export issue assignees.
+- [FIXED] Allow boards as top level route.
+- [FIXED] Fix widget of locked merge requests not being presented.
+- [FIXED] Fix editing issue description in mobile view.
+- [FIXED] Fix deletion of container registry or images returning an error.
+- [FIXED] Fix the writing of invalid environment refs.
+- [CHANGED] Store circuitbreaker settings in the database instead of config. !14842
+- [CHANGED] Update default disabled merge request widget message to reflect a general failure. !14960
+- [PERFORMANCE] Stop merge requests with thousands of commits from timing out. !15063
+
+## 10.1.0 (2017-10-22)
+
+- [SECURITY] Use a timeout on certain git operations. !14872
+- [SECURITY] Move project repositories between namespaces when renaming users.
+- [SECURITY] Prevent an open redirect on project pages.
+- [SECURITY] Prevent a persistent XSS in user-provided markup.
+- [REMOVED] Remove the ability to visit the issue edit form directly. !14523
+- [REMOVED] Remove animate.js and label animation.
+- [FIXED] Perform prometheus data endpoint requests in parallel. !14003
+- [FIXED] Escape quotes in git username. !14020 (Brandon Everett)
+- [FIXED] Fixed non-UTF-8 valid branch names from causing an error. !14090
+- [FIXED] Read import sources from setting at first initialization. !14141 (Visay Keo)
+- [FIXED] Display full pre-receive and post-receive hook output in GitLab UI. !14222 (Robin Bobbitt)
+- [FIXED] Fix incorrect X-axis labels in Prometheus graphs. !14258
+- [FIXED] Fix the default branches sorting to actually be 'Last updated'. !14295
+- [FIXED] Fixes project denial of service via gitmodules using Extended ASCII. !14301
+- [FIXED] Fix the filesystem shard health check to check all configured shards. !14341
+- [FIXED] Compare email addresses case insensitively when verifying GPG signatures. !14376 (Tim Bishop)
+- [FIXED] Allow the git circuit breaker to correctly handle missing repository storages. !14417
+- [FIXED] Fix `rake gitlab:incoming_email:check` and make it report the actual error. !14423
+- [FIXED] Does not check if an invariant hashed storage path exists on disk when renaming projects. !14428
+- [FIXED] Also reserve refs/replace after importing a project. !14436
+- [FIXED] Fix profile image orientation based on EXIF data gvieira37. !14461 (gvieira37)
+- [FIXED] Move the deployment flag content to the left when deployment marker is near the end. !14514
+- [FIXED] Fix notes type created from import. This should fix some missing notes issues from imported projects. !14524
+- [FIXED] Fix bottom spacing for dropdowns that open upwards. !14535
+- [FIXED] Adjusts tag link to avoid underlining spaces. !14544 (Guilherme Vieira)
+- [FIXED] Add missing space in Sidekiq memory killer log message. !14553 (Benjamin Drung)
+- [FIXED] Ensure no exception is raised when Raven tries to get the current user in API context. !14580
+- [FIXED] Fix edit project service cancel button position. !14596 (Matt Coleman)
+- [FIXED] Fix case sensitive email confirmation on signup. !14606 (robdel12)
+- [FIXED] Whitelist authorized_keys.lock in the gitlab:check rake task. !14624
+- [FIXED] Allow merge in MR widget with no pipeline but using "Only allow merge requests to be merged if the pipeline succeeds". !14633
+- [FIXED] Fix navigation dropdown close animation on mobile screens. !14649
+- [FIXED] Fix the project import with issues and milestones. !14657
+- [FIXED] Use explicit boolean true attribute for show-disabled-button in Vue files. !14672
+- [FIXED] Make tabs on top scrollable on admin dashboard. !14685 (Takuya Noguchi)
+- [FIXED] Fix broken Y-axis scaling in some Prometheus graphs. !14693
+- [FIXED] Search or compare LDAP DNs case-insensitively and ignore excess whitespace. !14697
+- [FIXED] Allow prometheus graphs to correctly handle NaN values. !14741
+- [FIXED] Don't show an "Unsubscribe" link in snippet comment notifications. !14764
+- [FIXED] Fixed duplicate notifications when added multiple labels on an issue. !14798
+- [FIXED] Fix alignment for indeterminate marker in dropdowns. !14809
+- [FIXED] Fix error when updating a forked project with deleted `ForkedProjectLink`. !14916
+- [FIXED] Correctly render asset path for locales with a region. !14924
+- [FIXED] Fix the external URLs generated for online view of HTML artifacts. !14977
+- [FIXED] Reschedule merge request diff background migrations to catch failures from 9.5 run.
+- [FIXED] fix merge request widget status icon for failed CI.
+- [FIXED] Fix the number representing the amount of commits related to a push event.
+- [FIXED] Sync up hover and legend data across all graphs for the prometheus dashboard.
+- [FIXED] Fixes mini pipeline graph in commit view.
+- [FIXED] Fix comment deletion confirmation dialog typo.
+- [FIXED] Fix project snippets breadcrumb link.
+- [FIXED] Make usage ping scheduling more robust.
+- [FIXED] Make "merge ongoing" check more consistent.
+- [FIXED] Add 1000+ counters to job page.
+- [FIXED] Fixed issue/merge request breadcrumb titles not having links.
+- [FIXED] Fixed commit avatars being centered vertically.
+- [FIXED] Tooltips in the commit info box now all face the same direction. (Jedidiah Broadbent)
+- [FIXED] Fixed navbar title colors leaking out of the navbar.
+- [FIXED] Fix bug that caused merge requests with diff notes imported from Bitbucket to raise errors.
+- [FIXED] Correctly detect multiple issue URLs after 'Closes...' in MR descriptions.
+- [FIXED] Set default scope on PATs that don't have one set to allow them to be revoked.
+- [FIXED] Fix application setting to cache nil object.
+- [FIXED] Fix image diff swipe handle offset to correctly align with the frame.
+- [FIXED] Force non diff resolved discussion to display when collapse toggled.
+- [FIXED] Fix resolved discussions not expanding on side by side view.
+- [FIXED] Fixed the sidebar scrollbar overlapping links.
+- [FIXED] Issue board tooltips are now the correct width when the column is collapsed. (Jedidiah Broadbent)
+- [FIXED] Improve autodevops banner UX and render it only in project page.
+- [FIXED] Fix typo in cycle analytics breaking time component.
+- [FIXED] Force two up view to load by default for image diffs.
+- [FIXED] Fixed milestone breadcrumb links.
+- [FIXED] Fixed group sort dropdown defaulting to empty.
+- [FIXED] Fixed notes not being scrolled to in merge requests.
+- [FIXED] Adds Event polyfill for IE11.
+- [FIXED] Update native unicode emojis to always render as normal text (previously could render italicized). (Branka Martinovic)
+- [FIXED] Sort JobsController by id, not created_at.
+- [FIXED] Fix revision and total size missing for Container Registry.
+- [FIXED] Fixed milestone issuable assignee link URL.
+- [FIXED] Fixed breadcrumbs container expanding in side-by-side diff view.
+- [FIXED] Fixed merge request widget merged & closed date tooltip text.
+- [FIXED] Prevent creating multiple ApplicationSetting instances.
+- [FIXED] Fix username and ID not logging in production_json.log for Git activity.
+- [FIXED] Make Redcarpet Markdown renderer thread-safe.
+- [FIXED] Two factor auth messages in settings no longer overlap the button. (Jedidiah Broadbent)
+- [FIXED] Made the "remember me" check boxes have consistent styles and alignment. (Jedidiah Broadbent)
+- [FIXED] Prevent branches or tags from starting with invalid characters (e.g. -, .).
+- [DEPRECATED] Removed two legacy config options. (Daniel Voogsgerd)
+- [CHANGED] Show notes number more user-friendly in the graph. !13949 (Vladislav Kaverin)
+- [CHANGED] Link SAML users to LDAP by email. !14216
+- [CHANGED] Display whether branch has been merged when deleting protected branch. !14220
+- [CHANGED] Make the labels in the Compare form less confusing. !14225
+- [CHANGED] Confirmation email shows link as text instead of human readable text. !14243 (bitsapien)
+- [CHANGED] Return only group's members in user dropdowns on issuables list pages. !14249
+- [CHANGED] Added defaults for protected branches dropdowns on the repository settings. !14278
+- [CHANGED] Show confirmation modal before deleting account. !14360
+- [CHANGED] Allow creating merge requests across a fork network. !14422
+- [CHANGED] Re-arrange script HTML tags before template HTML tags in .vue files. !14671
+- [CHANGED] Create idea of read-only database. !14688
+- [CHANGED] Add active states to nav bar counters.
+- [CHANGED] Add view replaced file link for image diffs.
+- [CHANGED] Adjust tooltips to adhere to 8px grid and make them more readable.
+- [CHANGED] breadcrumbs receives padding when double lined.
+- [CHANGED] Allow developer role to admin milestones.
+- [CHANGED] Stop using Sidekiq for updating Key#last_used_at.
+- [CHANGED] Include GitLab full name in Slack messages.
+- [ADDED] Expose last pipeline details in API response when getting a single commit. !13521 (Mehdi Lahmam (@mehlah))
+- [ADDED] Allow to use same periods for different housekeeping tasks (effectively skipping the lesser task). !13711 (cernvcs)
+- [ADDED] Add GitLab-Pages version to Admin Dashboard. !14040 (travismiller)
+- [ADDED] Commenting on image diffs. !14061
+- [ADDED] Script to migrate project's repositories to new Hashed Storage. !14067
+- [ADDED] Hide close MR button after merge without reloading page. !14122 (Jacopo Beschi @jacopo-beschi)
+- [ADDED] Add Gitaly version to Admin Dashboard. !14313 (Jacopo Beschi @jacopo-beschi)
+- [ADDED] Add 'closed_at' attribute to Issues API. !14316 (Vitaliy @blackst0ne Klachkov)
+- [ADDED] Add tooltip for milestone due date to issue and merge request lists. !14318 (Vitaliy @blackst0ne Klachkov)
+- [ADDED] Improve list of sorting options. !14320 (Vitaliy @blackst0ne Klachkov)
+- [ADDED] Add client and call site metadata to Gitaly calls for better traceability. !14332
+- [ADDED] Strip gitlab-runner section markers in build trace HTML view. !14393
+- [ADDED] Add online view of HTML artifacts for public projects. !14399
+- [ADDED] Create Kubernetes cluster on GKE from k8s service. !14470
+- [ADDED] Add support for GPG subkeys in signature verification. !14517
+- [ADDED] Parse and store gitlab-runner timestamped section markers. !14551
+- [ADDED] Add "implements" to the default issue closing message regex. !14612 (Guilherme Vieira)
+- [ADDED] Replace `tag: true` into `:tag` in the specs. !14653 (Jacopo Beschi @jacopo-beschi)
+- [ADDED] Discussion lock for issues and merge requests.
+- [ADDED] Add an API endpoint to determine the forks of a project.
+- [ADDED] Add help text to runner edit: tags should be separated by commas. (Brendan O'Leary)
+- [ADDED] Only copy old/new code when selecting left/right side of parallel diff.
+- [ADDED] Expose avatar_url when requesting list of projects from API with simple=true.
+- [ADDED] A confirmation email is now sent when adding a secondary email address. (digitalmoksha)
+- [ADDED] Move Custom merge methods from EE.
+- [ADDED] Makes @mentions links have a different styling for better separation.
+- [ADDED] Added tabs to dashboard/projects to easily switch to personal projects.
+- [OTHER] Extract AutocompleteController#users into finder. !13778 (Maxim Rydkin, Mayra Cabrera)
+- [OTHER] Replace 'project/wiki.feature' spinach test with an rspec analog. !13856 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Expand docs for changing username or group path. !13914
+- [OTHER] Move `lib/ci` to `lib/gitlab/ci`. !14078 (Maxim Rydkin)
+- [OTHER] Decrease Cyclomatic Complexity threshold to 13. !14152 (Maxim Rydkin)
+- [OTHER] Decrease Perceived Complexity threshold to 15. !14160 (Maxim Rydkin)
+- [OTHER] Replace project/group_links.feature spinach test with an rspec analog. !14169 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Replace the project/milestone.feature spinach test with an rspec analog. !14171 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Replace the profile/emails.feature spinach test with an rspec analog. !14172 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Replace the project/team_management.feature spinach test with an rspec analog. !14173 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Replace the 'project/merge_requests/accept.feature' spinach test with an rspec analog. !14176 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Replace the 'project/builds/summary.feature' spinach test with an rspec analog. !14177 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Optimize the boards' issues fetching. !14198
+- [OTHER] Replace the 'project/merge_requests/revert.feature' spinach test with an rspec analog. !14201 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Replace the 'project/issues/award_emoji.feature' spinach test with an rspec analog. !14202 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Replace the 'profile/active_tab.feature' spinach test with an rspec analog. !14239 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Replace the 'search.feature' spinach test with an rspec analog. !14248 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Load sidebar participants avatars only when visible. !14270
+- [OTHER] Adds gitlab features and components to usage ping data. !14305
+- [OTHER] Replace the 'project/archived.feature' spinach test with an rspec analog. !14322 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Replace the 'project/commits/revert.feature' spinach test with an rspec analog. !14325 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Replace the 'project/snippets.feature' spinach test with an rspec analog. !14326 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Add link to OpenID Connect documentation. !14368 (Markus Koller)
+- [OTHER] Upgrade doorkeeper-openid_connect. !14372 (Markus Koller)
+- [OTHER] Upgrade gitlab-markup gem. !14395 (Markus Koller)
+- [OTHER] Index projects on repository storage. !14414
+- [OTHER] Replace the 'project/shortcuts.feature' spinach test with an rspec analog. !14431 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Replace the 'project/service.feature' spinach test with an rspec analog. !14432 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Improve GitHub import performance. !14445
+- [OTHER] Add basic sprintf implementation to JavaScript. !14506
+- [OTHER] Replace the 'project/merge_requests.feature' spinach test with an rspec analog. !14621 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Update GitLab Pages to v0.6.0. !14630
+- [OTHER] Add documentation to summarise project archiving. !14650
+- [OTHER] Remove 'Repo' prefix from API entites. !14694 (Vitaliy @blackst0ne Klachkov)
+- [OTHER] Removes cycle analytics service and store from global namespace.
+- [OTHER] Improves i18n for Auto Devops callout.
+- [OTHER] Exports common_utils utility functions as modules.
+- [OTHER] Use `simple=true` for projects API in Projects dropdown for better search performance.
+- [OTHER] Change index on ci_builds to optimize Jobs Controller.
+- [OTHER] Add index for merge_requests.merge_commit_sha.
+- [OTHER] Add (partial) index on Labels.template.
+- [OTHER] Cache issue and MR template names in Redis.
+- [OTHER] changed dashed border button color to be darker.
+- [OTHER] Speed up permission checks.
+- [OTHER] Fix docs for lightweight tag creation via API.
+- [OTHER] Clarify artifact download via the API only accepts branch or tag name for ref.
+- [OTHER] Change recommended MySQL version to 5.6.
+- [OTHER] Bump google-api-client Gem from 0.8.6 to 0.13.6.
+- [OTHER] Detect when changelog entries are invalid.
+- [OTHER] Use a UNION ALL for getting merge request notes.
+- [OTHER] Remove an index on ci_builds meant to be only temporary.
+- [OTHER] Remove a SQL query from the todos index page.
+- Support custom attributes on users. !13038 (Markus Koller)
+- made read-only APIs for public merge requests available without authentication. !13291 (haseebeqx)
+- Hide read_registry scope when registry is disabled on instance. !13314 (Robin Bobbitt)
+- creation of keys moved to services. !13331 (haseebeqx)
+- Add username as GL_USERNAME in hooks.
+
+## 10.0.5 (2017-11-03)
+
+- [FIXED] Fix incorrect X-axis labels in Prometheus graphs. !14258
+- [FIXED] Fix `rake gitlab:incoming_email:check` and make it report the actual error. !14423
+- [FIXED] Does not check if an invariant hashed storage path exists on disk when renaming projects. !14428
+- [FIXED] Fix bottom spacing for dropdowns that open upwards. !14535
+- [FIXED] Fix the project import with issues and milestones. !14657
+- [FIXED] Fix broken Y-axis scaling in some Prometheus graphs. !14693
+- [FIXED] Fixed duplicate notifications when added multiple labels on an issue. !14798
+- [FIXED] Don't rename paths that were freed up when upgrading. !15029
+- [FIXED] Fixed issue/merge request breadcrumb titles not having links.
+- [FIXED] Fix application setting to cache nil object.
+- [FIXED] Fix missing Import/Export issue assignees.
+- [FIXED] Allow boards as top level route.
+- [FIXED] Fixed milestone breadcrumb links.
+- [FIXED] Fixed merge request widget merged & closed date tooltip text.
+- [FIXED] fix merge request widget status icon for failed CI.
+
+## 10.0.4 (2017-10-16)
+
+- [SECURITY] Move project repositories between namespaces when renaming users.
+- [SECURITY] Prevent an open redirect on project pages.
+- [SECURITY] Prevent a persistent XSS in user-provided markup.
+
## 10.0.3 (2017-10-05)
- [FIXED] find_user Users helper method no longer overrides find_user API helper method. !14418
@@ -212,6 +700,14 @@ entry.
- Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi)
- [BUGIFX] Improves subgroup creation permissions. !13418
+## 9.5.9 (2017-10-16)
+
+- [SECURITY] Move project repositories between namespaces when renaming users.
+- [SECURITY] Prevent an open redirect on project pages.
+- [SECURITY] Prevent a persistent XSS in user-provided markup.
+- [FIXED] Allow using newlines in pipeline email service recipients. !14250
+- Escape user name in filtered search bar.
+
## 9.5.8 (2017-10-04)
- [FIXED] Fixed fork button being disabled for users who can fork to a group.
@@ -457,6 +953,15 @@ entry.
- Use a specialized class for querying events to improve performance.
- Update build badges to be pipeline badges and display passing instead of success.
+## 9.4.7 (2017-10-16)
+
+- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
+- [SECURITY] Move project repositories between namespaces when renaming users.
+- [SECURITY] Prevent an open redirect on project pages.
+- [SECURITY] Prevent a persistent XSS in user-provided markup.
+- [FIXED] Allow using newlines in pipeline email service recipients. !14250
+- Escape user name in filtered search bar.
+
## 9.4.6 (2017-09-06)
- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6b261bd5938..4930b541ba2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,9 +1,13 @@
-## Contributor license agreement
+## Developer Certificate of Origin + License
-By submitting code as an individual you agree to the
-[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md).
-By submitting code as an entity you agree to the
-[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
+By contributing to GitLab B.V., You accept and agree to the following terms and
+conditions for Your present and future Contributions submitted to GitLab B.V.
+Except for the license granted herein to GitLab B.V. and recipients of software
+distributed by GitLab B.V., You reserve all right, title, and interest in and to
+Your Contributions. All Contributions are subject to the following DCO + License
+terms.
+
+[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
_This notice should stay as the first item in the CONTRIBUTING.md file._
@@ -21,10 +25,10 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc)
+ - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
-- [Implement design & UI elements](#implement-design--ui-elements)
+- [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker)
- [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals)
@@ -100,8 +104,7 @@ the remaining issues on the GitHub issue tracker.
## I want to contribute!
-If you want to contribute to GitLab, but are not sure where to start,
-look for [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight].
+If you want to contribute to GitLab, [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight] is a great place to start. Issues with a lower weight (1 or 2) are deemed suitable for beginners.
These issues will be of reasonable size and challenge, for anyone to start
contributing to GitLab.
@@ -296,9 +299,9 @@ might be edited to make them small and simple.
Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker.
-For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
-be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
-need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself.
+For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
+be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
+need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself.
If you want to create something yourself, consider opening an issue first to
discuss whether it is interesting to include this in GitLab.
@@ -540,6 +543,7 @@ When having your code reviewed and when reviewing merge requests please take the
etc.), they should conform to our [Licensing guidelines][license-finder-doc].
See the instructions in that document for help if your MR fails the
"license-finder" test with a "Dependencies that need approval" error.
+1. The merge request meets the [definition of done](#definition-of-done).
## Definition of done
@@ -658,7 +662,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[license-finder-doc]: doc/development/licensing.md
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html
-[testing]: doc/development/testing.md
+[testing]: doc/development/testing_guide/index.md
[^1]: Please note that specs other than JavaScript specs are considered backend
code.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 8298bb08b2d..316ba4bd9e6 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.43.0
+0.55.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 5508e17c23b..c5b7013b9c5 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1,2 +1 @@
-5.9.3
-
+5.9.4
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 944880fa15e..bea438e9ade 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-3.2.0
+3.3.1
diff --git a/Gemfile b/Gemfile
index 5645d1ea4ff..b6580c28eb7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -90,7 +90,7 @@ gem 'kaminari', '~> 1.0'
gem 'hamlit', '~> 2.6.1'
# Files attachments
-gem 'carrierwave', '~> 1.1'
+gem 'carrierwave', '~> 1.2'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
@@ -102,7 +102,7 @@ gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1'
-gem 'fog-aliyun', '~> 0.1.0'
+gem 'fog-aliyun', '~> 0.2.0'
# for Google storage
gem 'google-api-client', '~> 0.13.6'
@@ -111,7 +111,7 @@ gem 'google-api-client', '~> 0.13.6'
gem 'unf', '~> 0.1.4'
# Seed data
-gem 'seed-fu', '~> 2.3.5'
+gem 'seed-fu', '~> 2.3.7'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
@@ -245,7 +245,7 @@ gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.3'
gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2'
-gem 'jquery-rails', '~> 4.1.0'
+gem 'jquery-rails', '~> 4.3.1'
gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
@@ -263,6 +263,8 @@ gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development
+gem 'batch-loader'
+
# Perf bar
gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2'
@@ -281,7 +283,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
- gem 'prometheus-client-mmap', '~>0.7.0.beta14'
+ gem 'prometheus-client-mmap', '~> 0.7.0.beta39'
gem 'raindrops', '~> 0.18'
end
@@ -324,9 +326,9 @@ group :development, :test do
# Generate Fake data
gem 'ffaker', '~> 2.4'
- gem 'capybara', '~> 2.15.0'
+ gem 'capybara', '~> 2.15'
gem 'capybara-screenshot', '~> 1.0.0'
- gem 'poltergeist', '~> 1.9.0'
+ gem 'selenium-webdriver', '~> 3.5'
gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
@@ -343,7 +345,7 @@ group :development, :test do
gem 'benchmark-ips', '~> 2.3.0', require: false
- gem 'license_finder', '~> 2.1.0', require: false
+ gem 'license_finder', '~> 3.1', require: false
gem 'knapsack', '~> 1.11.0'
gem 'activerecord_sane_schema_dumper', '0.2'
@@ -398,7 +400,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.39.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.54.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 6040596dda5..7375fce8b1e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -73,6 +73,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
base32 (0.3.2)
+ batch-loader (1.1.1)
bcrypt (3.1.11)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
@@ -83,6 +84,7 @@ GEM
bindata (2.4.1)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
+ blankslate (2.1.2.4)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
@@ -107,18 +109,19 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
- carrierwave (1.1.0)
+ carrierwave (1.2.1)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
cause (0.1)
charlock_holmes (0.7.5)
+ childprocess (0.7.0)
+ ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2)
chronic_duration (0.10.6)
numerizer (~> 0.1.1)
chunky_png (1.3.5)
citrus (3.0.2)
- cliver (0.3.2)
coderay (1.1.1)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
@@ -214,7 +217,7 @@ GEM
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
- fog-aliyun (0.1.0)
+ fog-aliyun (0.2.0)
fog-core (~> 1.27)
fog-json (~> 1.0)
ipaddress (~> 0.8)
@@ -273,7 +276,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.39.0)
+ gitaly-proto (0.54.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -291,7 +294,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16)
posix-spawn (~> 0.3)
- gitlab-markup (1.6.2)
+ gitlab-markup (1.6.3)
gitlab_omniauth-ldap (2.0.4)
net-ldap (~> 0.16)
omniauth (~> 1.3)
@@ -324,7 +327,9 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
- google-protobuf (3.4.0.2)
+ google-protobuf (3.4.1.1)
+ googleapis-common-protos-types (1.0.0)
+ google-protobuf (~> 3.0)
googleauth (0.5.3)
faraday (~> 0.12)
jwt (~> 1.4)
@@ -351,9 +356,10 @@ GEM
rake
grape_logging (1.7.0)
grape
- grpc (1.6.0)
+ grpc (1.7.2)
google-protobuf (~> 3.1)
- googleauth (~> 0.5.1)
+ googleapis-common-protos-types (~> 1.0.0)
+ googleauth (>= 0.5.1, < 0.7)
haml (4.0.7)
tilt
haml_lint (0.26.0)
@@ -405,7 +411,7 @@ GEM
multipart-post
oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2)
- jquery-rails (4.1.1)
+ jquery-rails (4.3.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
@@ -447,11 +453,13 @@ GEM
actionmailer (>= 3.2)
letter_opener (~> 1.0)
railties (>= 3.2)
- license_finder (2.1.0)
+ license_finder (3.1.1)
bundler
httparty
rubyzip
thor
+ toml (= 0.1.2)
+ with_env (> 1.0)
xml-simple
licensee (8.7.0)
rugged (~> 0.24)
@@ -480,7 +488,7 @@ GEM
mini_mime (0.1.4)
mini_portile2 (2.3.0)
minitest (5.7.0)
- mmap2 (2.2.7)
+ mmap2 (2.2.9)
mousetrap-rails (1.4.6)
multi_json (1.12.2)
multi_xml (0.6.0)
@@ -567,6 +575,8 @@ GEM
activerecord (>= 4.0, < 5.2)
parser (2.4.0.0)
ast (~> 2.2)
+ parslet (1.5.0)
+ blankslate (~> 2.0)
path_expander (1.0.1)
peek (1.0.1)
concurrent-ruby (>= 0.9.0)
@@ -601,11 +611,6 @@ GEM
pg (0.18.4)
po_to_json (1.0.1)
json (>= 1.6.0)
- poltergeist (1.9.0)
- capybara (~> 2.1)
- cliver (~> 0.3.1)
- multi_json (~> 1.0)
- websocket-driver (>= 0.2.0)
posix-spawn (0.3.13)
powerpack (0.1.1)
premailer (1.10.4)
@@ -620,8 +625,8 @@ GEM
parser
unparser
procto (0.0.3)
- prometheus-client-mmap (0.7.0.beta14)
- mmap2 (~> 2.2, >= 2.2.7)
+ prometheus-client-mmap (0.7.0.beta39)
+ mmap2 (~> 2.2, >= 2.2.9)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@@ -810,11 +815,14 @@ GEM
rake (>= 0.9, < 13)
sass (~> 3.4.20)
securecompare (1.0.0)
- seed-fu (2.3.6)
+ seed-fu (2.3.7)
activerecord (>= 3.1)
activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
+ selenium-webdriver (3.5.0)
+ childprocess (~> 0.5)
+ rubyzip (~> 1.0)
sentry-raven (2.5.3)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
@@ -896,6 +904,8 @@ GEM
tilt (2.0.6)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
+ toml (0.1.2)
+ parslet (~> 1.5.0)
toml-rb (0.3.15)
citrus (~> 3.0, > 3.0)
truncato (0.7.10)
@@ -946,13 +956,11 @@ GEM
hashdiff
webpack-rails (0.9.10)
railties (>= 3.2.0)
- websocket-driver (0.6.3)
- websocket-extensions (>= 0.1.0)
- websocket-extensions (0.1.2)
wikicloth (0.8.1)
builder
expression_parser
rinku
+ with_env (1.1.0)
xml-simple (1.1.5)
xpath (2.1.0)
nokogiri (~> 1.3)
@@ -975,6 +983,7 @@ DEPENDENCIES
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
+ batch-loader
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
@@ -985,9 +994,9 @@ DEPENDENCIES
browser (~> 2.2)
bullet (~> 5.5.0)
bundler-audit (~> 0.5.0)
- capybara (~> 2.15.0)
+ capybara (~> 2.15)
capybara-screenshot (~> 1.0.0)
- carrierwave (~> 1.1)
+ carrierwave (~> 1.2)
charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
@@ -1012,7 +1021,7 @@ DEPENDENCIES
flay (~> 2.8.0)
flipper (~> 0.10.2)
flipper-active_record (~> 0.10.2)
- fog-aliyun (~> 0.1.0)
+ fog-aliyun (~> 0.2.0)
fog-aws (~> 1.4)
fog-core (~> 1.44)
fog-google (~> 0.5)
@@ -1027,7 +1036,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly-proto (~> 0.39.0)
+ gitaly-proto (~> 0.54.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
@@ -1052,14 +1061,14 @@ DEPENDENCIES
influxdb (~> 0.2)
jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2)
- jquery-rails (~> 4.1.0)
+ jquery-rails (~> 4.3.1)
json-schema (~> 2.8.0)
jwt (~> 1.5.6)
kaminari (~> 1.0)
knapsack (~> 1.11.0)
kubeclient (~> 2.2.0)
letter_opener_web (~> 1.3.0)
- license_finder (~> 2.1.0)
+ license_finder (~> 3.1)
licensee (~> 8.7.0)
lograge (~> 0.5)
loofah (~> 2.0.3)
@@ -1101,9 +1110,8 @@ DEPENDENCIES
peek-redis (~> 1.2.0)
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
- poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.7.0.beta14)
+ prometheus-client-mmap (~> 0.7.0.beta39)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
@@ -1145,8 +1153,9 @@ DEPENDENCIES
sanitize (~> 2.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.54.0)
- seed-fu (~> 2.3.5)
+ seed-fu (~> 2.3.7)
select2-rails (~> 3.5.9)
+ selenium-webdriver (~> 3.5)
sentry-raven (~> 2.5.3)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
@@ -1186,4 +1195,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.15.4
+ 1.16.0
diff --git a/MAINTENANCE.md b/MAINTENANCE.md
index 1efb2a35f6d..5cf9fee1a14 100644
--- a/MAINTENANCE.md
+++ b/MAINTENANCE.md
@@ -1,35 +1,3 @@
# GitLab Maintenance Policy
-GitLab follows the [Semantic Versioning](http://semver.org/) for its releases:
-`(Major).(Minor).(Patch)` in a [pragmatic way].
-
-- **Major version**: Whenever there is something significant or any backwards
- incompatible changes are introduced to the public API.
-- **Minor version**: When new, backwards compatible functionality is introduced
- to the public API or a minor feature is introduced, or when a set of smaller
- features is rolled out.
-- **Patch number**: When backwards compatible bug fixes are introduced that fix
- incorrect behavior.
-
-The current stable release will receive security patches and bug fixes
-(eg. `8.9.0` -> `8.9.1`). Feature releases will mark the next supported stable
-release where the minor version is increased numerically by increments of one
-(eg. `8.9 -> 8.10`).
-
-Our current policy is to support one stable release at any given time, but for
-medium-level security issues, we may consider [backporting to the previous two
-monthly releases][rel-sec].
-
-We encourage everyone to run the latest stable release to ensure that you can
-easily upgrade to the most secure and feature-rich GitLab experience. In order
-to make sure you can easily run the most recent stable release, we are working
-hard to keep the update process simple and reliable.
-
-More information about the release procedures can be found in our
-[release-tools documentation][rel]. You may also want to read our
-[Responsible Disclosure Policy][disclosure].
-
-[rel-sec]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/security.md#backporting
-[rel]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/
-[disclosure]: https://about.gitlab.com/disclosure/
-[pragmatic way]: https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e
+See [doc/policy/maintenance.md](doc/policy/maintenance.md)
diff --git a/PROCESS.md b/PROCESS.md
index 5e65bb59246..7c8db689256 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -1,4 +1,4 @@
-## GitLab Core Team & GitLab Inc. Contribution Process
+## GitLab core team & GitLab Inc. contribution process
---
@@ -141,21 +141,29 @@ the stable branch are:
* Fixes for security issues
* New or updated translations (as long as they do not touch application code)
-During the feature freeze all merge requests that are meant to go into the upcoming
-release should have the correct milestone assigned _and_ have the label
-~"Pick into Stable" set, so that release managers can find and pick them.
-Merge requests without a milestone and this label will
-not be merged into any stable branches.
-
-Fixes marked like this will be shipped in the next RC for that release. Once
-the final RC has been prepared ready for release on the 22nd, further fixes
-marked ~"Pick into Stable" will go into a patch for that release.
-
-If a merge request is to be picked into more than one release it will also need
-the ~"Pick into Backports" label set to remind the release manager to change
-the milestone after cherry-picking. As before, it should still have the
-~"Pick into Stable" label and the milestone of the highest release it will be
-picked into.
+During the feature freeze all merge requests that are meant to go into the
+upcoming release should have the correct milestone assigned _and_ the
+`Pick into X.Y` label where `X.Y` is equal to the milestone, so that release
+managers can find and pick them.
+Merge requests without this label will not be picked into the stable release.
+
+For example, if the upcoming release is `10.2.0` you will need to set the
+`Pick into 10.2` label.
+
+Fixes marked like this will be shipped in the next RC (before the 22nd), or the
+next patch release.
+
+If a merge request is to be picked into more than one release it will need one
+`Pick into X.Y` label per release where the merge request should be back-ported
+to.
+
+For example, if the current patch release is `10.1.1` and a regression fix needs
+to be backported down to the `9.5` release, you will need to assign it the
+`10.1` milestone and the following labels:
+
+- `Pick into 10.1`
+- `Pick into 10.0`
+- `Pick into 9.5`
### Asking for an exception
diff --git a/VERSION b/VERSION
index 22dd5aa0686..73cdb768a24 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-10.1.0-pre
+10.3.0-pre
diff --git a/app/assets/images/auth_buttons/signin_with_google.png b/app/assets/images/auth_buttons/signin_with_google.png
index b1327b4f7b4..f27bb243304 100644
--- a/app/assets/images/auth_buttons/signin_with_google.png
+++ b/app/assets/images/auth_buttons/signin_with_google.png
Binary files differ
diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png
index 5dcd9c09b70..723c2c3f4c8 100644
--- a/app/assets/images/emoji.png
+++ b/app/assets/images/emoji.png
Binary files differ
diff --git a/app/assets/images/emoji/gay_pride_flag.png b/app/assets/images/emoji/gay_pride_flag.png
new file mode 100644
index 00000000000..1bec5f2ffd7
--- /dev/null
+++ b/app/assets/images/emoji/gay_pride_flag.png
Binary files differ
diff --git a/app/assets/images/emoji/mrs_claus.png b/app/assets/images/emoji/mrs_claus.png
index 078f0657f95..9cf2458df1a 100644
--- a/app/assets/images/emoji/mrs_claus.png
+++ b/app/assets/images/emoji/mrs_claus.png
Binary files differ
diff --git a/app/assets/images/emoji/speech_left.png b/app/assets/images/emoji/speech_left.png
new file mode 100644
index 00000000000..00c05959bcd
--- /dev/null
+++ b/app/assets/images/emoji/speech_left.png
Binary files differ
diff --git a/app/assets/images/emoji@2x.png b/app/assets/images/emoji@2x.png
index b0fa9e1139e..987279c13cc 100644
--- a/app/assets/images/emoji@2x.png
+++ b/app/assets/images/emoji@2x.png
Binary files differ
diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json
index 6b8f85e37fd..6befc551263 100644
--- a/app/assets/images/icons.json
+++ b/app/assets/images/icons.json
@@ -1 +1 @@
-{"iconCount":135,"spriteSize":58718,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","comment-dots","comment-next","comment","comments","commit","credit-card","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","stop","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} \ No newline at end of file
+{"iconCount":179,"spriteSize":81882,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} \ No newline at end of file
diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg
index 30cb2109ec2..74e1c8c22f6 100644
--- a/app/assets/images/icons.svg
+++ b/app/assets/images/icons.svg
@@ -1 +1 @@
-<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.535 7.95L1.292 3.707a1 1 0 0 1-.281-.695 1 1 0 1 1 1.695-.719l4.95 4.95a.998.998 0 0 1 0 1.414l-4.95 4.95a1.002 1.002 0 0 1-.707.293c-.549 0-1-.452-1-1 0-.266.106-.52.293-.708L5.535 7.95zm7 0L8.292 3.707a1 1 0 0 1-.281-.695 1 1 0 1 1 1.695-.719l4.95 4.95a.998.998 0 0 1 0 1.414l-4.95 4.95a1.002 1.002 0 0 1-.707.293c-.549 0-1-.452-1-1.001 0-.265.106-.519.293-.707l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 15V1a1 1 0 0 1 1-1h4.604c.93 0 1.762.088 2.495.264.733.176 1.353.445 1.863.807.509.363.897.82 1.164 1.369.268.549.401 1.197.401 1.945 0 .366-.045.718-.137 1.055-.091.337-.23.652-.417.945a3.453 3.453 0 0 1-.71.796 3.645 3.645 0 0 1-1.021.588c.469.117.87.295 1.203.533.333.238.608.515.824.83.216.315.374.657.473 1.027.099.37.148.75.148 1.138 0 1.553-.5 2.725-1.5 3.516-1 .791-2.423 1.187-4.27 1.187H3a1 1 0 0 1-1-1zm3.297-5.967v4.319H8.12c.425 0 .791-.053 1.099-.16.307-.106.564-.252.769-.44.205-.186.357-.406.456-.659.099-.252.148-.529.148-.83a3.04 3.04 0 0 0-.131-.928 1.78 1.78 0 0 0-.413-.703 1.8 1.8 0 0 0-.73-.445c-.3-.103-.66-.154-1.077-.154H5.297zm0-2.33h2.44c.842-.014 1.468-.192 1.878-.533.41-.34.616-.826.616-1.456 0-.725-.21-1.247-.632-1.566-.421-.318-1.086-.478-1.995-.478H5.297v4.033z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-additions" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.009 6.958a4 4 0 0 0 5.283 4.775 1 1 0 0 1 .712 1.87A6 6 0 0 1 2.077 6.44l-.741-.2a.5.5 0 0 1-.12-.915L3.41 4.058a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.711-1.87 6 6 0 0 1 7.927 7.162l.74.2a.5.5 0 0 1 .121.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="talic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 0h7a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm2 2h3L8 14H5L8 2zM3 14h7a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thump-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 1600 1600" id="ellipsis_v" xmlns="http://www.w3.org/2000/svg"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></symbol><symbol viewBox="0 0 18 18" id="emoji_slightly_smiling_face" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445 2.91 2.91 0 0 0 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smile" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smiley" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z"/></symbol><symbol viewBox="0 0 16 16" id="epic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.985 8.044l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637A2 2 0 0 0 1.618 9h11.661a2 2 0 0 0 1.706-.956zm0 3l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637a2 2 0 0 0 .576.084h11.661a2 2 0 0 0 1.706-.956zM3.618 2h10.995a1 1 0 0 1 .948 1.316l-1.333 4a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l2-4A1 1 0 0 1 3.618 2zm-.382 4h9.322l.667-2H4.236l-1 2z"/></symbol><symbol viewBox="0 0 16 16" id="external-link" xmlns="http://www.w3.org/2000/svg"><path d="M13.121 4.177l-4.95 4.95a1 1 0 1 1-1.414-1.414l4.95-4.95-1.386-1.386a.5.5 0 0 1 .299-.85l4.709-.524a.5.5 0 0 1 .552.552l-.523 4.71a.5.5 0 0 1-.851.297l-1.386-1.385zM12 8.884a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-addition" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="hourglass" xmlns="http://www.w3.org/2000/svg"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></symbol><symbol viewBox="0 0 24 30" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><title>cursor_active</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#FFF" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#1F78D1"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787a6.92 6.92 0 0 0-2.558.469c-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068a.19.19 0 0 1 .033-.067.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26a2.57 2.57 0 0 0 .221-.342c.054-.103.114-.235.181-.395a4.18 4.18 0 0 0 .174-.51c-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#FFF" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="italic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.5 12l2-8H6a1 1 0 1 1 0-2h6a1 1 0 0 1 0 2h-1.5l-2 8H10a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2h1.5z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pencil-square" xmlns="http://www.w3.org/2000/svg"><path d="M12 9a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V9zm.778-7.179l1.414 1.415-6.476 6.476a1 1 0 0 1-.498.27l-1.51.325.323-1.512a1 1 0 0 1 .27-.497l6.477-6.477zM15.607.407a1 1 0 0 1 0 1.414l-.708.707-1.414-1.414.707-.707a1 1 0 0 1 1.415 0z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.114 6.958a4 4 0 0 0 5.283 4.775 1 1 0 1 1 .712 1.87A6 6 0 0 1 2.182 6.44l-.741-.2a.5.5 0 0 1-.12-.915l2.195-1.268a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.712-1.87 6 6 0 0 1 7.927 7.162l.742.2a.5.5 0 0 1 .12.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="eufirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="eusecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="euthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/clusters_empty.svg b/app/assets/images/illustrations/clusters_empty.svg
new file mode 100644
index 00000000000..c13228638be
--- /dev/null
+++ b/app/assets/images/illustrations/clusters_empty.svg
@@ -0,0 +1 @@
+<svg height="128" viewBox="0 0 142 128" width="142" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M94 62h20v4H94z" fill="#f0edf8"/><path d="M84.828 84l17.678 17.678-2.828 2.828L82 86.828z" fill="#fee1d3"/><path d="M42.828 24l17.678 17.678-2.828 2.828L40 26.828zM40 101.678L57.678 84l2.828 2.828-17.678 17.678z" fill="#f0edf8"/><g fill="#fee1d3"><path d="M82 41.678L99.678 24l2.828 2.828-17.678 17.678zM28 62h20v4H28z"/><rect height="30" rx="5" width="30" y="49"/></g><rect height="26" rx="5" stroke="#fdc4a8" stroke-width="4" width="26" x="2" y="51"/><rect fill="#c3b8e3" height="50" rx="10" width="50" x="46" y="39"/><rect height="46" rx="10" stroke="#6b4fbb" stroke-width="4" width="46" x="48" y="41"/><rect fill="#fef0e8" height="30" rx="5" width="30" x="84"/><rect height="26" rx="5" stroke="#fee1d3" stroke-width="4" width="26" x="86" y="2"/><rect fill="#fee1d3" height="30" rx="5" width="30" x="84" y="98"/><rect height="26" rx="5" stroke="#fdc4a8" stroke-width="4" width="26" x="86" y="100"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="112" y="49"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="114" y="51"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="28" y="98"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="30" y="100"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="28"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="30" y="2"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/convdev_no_data.svg b/app/assets/images/illustrations/convdev/convdev_no_data.svg
new file mode 100644
index 00000000000..b90eddcccfa
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/convdev_no_data.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="360" height="220" viewBox="0 0 360 220"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".02" d="M125 44V24.003C125 18.48 129.483 14 135.005 14h89.99C230.52 14 235 18.477 235 24.003V43h84.992C326.624 43 332 48.372 332 55.002v144.996c0 6.63-5.38 12.002-12.008 12.002h-85.984c-6.632 0-12.008-5.372-12.008-12.002V183h-78v17.002c0 6.626-5.38 11.998-12.008 11.998H46.008C39.376 212 34 206.624 34 200.002V55.998C34 49.372 39.38 44 46.008 44H125z"/><g transform="translate(214 36)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77a2 2 0 0 0-2.773-2.883A11.974 11.974 0 0 0 0 12.006a2 2 0 1 0 4 0zM14.388 4h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm17.51.227a8.015 8.015 0 0 1 5.022 3.756 2 2 0 1 0 3.458-2.011A12.01 12.01 0 0 0 104.844.34a2 2 0 0 0-.946 3.887zM110 16.78v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm-.024 17.844a7.99 7.99 0 0 1-2.903 5.558 2 2 0 0 0 2.54 3.09 11.977 11.977 0 0 0 4.35-8.338 2.002 2.002 0 0 0-1.838-2.15 2.003 2.003 0 0 0-2.15 1.84zM98.826 168h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-17.334-.4a8.032 8.032 0 0 1-4.71-4.143 1.998 1.998 0 0 0-2.667-.938 1.997 1.997 0 0 0-.938 2.667 12.022 12.022 0 0 0 7.063 6.21 1.998 1.998 0 1 0 1.252-3.798zM4 154.434v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0z"/><path fill="#F0EDF8" fill-rule="nonzero" d="M57 111c-11.598 0-21-9.402-21-21s9.402-21 21-21 21 9.402 21 21-9.402 21-21 21zm0-4c9.39 0 17-7.61 17-17s-7.61-17-17-17-17 7.61-17 17 7.61 17 17 17z"/><path fill="#6B4FBB" d="M58 88v-6.997c0-1.11-.895-2.003-2-2.003-1.112 0-2 .897-2 2.003v8.994a1.999 1.999 0 0 0 2.503 1.94c.162.04.33.063.506.063h7.98a2 2 0 0 0 .001-4H58z"/><rect width="8" height="4" x="8" y="14" fill="#EEE" rx="2"/><path fill="#EEE" d="M21 16c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 21 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 34 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 47 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 60 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 73 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 86 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 99 16z"/></g><g transform="translate(118 7)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77a2 2 0 0 0-2.773-2.883A11.974 11.974 0 0 0 0 12.006a2 2 0 1 0 4 0zM14.388 4h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm17.51.227a8.015 8.015 0 0 1 5.022 3.756 2 2 0 1 0 3.458-2.011A12.01 12.01 0 0 0 104.844.34a2 2 0 0 0-.946 3.887zM110 16.78v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm-.024 17.844a7.99 7.99 0 0 1-2.903 5.558 2 2 0 0 0 2.54 3.09 11.977 11.977 0 0 0 4.35-8.338 2.002 2.002 0 0 0-1.838-2.15 2.003 2.003 0 0 0-2.15 1.84zM98.826 168h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-17.334-.4a8.032 8.032 0 0 1-4.71-4.143 1.998 1.998 0 0 0-2.667-.938 1.997 1.997 0 0 0-.938 2.667 12.022 12.022 0 0 0 7.063 6.21 1.998 1.998 0 1 0 1.252-3.798zM4 154.434v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0z"/><g fill-rule="nonzero"><path fill="#F0EDF8" d="M57 112c-12.15 0-22-9.85-22-22s9.85-22 22-22 22 9.85 22 22-9.85 22-22 22zm0-6c8.837 0 16-7.163 16-16s-7.163-16-16-16-16 7.163-16 16 7.163 16 16 16z"/><path fill="#6B4FBB" d="M41.692 105.8A21.93 21.93 0 0 0 57 112c12.15 0 22-9.85 22-22s-9.85-22-22-22v6c8.837 0 16 7.163 16 16s-7.163 16-16 16a15.935 15.935 0 0 1-11.133-4.508l-4.175 4.31z"/></g><path fill="#EEE" d="M8 16c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2H9.998A1.995 1.995 0 0 1 8 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 21 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 34 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 47 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 60 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 73 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 86 16zm13 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004A1.995 1.995 0 0 1 99 16z"/></g><g transform="translate(26 36)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006v147.988A8 8 0 0 0 12.005 168h89.99a8.007 8.007 0 0 0 8.005-8.006V12.006A8 8 0 0 0 101.995 4h-89.99A8.007 8.007 0 0 0 4 12.006zm-4 0C0 5.376 5.377 0 12.005 0h89.99C108.628 0 114 5.37 114 12.006v147.988c0 6.63-5.377 12.006-12.005 12.006h-89.99C5.372 172 0 166.63 0 159.994V12.006z"/><g transform="translate(21 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/></g><g transform="translate(69 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/></g><g transform="translate(38 42)"><rect width="22" height="4" x="8" fill="#FEE1D3" rx="2"/><rect width="38" height="4" y="12" fill="#FB722E" rx="2"/></g><path fill="#EEE" d="M4 14h106v4H4z"/><path fill="#333" d="M35.724 138h9.696v-2.856h-2.856V122.76h-2.592c-1.08.648-2.136 1.08-3.792 1.392v2.184h2.856v8.808h-3.312V138zm17.736.288c-2.952 0-5.76-2.208-5.76-7.56 0-5.688 2.952-8.256 6.168-8.256 2.016 0 3.48.84 4.44 1.824l-1.848 2.112c-.528-.576-1.488-1.08-2.376-1.08-1.68 0-3.024 1.2-3.144 4.752.792-1.008 2.112-1.608 3.048-1.608 2.616 0 4.536 1.488 4.536 4.704 0 3.168-2.304 5.112-5.064 5.112zm-.072-2.64c1.056 0 1.92-.744 1.92-2.472 0-1.608-.84-2.208-1.992-2.208-.792 0-1.68.432-2.304 1.512.312 2.4 1.32 3.168 2.376 3.168zM63.9 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/convdev_no_index.svg b/app/assets/images/illustrations/convdev/convdev_no_index.svg
new file mode 100644
index 00000000000..4aaf505e0b8
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/convdev_no_index.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="360" height="200" viewBox="0 0 360 200"><g fill="none" fill-rule="evenodd" transform="translate(3 11)"><rect width="110" height="168" x="6" y="8" fill="#000" fill-opacity=".02" rx="10"/><g transform="translate(0 2)"><rect width="110" height="168" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M2 10.006v147.988A8 8 0 0 0 10.005 166h89.99a8.007 8.007 0 0 0 8.005-8.006V10.006A8 8 0 0 0 99.995 2h-89.99A8.007 8.007 0 0 0 2 10.006zm-4 0C-2 3.376 3.377-2 10.005-2h89.99C106.628-2 112 3.37 112 10.006v147.988c0 6.63-5.377 12.006-12.005 12.006h-89.99C3.372 170-2 164.63-2 157.994V10.006z"/><g transform="translate(19 80)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/></g><g transform="translate(67 80)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#6B4FBB" rx="2"/></g><g transform="translate(36 40)"><rect width="22" height="4" x="8" fill="#FEE1D3" rx="2"/><rect width="38" height="4" y="12" fill="#FB722E" rx="2"/></g><path fill="#EEE" d="M2 12h106v4H2z"/><path fill="#333" d="M38.048 127.792c.792 0 1.68-.432 2.28-1.512-.312-2.4-1.296-3.168-2.376-3.168-1.032 0-1.92.744-1.92 2.472 0 1.608.864 2.208 2.016 2.208zm-.552 8.496c-2.016 0-3.504-.864-4.464-1.824l1.872-2.112c.504.576 1.464 1.08 2.352 1.08 1.704 0 3.024-1.2 3.144-4.752-.792 1.008-2.112 1.608-3.048 1.608-2.592 0-4.536-1.488-4.536-4.704 0-3.168 2.304-5.112 5.064-5.112 2.952 0 5.784 2.208 5.784 7.56 0 5.688-2.976 8.256-6.168 8.256zm13.488 0c-3.048 0-5.304-1.704-5.304-4.176 0-1.848 1.152-2.976 2.592-3.744v-.096c-1.176-.888-2.04-1.992-2.04-3.6 0-2.592 2.04-4.2 4.872-4.2 2.784 0 4.632 1.656 4.632 4.176 0 1.464-.936 2.64-1.992 3.336v.096c1.464.792 2.64 1.968 2.64 3.984 0 2.4-2.16 4.224-5.4 4.224zm.96-9.168c.6-.696.936-1.44.936-2.232 0-1.176-.696-1.968-1.848-1.968-.936 0-1.704.576-1.704 1.752 0 1.248 1.056 1.848 2.616 2.448zm-.888 6.72c1.176 0 2.04-.624 2.04-1.896 0-1.344-1.296-1.848-3.216-2.664-.672.624-1.176 1.488-1.176 2.424 0 1.344 1.08 2.136 2.352 2.136zm10.8-3.84c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/></g><g transform="translate(122)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77a2 2 0 0 0-2.773-2.883A11.974 11.974 0 0 0 0 12.006a2 2 0 1 0 4 0zM14.388 4h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm17.51.227a8.015 8.015 0 0 1 5.022 3.756 2 2 0 1 0 3.458-2.011A12.01 12.01 0 0 0 104.844.34a2 2 0 0 0-.946 3.887zM110 16.78v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm-.024 17.844a7.99 7.99 0 0 1-2.903 5.558 2 2 0 0 0 2.54 3.09 11.977 11.977 0 0 0 4.35-8.338 2.002 2.002 0 0 0-1.838-2.15 2.003 2.003 0 0 0-2.15 1.84zM98.826 168h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-17.334-.4a8.032 8.032 0 0 1-4.71-4.143 1.998 1.998 0 0 0-2.667-.938 1.997 1.997 0 0 0-.938 2.667 12.022 12.022 0 0 0 7.063 6.21 1.998 1.998 0 1 0 1.252-3.798zM4 154.434v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0z"/><g transform="translate(21 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/></g><g transform="translate(69 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/></g><path fill="#FEE1D3" d="M44 44a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 44 44zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 54 44zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 64 44zM34 56a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 34 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 44 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 54 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 64 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 74 56z"/><rect width="8" height="4" x="8" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="21" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="34" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="47" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="60" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="73" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="86" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="99" y="14" fill="#EEE" rx="2"/><path fill="#EEE" d="M46.716 138.288c-3.264 0-5.448-2.784-5.448-7.968s2.184-7.848 5.448-7.848c3.264 0 5.448 2.664 5.448 7.848 0 5.184-2.184 7.968-5.448 7.968zm0-2.736c1.2 0 2.112-1.08 2.112-5.232 0-4.176-.912-5.112-2.112-5.112-1.176 0-2.112.936-2.112 5.112 0 4.152.936 5.232 2.112 5.232zM57.564 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/></g><g transform="translate(243)"><rect width="110" height="168" x="2" y="2" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M4 12.006c0-2.208.896-4.27 2.457-5.77a2 2 0 0 0-2.773-2.883A11.974 11.974 0 0 0 0 12.006a2 2 0 1 0 4 0zM14.388 4h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm18 0h8a2 2 0 0 0 0-4h-8a2 2 0 1 0 0 4zm17.51.227a8.015 8.015 0 0 1 5.022 3.756 2 2 0 1 0 3.458-2.011A12.01 12.01 0 0 0 104.844.34a2 2 0 0 0-.946 3.887zM110 16.78v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm0 18v8a2 2 0 0 0 4 0v-8a2 2 0 1 0-4 0zm-.024 17.844a7.99 7.99 0 0 1-2.903 5.558 2 2 0 0 0 2.54 3.09 11.977 11.977 0 0 0 4.35-8.338 2.002 2.002 0 0 0-1.838-2.15 2.003 2.003 0 0 0-2.15 1.84zM98.826 168h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-18 0h-8a2 2 0 0 0 0 4h8a2 2 0 1 0 0-4zm-17.334-.4a8.032 8.032 0 0 1-4.71-4.143 1.998 1.998 0 0 0-2.667-.938 1.997 1.997 0 0 0-.938 2.667 12.022 12.022 0 0 0 7.063 6.21 1.998 1.998 0 1 0 1.252-3.798zM4 154.434v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0zm0-18v-8a2 2 0 0 0-4 0v8a2 2 0 1 0 4 0z"/><path fill="#FEE1D3" d="M44 44a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 44 44zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 54 44zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 64 44zM34 56a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 34 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 44 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 54 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 64 56zm10 0a2 2 0 0 1 1.998-2h2.004a2 2 0 0 1 0 4h-2.004A1.994 1.994 0 0 1 74 56z"/><g transform="translate(21 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/></g><g transform="translate(69 82)"><rect width="24" height="4" y="10" fill="#F0EDF8" rx="2"/><rect width="14" height="4" x="5" fill="#C3B8E3" rx="2"/></g><rect width="8" height="4" x="8" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="21" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="34" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="47" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="60" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="73" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="86" y="14" fill="#EEE" rx="2"/><rect width="8" height="4" x="99" y="14" fill="#EEE" rx="2"/><path fill="#EEE" d="M46.716 138.288c-3.264 0-5.448-2.784-5.448-7.968s2.184-7.848 5.448-7.848c3.264 0 5.448 2.664 5.448 7.848 0 5.184-2.184 7.968-5.448 7.968zm0-2.736c1.2 0 2.112-1.08 2.112-5.232 0-4.176-.912-5.112-2.112-5.112-1.176 0-2.112.936-2.112 5.112 0 4.152.936 5.232 2.112 5.232zM57.564 132c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024zm.528 8.256l8.448-16.224h2.04l-8.448 16.224h-2.04zm11.016 0c-2.256 0-3.888-1.848-3.888-4.992 0-3.12 1.632-4.944 3.888-4.944 2.256 0 3.912 1.824 3.912 4.944 0 3.144-1.656 4.992-3.912 4.992zm0-1.968c.792 0 1.44-.792 1.44-3.024s-.648-2.976-1.44-2.976c-.792 0-1.44.744-1.44 2.976s.648 3.024 1.44 3.024z"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/convdev_overview.svg b/app/assets/images/illustrations/convdev/convdev_overview.svg
new file mode 100644
index 00000000000..a06d70812ca
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/convdev_overview.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="208" height="127" viewBox="0 0 208 127" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="58" height="98" y="17" rx="6"/><rect id="b" width="58" height="98" x="3.5" y="17" rx="6"/><rect id="c" width="58" height="98.394" rx="6"/></defs><g fill="none" fill-rule="evenodd" transform="translate(1)"><path fill="#000" fill-opacity=".06" fill-rule="nonzero" d="M16 11.06c0-1.39.56-2.69 1.534-3.635.398-.386.41-1.025.027-1.426a.993.993 0 0 0-1.413-.028A7.075 7.075 0 0 0 14 11.062c0 .556.448 1.007 1 1.007s1-.452 1-1.01zm6.432-5.043h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0h4.8c.552 0 1-.452 1-1.01 0-.556-.448-1.007-1-1.007h-4.8c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zm10.8 0H185a4.95 4.95 0 0 1 3.254 1.215.995.995 0 0 0 1.41-.108c.36-.423.312-1.06-.107-1.422A6.944 6.944 0 0 0 185 4h-.568c-.552 0-1 .45-1 1.008 0 .557.448 1.01 1 1.01zM190 11.932v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.008zm0 10.89v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.01v-4.838c0-.557-.448-1.01-1-1.01s-1 .453-1 1.01zm0 10.89v4.84c0 .555.448 1.007 1 1.007s1-.453 1-1.01v-4.84c0-.556-.448-1.007-1-1.007s-1 .45-1 1.008zm0 10.888v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008V44.6c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008zm0 10.89v4.84c0 .556.448 1.007 1 1.007s1-.45 1-1.008v-4.84c0-.557-.448-1.01-1-1.01s-1 .453-1 1.01zm0 10.89v4.838c0 .557.448 1.01 1 1.01s1-.453 1-1.01v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.01zm0 10.888v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.008v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008zm0 10.89v4.84c0 .556.448 1.007 1 1.007s1-.45 1-1.008v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.007zm0 10.888v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.008zm-.24 21.446a5.06 5.06 0 0 1-2.572 2.985 1.01 1.01 0 0 0-.46 1.348c.24.5.84.708 1.336.464a7.06 7.06 0 0 0 3.598-4.178c.17-.53-.12-1.098-.644-1.27a1 1 0 0 0-1.26.65zm-8.063 3.49h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.8 0h-4.8c-.552 0-1 .453-1 1.01 0 .557.448 1.008 1 1.008h4.8c.553 0 1-.45 1-1.008 0-.557-.447-1.01-1-1.01zm-10.577-.116a5.009 5.009 0 0 1-3.19-2.3.994.994 0 0 0-1.373-.333c-.472.29-.62.91-.332 1.386.99 1.632 2.6 2.8 4.465 3.215a1 1 0 0 0 1.192-.768 1.005 1.005 0 0 0-.762-1.2zM16 105.292v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.01v4.838c0 .557.448 1.01 1 1.01s1-.453 1-1.01zm0-10.89v-4.84c0-.555-.448-1.007-1-1.007s-1 .452-1 1.008v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.007zm0-10.888v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008zm0-10.89v-4.84c0-.556-.448-1.007-1-1.007s-1 .45-1 1.008v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.008zm0-10.89v-4.838c0-.557-.448-1.01-1-1.01s-1 .453-1 1.01v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.01zm0-11.888v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.008v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008zm0-9.89v-4.84c0-.556-.448-1.007-1-1.007s-1 .45-1 1.007v4.84c0 .557.448 1.008 1 1.008s1-.45 1-1.008zm0-10.888v-4.84c0-.557-.448-1.008-1-1.008s-1 .45-1 1.008v4.84c0 .556.448 1.008 1 1.008s1-.452 1-1.008zm0-10.89v-4.84c0-.556-.448-1.008-1-1.008s-1 .452-1 1.01v4.838c0 .557.448 1.01 1 1.01s1-.453 1-1.01z"/><g transform="translate(74)"><rect width="58" height="98" y="20" fill="#000" fill-opacity=".02" rx="6"/><use fill="#FFF" xlink:href="#a"/><rect width="56" height="96" x="1" y="18" stroke="#EEE" stroke-width="2" rx="6"/><g transform="translate(16 45.185)"><path fill="#333" d="M.59 33.815h5.655V32.15H4.58v-7.225H3.066c-.63.378-1.246.63-2.212.812v1.274H2.52v5.14H.59v1.665zm10.093.168c-1.778 0-3.094-.994-3.094-2.436 0-1.078.67-1.736 1.51-2.184v-.056c-.685-.518-1.19-1.162-1.19-2.1 0-1.512 1.19-2.45 2.843-2.45 1.624 0 2.702.966 2.702 2.436 0 .854-.546 1.54-1.162 1.946v.055c.854.462 1.54 1.148 1.54 2.324 0 1.4-1.26 2.463-3.15 2.463zm.56-5.348c.35-.406.546-.84.546-1.302 0-.686-.407-1.148-1.08-1.148-.545 0-.993.336-.993 1.022 0 .728.616 1.078 1.526 1.428zm-.518 3.92c.686 0 1.19-.364 1.19-1.106 0-.785-.756-1.08-1.876-1.555-.393.364-.687.868-.687 1.414 0 .783.63 1.245 1.372 1.245zm6.3-2.24c-1.316 0-2.268-1.078-2.268-2.912 0-1.82.952-2.884 2.268-2.884 1.316 0 2.282 1.063 2.282 2.883 0 1.834-.966 2.912-2.282 2.912zm0-1.148c.462 0 .84-.462.84-1.764s-.378-1.736-.84-1.736c-.462 0-.84.434-.84 1.736s.378 1.764.84 1.764zm.308 4.816l4.928-9.464h1.19l-4.927 9.463h-1.19zm6.426 0c-1.317 0-2.27-1.078-2.27-2.912 0-1.82.953-2.883 2.27-2.883 1.315 0 2.28 1.064 2.28 2.884 0 1.835-.965 2.913-2.28 2.913zm0-1.148c.46 0 .84-.462.84-1.764 0-1.3-.38-1.735-.84-1.735-.463 0-.84.434-.84 1.736 0 1.303.377 1.765.84 1.765z"/><rect width="13" height="2" x="6" y=".815" fill="#FB722E" rx="1"/><path fill="#F0EDF8" d="M3 47.815c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992a.994.994 0 0 1-.992-1zm0 6c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992a.994.994 0 0 1-.992-1z"/><rect width="20" height="2" x="3" y="6.815" fill="#FEE1D3" rx="1"/></g><g transform="translate(10.81)"><circle cx="18.19" cy="18" r="18" fill="#FFF"/><path fill="#F0EDF8" fill-rule="nonzero" d="M18.19 34c8.837 0 16-7.163 16-16s-7.163-16-16-16-16 7.163-16 16 7.163 16 16 16zm0 2c-9.94 0-18-8.06-18-18s8.06-18 18-18 18 8.06 18 18-8.06 18-18 18z"/><g transform="translate(10 11)"><path fill="#C3B8E3" fill-rule="nonzero" d="M2.19 13.32L5.397 11h7.783a.998.998 0 0 0 1.01-1V3c0-.55-.45-1-1.01-1H3.2a.998.998 0 0 0-1.01 1v10.32zM6.045 13l-3.422 2.476C1.28 16.45.19 15.892.19 14.23V3c0-1.657 1.337-3 3.01-3h9.98a3.004 3.004 0 0 1 3.01 3v7c0 1.657-1.337 3-3.01 3H6.045z"/><rect width="4" height="2" x="5.19" y="4" fill="#6B4FBB" rx="1"/><rect width="6" height="2" x="5.19" y="7" fill="#6B4FBB" rx="1"/></g></g></g><g transform="translate(144.5)"><rect width="58" height="98" x=".5" y="20" fill="#000" fill-opacity=".02" rx="6"/><use fill="#FFF" xlink:href="#b"/><rect width="56" height="96" x="4.5" y="18" stroke="#EEE" stroke-width="2" rx="6"/><g transform="translate(19 46.185)"><path fill="#333" d="M4.01 33.746c1.793 0 3.305-.938 3.305-2.59 0-1.148-.742-1.876-1.764-2.17v-.056c.953-.406 1.485-1.05 1.485-1.974 0-1.554-1.232-2.436-3.066-2.436-1.093 0-1.99.434-2.8 1.134l1.035 1.26c.56-.49 1.036-.784 1.666-.784.7 0 1.093.364 1.093.98 0 .714-.504 1.19-2.1 1.19v1.456c1.932 0 2.394.49 2.394 1.274 0 .672-.574 1.05-1.442 1.05-.756 0-1.414-.378-1.946-.896l-.953 1.302c.644.756 1.652 1.26 3.094 1.26zm4.51-.168h6.257v-1.736h-1.792c-.42 0-1.036.056-1.484.112 1.443-1.512 2.843-3.108 2.843-4.606 0-1.708-1.19-2.828-2.94-2.828-1.274 0-2.1.476-2.982 1.414l1.12 1.106c.45-.476.94-.91 1.583-.91.77 0 1.26.476 1.26 1.344 0 1.26-1.596 2.786-3.864 4.928v1.176zm9.505-3.5c-1.316 0-2.268-1.078-2.268-2.912 0-1.82.952-2.884 2.268-2.884 1.316 0 2.282 1.064 2.282 2.884 0 1.834-.966 2.912-2.282 2.912zm0-1.148c.462 0 .84-.462.84-1.764s-.378-1.736-.84-1.736c-.462 0-.84.434-.84 1.736s.378 1.764.84 1.764zm.308 4.816l4.928-9.464h1.19l-4.927 9.464h-1.19zm6.426 0c-1.317 0-2.27-1.078-2.27-2.912 0-1.82.953-2.884 2.27-2.884 1.315 0 2.28 1.064 2.28 2.884 0 1.834-.965 2.912-2.28 2.912zm0-1.148c.46 0 .84-.462.84-1.764s-.38-1.736-.84-1.736c-.463 0-.84.434-.84 1.736s.377 1.764.84 1.764z"/><rect width="13" height="2.008" x="7.5" fill="#FB722E" rx="1.004"/><path fill="#F0EDF8" d="M3.5 47.19c0-.556.455-1.005 1.006-1.005h17.988c.556 0 1.006.445 1.006 1.004 0 .553-.455 1.003-1.006 1.003H4.506A1.003 1.003 0 0 1 3.5 47.188zm0 6.023c0-.555.455-1.004 1.006-1.004h17.988c.556 0 1.006.444 1.006 1.003 0 .554-.455 1.004-1.006 1.004H4.506A1.003 1.003 0 0 1 3.5 53.212z"/><rect width="20" height="2.008" x="4" y="6.024" fill="#FEE1D3" rx="1.004"/></g><g transform="translate(14.413)"><circle cx="18.087" cy="18" r="18" fill="#FFF"/><path fill="#F0EDF8" fill-rule="nonzero" d="M18.087 34c8.836 0 16-7.163 16-16s-7.164-16-16-16c-8.837 0-16 7.163-16 16s7.163 16 16 16zm0 2c-9.942 0-18-8.06-18-18s8.058-18 18-18c9.94 0 18 8.06 18 18s-8.06 18-18 18z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M18.087 24a6 6 0 1 0 0-12 6 6 0 0 0 0 12zm0 2c-4.42 0-8-3.582-8-8s3.58-8 8-8a8 8 0 0 1 0 16z"/><path fill="#6B4FBB" d="M19.087 17v-2c0-.556-.448-1-1-1-.557 0-1 .448-1 1v3a.997.997 0 0 0 .998 1h3c.557 0 1-.448 1-1 0-.556-.447-1-1-1h-2z"/></g></g><rect width="58" height="98" x="3" y="20" fill="#000" fill-opacity=".02" rx="6"/><g transform="translate(0 16.754)"><use fill="#FFF" xlink:href="#c"/><rect width="56" height="96.394" x="1" y="1" stroke="#EEE" stroke-width="2" rx="6"/><g transform="translate(16 29.618)"><path fill="#333" d="M3.137 27.84c.462 0 .98-.253 1.33-.883-.182-1.4-.756-1.848-1.386-1.848-.6 0-1.12.433-1.12 1.44 0 .94.505 1.29 1.177 1.29zm-.322 4.955A3.626 3.626 0 0 1 .21 31.73l1.093-1.23c.294.335.854.63 1.372.63.994 0 1.764-.7 1.834-2.773-.463.588-1.233.938-1.78.938-1.51 0-2.645-.868-2.645-2.744 0-1.847 1.344-2.98 2.954-2.98 1.72 0 3.373 1.287 3.373 4.41 0 3.317-1.736 4.815-3.598 4.815zm8.12 0c-1.722 0-3.36-1.288-3.36-4.41 0-3.318 1.722-4.816 3.598-4.816 1.176 0 2.03.49 2.59 1.063l-1.078 1.232c-.308-.336-.868-.63-1.386-.63-.98 0-1.765.7-1.835 2.772.462-.588 1.232-.938 1.778-.938 1.526 0 2.646.867 2.646 2.743 0 1.848-1.345 2.982-2.955 2.982zm-.042-1.54c.616 0 1.12-.434 1.12-1.442 0-.938-.49-1.288-1.162-1.288-.46 0-.98.252-1.343.882.182 1.4.77 1.848 1.386 1.848zm6.132-2.128c-1.316 0-2.268-1.078-2.268-2.912 0-1.82.952-2.884 2.268-2.884 1.316 0 2.282 1.065 2.282 2.885 0 1.834-.966 2.912-2.282 2.912zm0-1.148c.462 0 .84-.463.84-1.765 0-1.302-.378-1.736-.84-1.736-.462 0-.84.433-.84 1.735s.378 1.764.84 1.764zm.308 4.815l4.928-9.464h1.19l-4.927 9.465h-1.19zm6.426 0c-1.317 0-2.27-1.078-2.27-2.912 0-1.82.953-2.884 2.27-2.884 1.315 0 2.28 1.063 2.28 2.883 0 1.834-.965 2.912-2.28 2.912zm0-1.148c.46 0 .84-.462.84-1.764s-.38-1.736-.84-1.736c-.463 0-.84.434-.84 1.736s.377 1.764.84 1.764z"/><rect width="13" height="2.008" x="6.5" y=".314" fill="#FEE1D3" rx="1.004"/><path fill="#F0EDF8" d="M3 46.627c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992a.994.994 0 0 1-.992-1zm0 6c0-.552.455-1 .992-1h18.016c.548 0 .992.444.992 1 0 .553-.455 1-.992 1H3.992a.994.994 0 0 1-.992-1z"/><rect width="20" height="2" x="3" y="5.627" fill="#FB722E" rx="1"/></g></g><g transform="translate(10.41)"><circle cx="18.589" cy="18" r="18" fill="#FFF"/><path fill="#F0EDF8" fill-rule="nonzero" d="M18.59 34c8.836 0 16-7.163 16-16s-7.164-16-16-16c-8.837 0-16 7.163-16 16s7.163 16 16 16zm0 2c-9.942 0-18-8.06-18-18s8.058-18 18-18c9.94 0 18 8.06 18 18s-8.06 18-18 18z"/><path fill="#C3B8E3" d="M17.05 19.262h3.367l.248-2.808H17.3l-.25 2.808zm-.177 2.008l-.144 1.627c-.06.662-.646 1.2-1.3 1.2h.25c-.658 0-1.144-.534-1.085-1.2l.144-1.627H13.59a1.001 1.001 0 0 1-1.003-1.004c0-.555.455-1.004 1.002-1.004h1.325l.248-2.808h-1.15a1 1 0 0 1-1.004-1.004 1.01 1.01 0 0 1 1.004-1.004h1.33l.106-1.2c.058-.66.644-1.198 1.298-1.198h-.25c.66 0 1.145.533 1.086 1.2l-.106 1.198h3.365l.107-1.2c.058-.66.644-1.198 1.298-1.198h-.25c.66 0 1.145.533 1.086 1.2l-.106 1.198h1.03c.554 0 1.003.446 1.003 1.004 0 .555-.455 1.004-1 1.004H22.8l-.25 2.808h1.037a1 1 0 0 1 1.002 1.004c0 .554-.456 1.004-1.003 1.004h-1.214l-.144 1.627c-.06.662-.646 1.2-1.3 1.2h.25c-.658 0-1.144-.534-1.085-1.2l.144-1.627H16.87z"/><path fill="#6B4FBB" d="M17.05 19.262l-.177 2.008H14.74l.177-2.008h2.134zm-1.707-4.816h2.135l-.178 2.008h-2.135l.178-2.008zm5.5 0h2.135l-.178 2.008h-2.135l.178-2.008zm1.708 4.816l-.177 2.008H20.24l.177-2.008h2.134z"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_1.svg b/app/assets/images/illustrations/convdev/i2p_step_1.svg
new file mode 100644
index 00000000000..67467b1513d
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_1.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M45.688 18.854c-4.869-1.989-10.488-1.975-15.29-.001a20.014 20.014 0 0 0-6.493 4.268 19.798 19.798 0 0 0-4.346 6.381 19.135 19.135 0 0 0-1.525 7.537c0 2.066.33 4.118.983 6.104a20.142 20.142 0 0 0 1.83 3.937 5.983 5.983 0 0 0-2.086 4.538c0 3.309 2.691 6 6 6s6-2.691 6-6-2.691-6-6-6c-.779 0-1.522.154-2.205.425a18.13 18.13 0 0 1-1.642-3.533 17.467 17.467 0 0 1-.881-5.472c0-2.351.459-4.623 1.391-6.814a17.721 17.721 0 0 1 3.88-5.675 18.057 18.057 0 0 1 5.85-3.845c4.329-1.778 9.392-1.79 13.78.002a18.077 18.077 0 0 1 5.843 3.84c3.39 3.34 5.257 7.776 5.257 12.493a17.463 17.463 0 0 1-.878 5.481 17.451 17.451 0 0 1-2.569 4.923c-2.134 2.866-3.818 4.698-5.174 6.173-2.424 2.643-3.98 4.599-4.383 8.384H32.215a1 1 0 1 0 0 2h11.739a1 1 0 0 0 .999-.947c.19-3.645 1.345-5.263 3.934-8.09 1.385-1.506 3.107-3.381 5.304-6.331a19.422 19.422 0 0 0 2.864-5.489c.651-1.98.98-4.04.979-6.109 0-5.256-2.078-10.198-5.856-13.92a20.079 20.079 0 0 0-6.49-4.265M28.761 51.612c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4M40 74h-4a1 1 0 1 0 0 2h4a1 1 0 1 0 0-2M42 70h-8a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2M38 10a1 1 0 0 0 1-1V1a1 1 0 1 0-2 0v8a1 1 0 0 0 1 1M20.828 15.828a.999.999 0 0 0 .707-1.707l-5.656-5.656a.999.999 0 1 0-1.414 1.414l5.656 5.656a.997.997 0 0 0 .707.293M10 33H2a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2M60.12 8.465l-5.656 5.656a.999.999 0 1 0 1.414 1.414l5.656-5.656a.999.999 0 1 0-1.414-1.414M74 33h-8a1 1 0 1 0 0 2h8a1 1 0 1 0 0-2M43 66H33a1 1 0 1 0 0 2h10a1 1 0 1 0 0-2"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_10.svg b/app/assets/images/illustrations/convdev/i2p_step_10.svg
new file mode 100644
index 00000000000..588ecd81414
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_10.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M5 43a1 1 0 1 0 2 0v-4h4a1 1 0 1 0 0-2H7v-4a1 1 0 1 0-2 0v4H1a1 1 0 1 0 0 2h4v4M75 37h-4v-4a1 1 0 1 0-2 0v4h-4a1 1 0 1 0 0 2h4v4a1 1 0 1 0 2 0v-4h4a1 1 0 1 0 0-2M21 38a1 1 0 0 0 .47.848l8 5a.999.999 0 0 0 1.061-1.696L23.887 38l6.644-4.152a1 1 0 1 0-1.061-1.695l-8 5A.998.998 0 0 0 21 38M55 38a1 1 0 0 0-.47-.848l-8-5a.999.999 0 1 0-1.061 1.695L52.113 38l-6.644 4.152a1 1 0 1 0 1.061 1.696l8-5A1 1 0 0 0 55 38M41.803 26.05a1 1 0 0 0-1.256.65l-7 22a1.001 1.001 0 0 0 .953 1.303 1 1 0 0 0 .953-.697l7-22a1.001 1.001 0 0 0-.65-1.256M62 7c3.859 0 7 3.141 7 7v11a1 1 0 1 0 2 0V14c0-4.963-4.04-9-9-9H45.91c-.479-2.833-2.943-5-5.91-5-3.309 0-6 2.691-6 6s2.691 6 6 6c2.967 0 5.431-2.167 5.91-5H62m-22 3c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4M6 26a1 1 0 0 0 1-1V14c0-3.859 3.141-7 7-7h11.09l-3.293 3.293a.999.999 0 1 0 1.414 1.414l5-5a.999.999 0 0 0 0-1.414l-5-5a.999.999 0 1 0-1.414 1.414L25.09 5H14c-4.963 0-9 4.04-9 9v11a1 1 0 0 0 1 1M36 64c-2.967 0-5.431 2.167-5.91 5H14c-3.859 0-7-3.141-7-7V51a1 1 0 1 0-2 0v11c0 4.963 4.04 9 9 9h16.09c.478 2.833 2.942 5 5.91 5 3.309 0 6-2.691 6-6s-2.691-6-6-6m0 10c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4M70 50a1 1 0 0 0-1 1v11c0 3.859-3.141 7-7 7H50.91l3.293-3.293a.999.999 0 1 0-1.414-1.414l-5 5a.999.999 0 0 0 0 1.414l5 5a.997.997 0 0 0 1.414 0 .999.999 0 0 0 0-1.414L50.91 71H62c4.963 0 9-4.04 9-9V51a1 1 0 0 0-1-1"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_2.svg b/app/assets/images/illustrations/convdev/i2p_step_2.svg
new file mode 100644
index 00000000000..4280024c23c
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M42.26 40.44a.989.989 0 0 0 1.109-.877l2.625-22.444a.997.997 0 0 0-.993-1.117h-14a1 1 0 0 0-.994 1.108l3.454 31.575a6.981 6.981 0 0 0-2.46 5.317c0 3.859 3.141 7 7 7s7-3.141 7-7-3.141-7-7-7c-.94 0-1.835.189-2.655.527l-3.23-29.527h11.761L41.383 39.33a1 1 0 0 0 .877 1.11m.741 13.562c0 2.757-2.243 5-5 5s-5-2.243-5-5 2.243-5 5-5 5 2.243 5 5"/><path d="M73.236 23.749a1 1 0 0 0-1.854.75A35.788 35.788 0 0 1 74 38c0 19.851-16.149 36-36 36S2 57.851 2 38 18.149 2 38 2c7.6 0 14.83 2.332 20.965 6.74A5.955 5.955 0 0 0 58 12c0 1.603.624 3.109 1.758 4.242A5.956 5.956 0 0 0 64 18a5.956 5.956 0 0 0 4.242-1.758C69.376 15.109 70 13.603 70 12s-.624-3.109-1.758-4.242A5.956 5.956 0 0 0 64 6a5.943 5.943 0 0 0-3.668 1.259C53.812 2.512 46.104 0 38 0 17.047 0 0 17.047 0 38s17.047 38 38 38 38-17.047 38-38c0-4.93-.93-9.725-2.764-14.251zM64 8c1.068 0 2.072.416 2.828 1.172S68 10.932 68 12s-.416 2.072-1.172 2.828c-1.512 1.512-4.145 1.512-5.656 0C60.416 14.072 60 13.068 60 12s.416-2.072 1.172-2.828S62.932 8 64 8z"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_3.svg b/app/assets/images/illustrations/convdev/i2p_step_3.svg
new file mode 100644
index 00000000000..7690f91b420
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_3.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M12 8c0-3.309-2.691-6-6-6S0 4.691 0 8c0 2.967 2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909s2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909s2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909 0 3.309 2.691 6 6 6s6-2.691 6-6c0-2.967-2.167-5.431-5-5.91v-8.18c2.833-.478 5-2.942 5-5.91s-2.167-5.431-5-5.91v-8.18c2.833-.478 5-2.942 5-5.91s-2.167-5.431-5-5.91v-8.18c2.833-.479 5-2.943 5-5.91M2 8c0-2.206 1.794-4 4-4s4 1.794 4 4-1.794 4-4 4-4-1.794-4-4m8 60c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4m0-20c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4m0-20c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4M21 6h54a1 1 0 1 0 0-2H21a1 1 0 1 0 0 2M21 12h35a1 1 0 1 0 0-2H21a1 1 0 1 0 0 2M75 24H21a1 1 0 1 0 0 2h54a1 1 0 1 0 0-2M21 32h34a1 1 0 1 0 0-2H21a1 1 0 1 0 0 2M75 44H21a1 1 0 1 0 0 2h54a1 1 0 1 0 0-2M21 52h34a1 1 0 1 0 0-2H21a1 1 0 1 0 0 2M75 64H21a1 1 0 1 0 0 2h54a1 1 0 1 0 0-2M55 70H21a1 1 0 1 0 0 2h34a1 1 0 1 0 0-2"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_4.svg b/app/assets/images/illustrations/convdev/i2p_step_4.svg
new file mode 100644
index 00000000000..ba21b9e2c3a
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_4.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M67.7 10h-6.751C60.442 4.402 55.728 0 50 0c-6.06 0-11 4.935-11 11s4.935 11 11 11c5.728 0 10.442-4.402 10.949-10H67.7c1.269 0 2.3.987 2.3 2.2v57.6c0 1.213-1.031 2.2-2.3 2.2H8.3C7.031 74 6 73.013 6 71.8V14.2C6 12.987 7.031 12 8.3 12h15.15a1 1 0 1 0 0-2H8.3C5.929 10 4 11.884 4 14.2v57.6C4 74.116 5.929 76 8.3 76h59.4c2.371 0 4.3-1.884 4.3-4.2V14.2c0-2.316-1.929-4.2-4.3-4.2M50 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/><path d="M21.293 29.29a.999.999 0 0 0 0 1.414l12.975 12.975-12.975 12.974a.999.999 0 1 0 1.414 1.414l13.682-13.682a.999.999 0 0 0 0-1.414L22.707 29.29a.999.999 0 0 0-1.414 0M54 59a1 1 0 1 0 0-2H42a1 1 0 1 0 0 2h12"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_5.svg b/app/assets/images/illustrations/convdev/i2p_step_5.svg
new file mode 100644
index 00000000000..3c8f8422a97
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_5.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M48.949 37C48.442 31.402 43.728 27 38 27s-10.442 4.402-10.949 10h-13.05a1 1 0 1 0 0 2h13.05c.507 5.598 5.221 10 10.949 10s10.442-4.402 10.949-10h12.24a1 1 0 1 0 0-2h-12.24M38 47c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/><path d="M73.236 23.749a1 1 0 0 0-1.854.75A35.788 35.788 0 0 1 74 38c0 19.851-16.149 36-36 36S2 57.851 2 38 18.149 2 38 2c7.6 0 14.83 2.332 20.965 6.74A5.955 5.955 0 0 0 58 12c0 1.603.624 3.109 1.758 4.242A5.956 5.956 0 0 0 64 18a5.956 5.956 0 0 0 4.242-1.758C69.376 15.109 70 13.603 70 12s-.624-3.109-1.758-4.242A5.956 5.956 0 0 0 64 6a5.943 5.943 0 0 0-3.668 1.259C53.812 2.512 46.104 0 38 0 17.047 0 0 17.047 0 38s17.047 38 38 38 38-17.047 38-38c0-4.93-.93-9.725-2.764-14.251zM64 8c1.068 0 2.072.416 2.828 1.172S68 10.932 68 12s-.416 2.072-1.172 2.828c-1.512 1.512-4.145 1.512-5.656 0C60.416 14.072 60 13.068 60 12s.416-2.072 1.172-2.828S62.932 8 64 8z"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_6.svg b/app/assets/images/illustrations/convdev/i2p_step_6.svg
new file mode 100644
index 00000000000..933860798ad
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_6.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M14.267 7.32l-4.896 5.277-1.702-1.533a.999.999 0 1 0-1.338 1.486l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062a.99.99 0 0 0 .752-.016c.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6a1 1 0 0 0-1.466-1.36M31 9h44a1 1 0 1 0 0-2H31a1 1 0 1 0 0 2M31 15h24a1 1 0 1 0 0-2H31a1 1 0 1 0 0 2"/><path d="M11 0C4.93 0 0 4.935 0 11s4.935 11 11 11 11-4.935 11-11S17.065 0 11 0m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9M14.267 34.32l-4.896 5.277-1.702-1.533a1 1 0 1 0-1.338 1.486l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062a.99.99 0 0 0 .752-.016c.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6a1 1 0 0 0-1.466-1.36M75 34H31a1 1 0 1 0 0 2h44a1 1 0 1 0 0-2M31 42h24a1 1 0 1 0 0-2H31a1 1 0 1 0 0 2"/><path d="M11 27C4.93 27 0 31.935 0 38s4.935 11 11 11 11-4.935 11-11-4.935-11-11-11m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9M14.267 61.32l-4.896 5.277-1.702-1.533a1 1 0 1 0-1.338 1.486l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062a.99.99 0 0 0 .752-.016c.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6a1 1 0 0 0-1.466-1.36"/><path d="M11 54C4.93 54 0 58.935 0 65s4.935 11 11 11 11-4.935 11-11-4.935-11-11-11m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9M75 61H31a1 1 0 1 0 0 2h44a1 1 0 1 0 0-2M55 67H31a1 1 0 1 0 0 2h24a1 1 0 1 0 0-2"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_7.svg b/app/assets/images/illustrations/convdev/i2p_step_7.svg
new file mode 100644
index 00000000000..d97c8f7c2d4
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_7.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M73.236 23.749a1 1 0 1 0-1.854.75A35.788 35.788 0 0 1 74 38c0 19.851-16.149 36-36 36S2 57.851 2 38 18.149 2 38 2c7.6 0 14.83 2.332 20.965 6.74A5.955 5.955 0 0 0 58 12c0 1.603.624 3.109 1.758 4.242A5.956 5.956 0 0 0 64 18a5.956 5.956 0 0 0 4.242-1.758C69.376 15.109 70 13.603 70 12s-.624-3.109-1.758-4.242A5.956 5.956 0 0 0 64 6a5.943 5.943 0 0 0-3.668 1.259C53.812 2.512 46.104 0 38 0 17.047 0 0 17.047 0 38s17.047 38 38 38 38-17.047 38-38c0-4.93-.93-9.725-2.764-14.251zM64 8c1.068 0 2.072.416 2.828 1.172S68 10.932 68 12s-.416 2.072-1.172 2.828c-1.512 1.512-4.145 1.512-5.656 0C60.416 14.072 60 13.068 60 12s.416-2.072 1.172-2.828S62.932 8 64 8z"/><path d="M27.19 32.17a.997.997 0 0 0-1.366-.364L13.17 39.132a1 1 0 0 0 0 1.73l12.654 7.326a1 1 0 0 0 1.002-1.73l-11.159-6.461 11.159-6.461a.998.998 0 0 0 .364-1.366M48.808 47.827a1 1 0 0 0 1.366.364l12.654-7.326a1 1 0 0 0 0-1.73l-12.654-7.326a1 1 0 0 0-1.002 1.73L60.331 40l-11.159 6.461a.998.998 0 0 0-.364 1.366M42.71 23.06L31.398 56.29a1 1 0 0 0 1.892.645l11.312-33.23a1 1 0 0 0-1.892-.645"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_8.svg b/app/assets/images/illustrations/convdev/i2p_step_8.svg
new file mode 100644
index 00000000000..919bbeff319
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_8.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M62.44 54.765l-9.912-11.09c.315-3.881.481-7.241.508-10.271-.029-13.871-3.789-23.05-13.413-32.746-.855-.859-2.411-.828-3.294.059-7.594 7.65-11.139 13.934-12.575 22.3a6.94 6.94 0 0 0-4.699 2.039c-1.321 1.321-2.05 3.079-2.05 4.949s.729 3.628 2.051 4.949c1.321 1.322 3.079 2.051 4.949 2.051s3.628-.729 4.949-2.051a6.951 6.951 0 0 0 2.051-4.949 6.955 6.955 0 0 0-2.051-4.949c-.9-.9-2-1.517-3.205-1.824 1.373-7.859 4.764-13.818 11.999-21.11.128-.13.356-.158.456-.059 9.207 9.274 12.805 18.06 12.832 31.33-.026 3.079-.202 6.527-.536 10.54a.997.997 0 0 0 .25.749l10.166 11.379c.062.076.109.23.093.32l-4.547 17.407c-.004.015-.009.036-.079.106a.403.403 0 0 1-.2.106l-3.577.002c-.144-.009-.265-.077-.309-.153l-5.425-10.328a1.002 1.002 0 0 0-.886-.535H30.024c-.371 0-.713.206-.886.535l-5.407 10.303-.069.072a.366.366 0 0 1-.199.105l-3.588.001c-.179-.009-.304-.123-.33-.227l-4.531-17.338a.525.525 0 0 1 .049-.34L25.26 44.682a1 1 0 0 0-1.492-1.332L13.539 54.803c-.448.554-.63 1.312-.474 2.084l4.544 17.396c.253.963 1.146 1.669 2.218 1.719h3.636c.581 0 1.187-.261 1.615-.693.114-.114.286-.286.406-.528l5.144-9.793h14.754l5.16 9.822c.396.697 1.124 1.143 2.01 1.192l3.712-.003a2.396 2.396 0 0 0 1.544-.694c.313-.316.504-.646.598-1.022l4.557-17.451a2.502 2.502 0 0 0-.518-2.066M29.01 30.001c0 1.335-.521 2.591-1.465 3.535s-2.2 1.465-3.535 1.465-2.591-.521-3.535-1.465-1.465-2.2-1.465-3.535.521-2.591 1.465-3.535 2.2-1.465 3.535-1.465 2.591.521 3.535 1.465 1.465 2.2 1.465 3.535"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/convdev/i2p_step_9.svg b/app/assets/images/illustrations/convdev/i2p_step_9.svg
new file mode 100644
index 00000000000..2d1b10d430d
--- /dev/null
+++ b/app/assets/images/illustrations/convdev/i2p_step_9.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76"><path d="M68 67c-1.725 0-3.36.541-4.723 1.545A12.998 12.998 0 0 0 52 62c-2.734 0-5.359.853-7.555 2.43L42.159 49h1.228l3.829 7.645c.339.598.962.979 1.724 1.022l2.812-.003a2.07 2.07 0 0 0 1.316-.595c.264-.266.433-.559.514-.882l3.433-13.145a2.138 2.138 0 0 0-.449-1.763l-7.385-8.268c.231-2.875.354-5.376.374-7.641C49.532 14.863 46.684 7.908 39.393.564c-.737-.742-2.072-.715-2.829.044-5.617 5.659-8.309 10.336-9.446 16.463a5.95 5.95 0 0 0-3.36 1.686C22.624 19.891 22 21.397 22 23s.624 3.109 1.758 4.242C24.891 28.376 26.397 29 28 29s3.109-.624 4.242-1.758C33.376 26.109 34 24.603 34 23s-.624-3.109-1.758-4.242a5.952 5.952 0 0 0-3.098-1.648c1.095-5.538 3.637-9.855 8.83-15.14 6.874 6.924 9.561 13.485 9.581 23.392-.021 2.316-.151 4.903-.402 7.91a.999.999 0 0 0 .25.749l7.663 8.572-3.391 13.07-2.695.036-4.081-8.15a1.001 1.001 0 0 0-.895-.553h-12.01c-.379 0-.725.214-.895.553l-4.04 8.114-2.707.015-3.427-13.07 7.671-8.588a1 1 0 0 0-1.492-1.332l-7.7 8.623c-.383.47-.54 1.116-.406 1.787l3.419 13.08c.216.829.98 1.438 1.907 1.48h2.735c.508 0 1.016-.218 1.391-.595.091-.09.242-.241.358-.475l3.804-7.597h1.228l-2.286 15.43a12.914 12.914 0 0 0-7.555-2.43c-4.685 0-8.979 2.53-11.277 6.545a7.943 7.943 0 0 0-4.723-1.545c-4.411 0-8 3.589-8 8a1 1 0 0 0 1 1h74a1 1 0 0 0 1-1c0-4.411-3.589-8-8-8m-36-44a3.973 3.973 0 0 1-1.172 2.828c-1.512 1.512-4.145 1.512-5.656 0-.756-.756-1.172-1.76-1.172-2.828s.416-2.072 1.172-2.828 1.76-1.172 2.828-1.172 2.072.416 2.828 1.172 1.172 1.76 1.172 2.828m-29.917 51a6.01 6.01 0 0 1 5.917-5c1.638 0 3.17.652 4.313 1.836a.998.998 0 0 0 1.634-.289 11.011 11.011 0 0 1 10.05-6.547c2.836 0 5.532 1.085 7.593 3.055a1.001 1.001 0 0 0 1.681-.576l2.588-17.479h4.275l2.589 17.479a.999.999 0 1 0 1.681.576 10.945 10.945 0 0 1 7.593-3.055c4.343 0 8.288 2.57 10.05 6.547a.998.998 0 0 0 1.634.289 5.948 5.948 0 0 1 4.313-1.836 6.01 6.01 0 0 1 5.917 5H2.076"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/epics.svg b/app/assets/images/illustrations/epics.svg
new file mode 100644
index 00000000000..1a37e6bba5f
--- /dev/null
+++ b/app/assets/images/illustrations/epics.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="430" height="300" viewBox="0 0 430 300"><g fill="none" fill-rule="evenodd"><g transform="translate(75 53)"><rect width="284" height="208" y="5" fill="#F9F9F9" rx="10"/><rect width="284" height="208" fill="#FFF" rx="10"/><path fill="#EEE" fill-rule="nonzero" d="M10 4a6 6 0 0 0-6 6v188a6 6 0 0 0 6 6h264a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h264c5.523 0 10 4.477 10 10v188c0 5.523-4.477 10-10 10H10c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0z"/><path fill="#EEE" fill-rule="nonzero" d="M25.168 153.995c3.837-.215 7.173.028 10.119.691a3 3 0 1 0 1.318-5.853c-3.509-.79-7.4-1.074-11.773-.828a3 3 0 1 0 .336 5.99zm19.043 4.66c2.401 1.704 4.388 3.61 7.569 7.083a3 3 0 0 0 4.424-4.054c-3.448-3.763-5.686-5.911-8.522-7.923a3 3 0 1 0-3.471 4.894zm15.575 15.173c3.181 2.675 6.52 4.665 10.397 6.039a3 3 0 0 0 2.004-5.655c-3.162-1.121-5.884-2.743-8.54-4.976a3 3 0 1 0-3.861 4.592zm22.133 8.148c1.02.037 2.067.045 3.143.023a72.664 72.664 0 0 0 8.346-.638 3 3 0 1 0-.812-5.945c-2.442.334-4.996.53-7.658.585a48.55 48.55 0 0 1-2.796-.021 3 3 0 0 0-.223 5.996zm22.778-3.286c3.9-1.37 7.427-3.15 10.54-5.305a3 3 0 0 0-3.415-4.933c-2.665 1.845-5.712 3.382-9.114 4.578a3 3 0 0 0 1.989 5.66zm19.156-13.62a33.752 33.752 0 0 0 5.276-10.817 3 3 0 1 0-5.773-1.633 27.753 27.753 0 0 1-4.341 8.9 3 3 0 1 0 4.838 3.55zm6.577-22.657c-.187-3.817-.926-7.71-2.204-11.596a3 3 0 0 0-5.7 1.874c1.113 3.384 1.75 6.745 1.91 10.016a3 3 0 1 0 5.994-.294zm-7.097-22.26c-1.897-3.2-4.152-6.325-6.748-9.344a3 3 0 0 0-4.55 3.913c2.372 2.756 4.421 5.597 6.136 8.49a3 3 0 0 0 5.162-3.06zm-11.546-17.793c-.938-3.025-1.402-6.42-1.365-9.976a3 3 0 0 0-6-.063c-.043 4.163.506 8.177 1.634 11.816a3 3 0 1 0 5.731-1.777zm.053-20.107c.905-3.341 2.22-6.538 3.904-9.448a3 3 0 0 0-5.194-3.004c-1.948 3.368-3.463 7.048-4.501 10.884a3 3 0 1 0 5.791 1.568zm10.134-17.305c2.475-2.28 5.265-4.09 8.335-5.374a3 3 0 1 0-2.314-5.536c-3.725 1.558-7.105 3.75-10.086 6.497a3 3 0 1 0 4.065 4.413zm18.177-7.586c3.202-.18 6.599.092 10.18.843a3 3 0 0 0 1.23-5.872c-4.086-.857-8.009-1.172-11.747-.962a3 3 0 1 0 .337 5.99zm20.047 3.95c3.068 1.268 6.232 2.842 9.487 4.728a3 3 0 0 0 3.009-5.191c-3.48-2.017-6.883-3.71-10.204-5.083a3 3 0 1 0-2.292 5.545zm19.578 9.955c3.711 1.586 7.376 2.77 10.997 3.565a3 3 0 0 0 1.286-5.86c-3.248-.713-6.555-1.782-9.925-3.222a3 3 0 1 0-2.358 5.517zm22.591 4.789c3.94-.04 7.808-.553 11.61-1.513a3 3 0 1 0-1.468-5.817 43.358 43.358 0 0 1-10.203 1.33 3 3 0 0 0 .061 6zm22.52-5.558c3.335-1.637 6.607-3.613 9.845-5.916a3 3 0 1 0-3.477-4.89c-2.984 2.122-5.98 3.931-9.011 5.42a3 3 0 1 0 2.643 5.386zm18.678-13.054a3 3 0 0 1-4.02-4.454 130.547 130.547 0 0 0 5.31-5.088 3 3 0 1 1 4.265 4.22 136.507 136.507 0 0 1-5.555 5.322zm-48.722 25.641a3 3 0 1 1 4.314-4.17c3.056 3.16 5.075 6.744 6.172 10.754a3 3 0 0 1-5.787 1.584c-.834-3.047-2.35-5.739-4.699-8.168zm5.347 18.049a3 3 0 1 1 5.978.52c-.282 3.232-.805 6.273-1.832 11.206a3 3 0 0 1-5.874-1.222c.981-4.717 1.473-7.572 1.728-10.504zm-3.777 21.555a3 3 0 0 1 5.953.747c-.5 3.988-.397 7.09.399 9.67a3 3 0 1 1-5.733 1.769c-1.087-3.52-1.217-7.426-.62-12.186zm7.393 22.444a3 3 0 0 1 4.461-4.013c2.703 3.005 5.224 5.296 7.594 6.947a3 3 0 0 1-3.429 4.924c-2.775-1.932-5.632-4.53-8.626-7.858zm20.352 12.28a3 3 0 1 1 .334-5.99c2.77.154 5.453-.554 9.224-2.254a3 3 0 0 1 2.466 5.47c-4.57 2.06-8.103 2.993-12.024 2.775zm21.784-7.058a3 3 0 0 1-1.815-5.719c4.227-1.342 8.24-1.61 12.496-.572a3 3 0 0 1-1.421 5.83c-3.116-.76-6.025-.566-9.26.46zM106.53 56.038a3 3 0 1 1-3.45 4.909c-1.074-.755-6.723-6.044-8.083-7.204a68.019 68.019 0 0 0-.332-.281 3 3 0 1 1 3.865-4.59l.362.306c1.643 1.402 6.971 6.391 7.638 6.86zM88.536 42.422a3 3 0 0 1-2.285 5.548c-3.14-1.293-5.78-1.34-8.105-.05a3 3 0 0 1-2.91-5.247c4.087-2.266 8.597-2.187 13.3-.25zM66.698 48.73a3 3 0 0 1 2.029 5.647c-4.432 1.592-8.786.835-13.166-1.88a3 3 0 1 1 3.16-5.1c2.93 1.816 5.425 2.25 7.977 1.333zm-15.636-8.038a3 3 0 0 1-4.352 4.13c-.911-.96-1.85-1.98-3.061-3.32-.295-.325-2.437-2.703-3.07-3.4-.47-.518-.9-.988-1.313-1.436a3 3 0 0 1 4.41-4.068c.425.46.866.942 1.346 1.47.642.709 2.79 3.092 3.076 3.41a180.865 180.865 0 0 0 2.964 3.214z"/><path fill="#E1DBF1" d="M254.66 72.196l2-3.464a2 2 0 1 0-3.464-2l-2 3.464-3.464-2a2 2 0 0 0-2 3.464l3.464 2-2 3.464a2 2 0 0 0 3.464 2l2-3.464 3.464 2a2 2 0 1 0 2-3.464l-3.464-2zm-151.904 78.732l2.829-2.828a2 2 0 0 0-2.829-2.829l-2.828 2.829-2.828-2.829a2 2 0 0 0-2.829 2.829l2.829 2.828-2.829 2.829a2 2 0 1 0 2.829 2.828l2.828-2.828 2.828 2.828a2 2 0 1 0 2.829-2.828l-2.829-2.829z"/><path fill="#6B4FBB" d="M210.66 173.66l3.464-2a2 2 0 1 0-2-3.464l-3.464 2-2-3.464a2 2 0 0 0-3.464 2l2 3.464-3.464 2a2 2 0 1 0 2 3.464l3.464-2 2 3.464a2 2 0 1 0 3.464-2l-2-3.464z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M27 181a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm0-4a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M138 85a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M200 57a7 7 0 1 1 0-14 7 7 0 0 1 0 14zm0-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill="#FC6D26" fill-rule="nonzero" d="M222.647 121.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M103.647 28.647v5h5v-5h-5zm0-4h5a4 4 0 0 1 4 4v5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4v-5a4 4 0 0 1 4-4z"/><path fill="#FC6D26" fill-rule="nonzero" d="M85 103.488L81.841 108h6.318L85 103.488zm6.436 2.218A4 4 0 0 1 88.159 112H81.84a4 4 0 0 1-3.277-6.294l3.16-4.512a4 4 0 0 1 6.553 0l3.159 4.512z"/></g><path fill="#F9F9F9" d="M334.376 99.43A48.805 48.805 0 0 0 366 111c27.062 0 49-21.938 49-49s-21.938-49-49-49-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71z"/><path fill="#FFF" d="M339.376 94.43A48.805 48.805 0 0 0 371 106c27.062 0 49-21.938 49-49S398.062 8 371 8s-49 21.938-49 49c0 9.454 2.677 18.283 7.315 25.77l-3.05 11.306a2.5 2.5 0 0 0 3.064 3.065l10.047-2.71z"/><path fill="#EEE" fill-rule="nonzero" d="M329.85 99.072a4.5 4.5 0 0 1-5.516-5.517l2.827-10.48C322.501 75.258 320 66.31 320 57c0-28.167 22.833-51 51-51s51 22.833 51 51-22.833 51-51 51c-11.859 0-23.096-4.064-32.102-11.37l-9.048 2.442zm10.817-6.169C349.091 100.027 359.737 104 371 104c25.957 0 47-21.043 47-47s-21.043-47-47-47-47 21.043-47 47c0 8.859 2.453 17.351 7.016 24.716l.456.737-3.277 12.144c.072.527.347.685.613.613l11.059-2.984.8.677z"/><g transform="translate(354 34)"><path fill="#E1DBF1" fill-rule="nonzero" d="M13 4a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-8zm0-4h8a5 5 0 0 1 5 5v1a5 5 0 0 1-5 5h-8a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M5 11a1 1 0 0 0 0 2h24a1 1 0 0 0 0-2H5zm0-4h24a5 5 0 0 1 0 10H5A5 5 0 0 1 5 7z"/><rect width="12" height="4" x="11" y="31" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="19" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="37" fill="#E1DBF1" rx="2"/><rect width="12" height="4" x="11" y="43" fill="#C3B8E3" rx="2"/><rect width="12" height="4" x="11" y="25" fill="#E1DBF1" rx="2"/></g><path fill="#F9F9F9" d="M344.238 225.072A38.83 38.83 0 0 1 368 217c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213z"/><path fill="#FFF" d="M348.238 221.072A38.83 38.83 0 0 1 372 213c21.54 0 39 17.46 39 39s-17.46 39-39 39-39-17.46-39-39a38.84 38.84 0 0 1 4.001-17.227l-3.737-13.85a2.5 2.5 0 0 1 3.065-3.064l11.91 3.213z"/><path fill="#EEE" fill-rule="nonzero" d="M336.85 215.928a4.5 4.5 0 0 0-5.516 5.517l3.543 13.13A40.848 40.848 0 0 0 331 252c0 22.644 18.356 41 41 41s41-18.356 41-41-18.356-41-41-41a40.82 40.82 0 0 0-24.182 7.887l-10.968-2.96zm12.608 6.73A36.824 36.824 0 0 1 372 215c20.435 0 37 16.565 37 37s-16.565 37-37 37-37-16.565-37-37c0-5.747 1.31-11.304 3.795-16.343l.334-.677-3.934-14.577a.5.5 0 0 1 .613-.613l12.865 3.471.785-.604z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M356.097 255.962a7 7 0 0 0 8.81 10.88l1.093-.885v1.454a7 7 0 1 0 14 0v-1.454l1.092.885a7 7 0 1 0 8.81-10.88l-1.185-.96 1.455-.337a7 7 0 1 0-3.15-13.64l-1.4.323.623-1.278a7 7 0 0 0-12.583-6.137l-.662 1.356-.662-1.356a7 7 0 0 0-12.583 6.137l.623 1.278-1.4-.324a7 7 0 1 0-3.15 13.641l1.455.336-1.186.96zm5.464-.913a11.914 11.914 0 0 1-.444-1.95l-.19-1.362-4.2-.97a3 3 0 0 1 1.35-5.845l4.178.964.768-1.145c.373-.557.793-1.082 1.254-1.57l.95-1.006-1.877-3.849a3 3 0 0 1 5.393-2.63l1.892 3.879 1.363-.113a12.188 12.188 0 0 1 2.004 0l1.363.113 1.892-3.879a3 3 0 0 1 5.393 2.63l-1.877 3.849.95 1.006c.461.488.88 1.013 1.254 1.57l.768 1.145 4.178-.964a3 3 0 1 1 1.35 5.846l-4.2.97-.19 1.36a11.914 11.914 0 0 1-.444 1.95l-.413 1.302 3.36 2.72a3 3 0 1 1-3.776 4.663l-3.32-2.688-1.196.706a11.94 11.94 0 0 1-1.808.873l-1.286.492v4.295a3 3 0 1 1-6 0v-4.295l-1.286-.492a11.94 11.94 0 0 1-1.808-.873l-1.196-.706-3.32 2.688a3 3 0 1 1-3.776-4.663l3.36-2.72-.413-1.301z"/><path fill="#FC6D26" fill-rule="nonzero" d="M373 245.411a6 6 0 1 0 0 12 6 6 0 0 0 0-12zm0 4a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/><g><path fill="#F9F9F9" d="M94.624 162.43A48.805 48.805 0 0 1 63 174c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71z"/><path fill="#FFF" stroke="#EEE" stroke-width="4" d="M89.624 157.43A48.805 48.805 0 0 1 58 169c-27.062 0-49-21.938-49-49s21.938-49 49-49 49 21.938 49 49c0 9.454-2.677 18.283-7.315 25.77l3.05 11.306a2.5 2.5 0 0 1-3.064 3.065l-10.047-2.71z"/><path fill="#EEE" fill-rule="nonzero" d="M99.15 162.072a4.5 4.5 0 0 0 5.516-5.517l-2.827-10.48C106.499 138.258 109 129.31 109 120c0-28.167-22.833-51-51-51S7 91.833 7 120s22.833 51 51 51c11.859 0 23.096-4.064 32.102-11.37l9.048 2.442zm-10.817-6.169C79.909 163.027 69.263 167 58 167c-25.957 0-47-21.043-47-47s21.043-47 47-47 47 21.043 47 47c0 8.859-2.453 17.351-7.016 24.716l-.456.737 3.277 12.144c-.072.527-.347.685-.613.613l-11.059-2.984-.8.677z"/><g fill-rule="nonzero"><path fill="#FEE1D3" d="M55.47 94.47l-16.148 6.688a4 4 0 0 0-2.164 2.164l-6.689 16.147a4 4 0 0 0 0 3.062l6.689 16.147a4 4 0 0 0 2.164 2.164l16.147 6.689a4 4 0 0 0 3.062 0l16.147-6.689a4 4 0 0 0 2.164-2.164l6.689-16.147a4 4 0 0 0 0-3.062l-6.689-16.147a4 4 0 0 0-2.164-2.164L58.53 94.469a4 4 0 0 0-3.062 0zM57 98.164l16.147 6.688L79.835 121l-6.688 16.147L57 143.835l-16.147-6.688L34.165 121l6.688-16.147L57 98.165zM57 107a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 4a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4 11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-12-6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm12 20c6.075 0 11-4.925 11-11s-4.925-11-11-11-11 4.925-11 11 4.925 11 11 11zm0-4a7 7 0 1 1 0-14 7 7 0 0 1 0 14z"/><path fill="#FC6D26" d="M57 126.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0-3a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></g></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/gitlab_logo.svg b/app/assets/images/illustrations/gitlab_logo.svg
new file mode 100644
index 00000000000..8dbd75a340e
--- /dev/null
+++ b/app/assets/images/illustrations/gitlab_logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="492.509" height="453.68" viewBox="0 0 492.50943 453.67966"><g fill="none" fill-rule="evenodd"><path d="M491.589 259.398l-27.559-84.814L409.413 6.486c-2.81-8.648-15.045-8.648-17.856 0l-54.619 168.098H155.572L100.952 6.486c-2.81-8.648-15.046-8.648-17.856 0L28.478 174.584.921 259.398a18.775 18.775 0 0 0 6.82 20.992l238.513 173.29L484.77 280.39a18.777 18.777 0 0 0 6.82-20.992" fill="#fc6d26"/><path d="M246.255 453.68l90.684-279.096H155.57z" fill="#e24329"/><path d="M246.255 453.68L155.57 174.583H28.479z" fill="#fc6d26"/><path d="M28.479 174.584L.92 259.4a18.773 18.773 0 0 0 6.821 20.99l238.514 173.29z" fill="#fca326"/><path d="M28.479 174.584H155.57L100.952 6.487c-2.81-8.65-15.047-8.65-17.856 0z" fill="#e24329"/><path d="M246.255 453.68l90.684-279.096H464.03z" fill="#fc6d26"/><path d="M464.03 174.584l27.56 84.815a18.773 18.773 0 0 1-6.822 20.99L246.255 453.68z" fill="#fca326"/><path d="M464.03 174.584H336.94L391.557 6.487c2.811-8.65 15.047-8.65 17.856 0z" fill="#e24329"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/logos/go_logo.svg b/app/assets/images/illustrations/logos/go_logo.svg
new file mode 100644
index 00000000000..7fd49118006
--- /dev/null
+++ b/app/assets/images/illustrations/logos/go_logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="M14 16.01h1V7.99C15 4.128 11.866.999 8 .999c-3.858 0-7 3.13-7 6.991v8.02h1V7.99c0-3.306 2.691-5.991 6-5.991 3.314 0 6 2.682 6 5.991v8.02M3.48 2.656a2 2 0 1 0-2.155 3.228c.102-.321.226-.631.371-.93a1.001 1.001 0 1 1 1.069-1.599 6.96 6.96 0 0 1 .717-.699m9.04-.002a2 2 0 1 1 2.155 3.23 6.835 6.835 0 0 0-.37-.931 1 1 0 1 0-1.068-1.599 6.96 6.96 0 0 0-.717-.699"/><path d="M5.726 8.04h1.557v.124c0 .283-.033.534-.1.752a1.583 1.583 0 0 1-.33.566c-.35.394-.795.591-1.335.591-.527 0-.979-.19-1.355-.571a1.893 1.893 0 0 1-.564-1.377c0-.547.191-1.01.574-1.391a1.902 1.902 0 0 1 1.396-.574c.295 0 .57.06.825.181.244.12.484.316.72.586l-.405.388c-.309-.412-.686-.618-1.13-.618-.399 0-.733.138-1 .413-.27.27-.405.609-.405 1.015 0 .42.151.766.452 1.037.282.252.587.378.915.378.28 0 .531-.094.754-.283.223-.19.347-.418.373-.683h-.94v-.535m2.884.061c0-.53.194-.986.583-1.367a1.919 1.919 0 0 1 1.396-.571c.537 0 .998.192 1.382.576.386.384.578.845.578 1.384 0 .542-.194 1-.581 1.379a1.944 1.944 0 0 1-1.408.569c-.487 0-.923-.168-1.311-.505-.426-.373-.64-.861-.64-1.465m.574.007c0 .417.14.759.42 1.028.278.269.6.403.964.403.395 0 .729-.137 1-.41.272-.277.408-.613.408-1.01 0-.402-.134-.739-.403-1.01a1.33 1.33 0 0 0-.991-.41c-.392 0-.723.137-.993.41a1.36 1.36 0 0 0-.405 1m-.184 3.918c.525.026.812.063.812.063.271.025.324-.096.116-.273 0 0-.775-.813-1.933-.813-1.159 0-1.923.813-1.923.813-.211.174-.153.3.12.273 0 0 .286-.037.81-.063v.477c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.252.25c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.478m-1-1.023c.552 0 1-.224 1-.5s-.448-.5-1-.5-1 .224-1 .5.448.5 1 .5"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/logos/mattermost_logo.svg b/app/assets/images/illustrations/logos/mattermost_logo.svg
new file mode 100644
index 00000000000..b577c0599aa
--- /dev/null
+++ b/app/assets/images/illustrations/logos/mattermost_logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" version="1" viewBox="0 0 501 501"><path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z"/><path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/monitoring/getting_started.svg b/app/assets/images/illustrations/monitoring/getting_started.svg
index db7a1c2e708..ff783bdd388 100644
--- a/app/assets/images/illustrations/monitoring/getting_started.svg
+++ b/app/assets/images/illustrations/monitoring/getting_started.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="0" width="159.8" height="127.81" x=".196" y="5" rx="10"/><rect id="2" width="160" height="128" x=".666" y=".41" rx="10"/><rect id="4" width="160.19" height="128.19" x=".339" y=".59" rx="10"/><mask id="1" width="159.8" height="127.81" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="160" height="128" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="5" width="160.19" height="128.19" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(12 3)"><rect width="160" height="128" x="122.08" y="146.08" fill="#f9f9f9" transform="matrix(.99619.08716-.08716.99619 19.08-16.813)" rx="10"/><g transform="matrix(.96593.25882-.25882.96593 227.1 57.47)"><rect width="159.8" height="127.81" x="1.64" y="10.06" fill="#f9f9f9" rx="8"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><g transform="translate(24.368 36.951)"><path fill="#d2caea" fill-rule="nonzero" d="m71.785 44.2c.761.296 1.625.099 2.184-.496l35.956-38.34c.756-.806.715-2.071-.091-2.827-.806-.756-2.071-.715-2.827.091l-35.03 37.36-41.888-16.285c-.749-.291-1.6-.106-2.16.471l-26.368 27.16c-.769.793-.751 2.059.042 2.828.793.769 2.059.751 2.828-.042l25.444-26.21 41.911 16.294"/><g fill="#fff"><circle cx="5.716" cy="5.104" r="5" stroke="#6b4fbb" stroke-width="4" transform="translate(65.917 34.945)"/><g stroke="#fb722e"><ellipse cx="4.632" cy="50.05" stroke-width="3.2" rx="4" ry="3.999"/><g stroke-width="4"><ellipse cx="29.632" cy="27.05" rx="4" ry="3.999"/><ellipse cx="107.63" cy="4.048" rx="4" ry="3.999"/></g></g></g></g></g><rect width="160.19" height="128.19" x="36.28" y="86.74" fill="#f9f9f9" transform="matrix(.99619-.08716.08716.99619-12.703 10.717)" rx="10"/><g transform="matrix(.99619.08716-.08716.99619 126.61 137.8)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#3)" xlink:href="#2"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width="3.2" d="m84.67 28.41c18.225 0 33 15.07 33 33.651h-33v-33.651" stroke-linecap="round" stroke-linejoin="round"/><path fill="#d2caea" fill-rule="nonzero" d="m78.67 66.41h30c1.105 0 2 .895 2 2 0 18.778-15.222 34-34 34-18.778 0-34-15.222-34-34 0-18.778 15.222-34 34-34 1.105 0 2 .895 2 2v30m-32 2c0 16.569 13.431 30 30 30 15.896 0 28.905-12.364 29.934-28h-29.934c-1.105 0-2-.895-2-2v-29.934c-15.636 1.029-28 14.04-28 29.934"/></g><g transform="matrix(.99619-.08716.08716.99619 30 88.03)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><g transform="translate(42 34)"><path fill="#fef0ea" d="m0 13.391c0-.768.628-1.391 1.4-1.391h9.2c.773 0 1.4.626 1.4 1.391v49.609h-12v-49.609"/><path fill="#fb722e" d="m66 21.406c0-.777.628-1.406 1.4-1.406h9.2c.773 0 1.4.624 1.4 1.406v41.594h-12v-41.594"/><path fill="#6b4fbb" d="m22 1.404c0-.776.628-1.404 1.4-1.404h9.2c.773 0 1.4.624 1.4 1.404v61.6h-12v-61.6"/><path fill="#d2caea" d="m44 39.4c0-.772.628-1.398 1.4-1.398h9.2c.773 0 1.4.618 1.4 1.398v23.602h-12v-23.602"/></g></g><g fill="#fee8dc"><path d="m6.226 94.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" transform="matrix(.70711.70711-.70711.70711 66.33 22.317)"/><path d="m312.78 53.43l-3.634.807c-1.296.288-2.115-.52-1.825-1.825l.807-3.634-.807-3.634c-.288-1.296.52-2.115 1.825-1.825l3.634.807 3.634-.807c1.296-.288 2.115.52 1.825 1.825l-.807 3.634.807 3.634c.288 1.296-.52 2.115-1.825 1.825l-3.634-.807" transform="matrix(.70711.70711-.70711.70711 126.1-206.88)"/></g><path fill="#e1dcf1" d="m124.78 12.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711 31.05 90.51)"/><path fill="#d2caea" d="m374.78 244.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711-59.779 335.24)"/></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="159.8" height="127.81" x=".196" y="5" rx="10"/><rect id="b" width="160" height="128" x=".666" y=".41" rx="10"/><rect id="c" width="160.19" height="128.19" x=".339" y=".59" rx="10"/><mask id="d" width="159.8" height="127.81" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><mask id="e" width="160" height="128" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><mask id="f" width="160.19" height="128.19" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(12 3)"><rect width="160" height="128" x="122.08" y="146.08" fill="#f9f9f9" transform="rotate(5 202.071 210.085)" rx="10"/><g transform="rotate(15 -104.714 891.23)"><rect width="159.8" height="127.81" x="1.64" y="10.06" fill="#f9f9f9" rx="8"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#d2caea" fill-rule="nonzero" d="M96.153 81.151a2.001 2.001 0 0 0 2.184-.496l35.956-38.34a2 2 0 1 0-2.918-2.736l-35.03 37.36-41.888-16.285a2 2 0 0 0-2.16.471l-26.368 27.16a2 2 0 1 0 2.87 2.786l25.444-26.21 41.911 16.294"/><g fill="#fff" transform="translate(24.368 36.951)"><circle cx="5.716" cy="5.104" r="5" stroke="#6b4fbb" stroke-width="4" transform="translate(65.917 34.945)"/><g stroke="#fb722e"><ellipse cx="4.632" cy="50.05" stroke-width="3.2" rx="4" ry="3.999"/><g stroke-width="4"><ellipse cx="29.632" cy="27.05" rx="4" ry="3.999"/><ellipse cx="107.63" cy="4.048" rx="4" ry="3.999"/></g></g></g></g><rect width="160.19" height="128.19" x="36.28" y="86.74" fill="#f9f9f9" transform="rotate(-5 116.372 150.825)" rx="10"/><g transform="rotate(5 -1514.687 1518.752)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#e)" xlink:href="#b"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width="3.2" d="M84.67 28.41c18.225 0 33 15.07 33 33.651h-33V28.41" stroke-linecap="round" stroke-linejoin="round"/><path fill="#d2caea" fill-rule="nonzero" d="M78.67 66.41h30a2 2 0 0 1 2 2c0 18.778-15.222 34-34 34s-34-15.222-34-34 15.222-34 34-34a2 2 0 0 1 2 2v30m-32 2c0 16.569 13.431 30 30 30 15.896 0 28.905-12.364 29.934-28H76.67a2 2 0 0 1-2-2V38.476c-15.636 1.029-28 14.04-28 29.934"/></g><g transform="rotate(-5 1023.06 -299.524)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#f)" xlink:href="#c"/><path fill="#fef0ea" d="M42 47.391c0-.768.628-1.391 1.4-1.391h9.2c.773 0 1.4.626 1.4 1.391V97H42V47.391"/><path fill="#fb722e" d="M108 55.406c0-.777.628-1.406 1.4-1.406h9.2a1.4 1.4 0 0 1 1.4 1.406V97h-12V55.406"/><path fill="#6b4fbb" d="M64 35.404c0-.776.628-1.404 1.4-1.404h9.2a1.4 1.4 0 0 1 1.4 1.404v61.6H64v-61.6"/><path fill="#d2caea" d="M86 73.4a1.4 1.4 0 0 1 1.4-1.398h9.2c.773 0 1.4.618 1.4 1.398v23.602H86V73.4"/></g><g fill="#fee8dc"><path d="M3.592 93.86l-2.454-1.562c-.93-.592-.924-1.554 0-2.143l2.454-1.562 1.562-2.454c.592-.93 1.554-.925 2.143 0l1.562 2.454 2.454 1.562c.93.591.924 1.554 0 2.143L8.86 93.86l-1.562 2.454c-.591.93-1.554.924-2.143 0L3.592 93.86M309.489 52.07l-3.14-1.998c-1.12-.713-1.128-1.863 0-2.581l3.14-2 1.999-3.14c.713-1.12 1.863-1.127 2.58 0l2 3.14 3.14 2c1.12.713 1.128 1.863 0 2.58l-3.14 2-2 3.14c-.712 1.12-1.862 1.128-2.58 0l-1.999-3.14"/></g><path fill="#e1dcf1" d="M128.073 11.066l-1.99 3.126c-.718 1.129-1.88 1.131-2.6 0l-1.99-3.126-3.126-1.989c-1.128-.718-1.13-1.88 0-2.6l3.127-1.99 1.989-3.126c.718-1.128 1.88-1.13 2.6 0l1.99 3.126 3.126 1.99c1.128.718 1.13 1.88 0 2.6l-3.126 1.99"/><path fill="#d2caea" d="M378.07 243.068l-1.989 3.126c-.718 1.129-1.88 1.131-2.6 0l-1.99-3.126-3.126-1.989c-1.128-.718-1.13-1.88 0-2.6l3.127-1.99 1.989-3.126c.718-1.128 1.88-1.13 2.6 0l1.99 3.126 3.126 1.99c1.128.718 1.13 1.88 0 2.6l-3.126 1.99"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/monitoring/loading.svg b/app/assets/images/illustrations/monitoring/loading.svg
index 6bbd7a6c5b9..1e196fc8ad1 100644
--- a/app/assets/images/illustrations/monitoring/loading.svg
+++ b/app/assets/images/illustrations/monitoring/loading.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="C" width="161" height="100" x="92" y="181" rx="10"/><rect id="E" width="151" height="32" x="20" rx="10"/><rect id="G" width="191" height="62" y="10" rx="10"/><circle id="I" cx="23" cy="41" r="9"/><circle id="4" cx="36.5" cy="36.5" r="36.5"/><circle id="8" cx="262.5" cy="169.5" r="15.5"/><circle id="A" cx="79.5" cy="169.5" r="15.5"/><circle id="K" cx="45" cy="41" r="9"/><circle id="0" cx="30.5" cy="30.5" r="30.5"/><circle id="2" cx="18" cy="34" r="3"/><ellipse id="6" cx="43.5" cy="43.5" rx="43.5" ry="43.5"/><mask id="H" width="191" height="62" x="0" y="0" fill="#fff"><use xlink:href="#G"/></mask><mask id="J" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#I"/></mask><mask id="D" width="161" height="100" x="0" y="0" fill="#fff"><use xlink:href="#C"/></mask><mask id="F" width="151" height="32" x="0" y="0" fill="#fff"><use xlink:href="#E"/></mask><mask id="9" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#8"/></mask><mask id="1" width="61" height="61" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="B" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#A"/></mask><mask id="3" width="6" height="6" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="7" width="87" height="87" x="0" y="0" fill="#fff"><use xlink:href="#6"/></mask><mask id="L" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#K"/></mask><mask id="5" width="73" height="73" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(28 2)"><g transform="translate(133 87)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><path stroke="#d2caea" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" d="m19 32l2-9 5 17 4-12 4 5 6-10 3 5"/><g fill="#fff" stroke="#fb722e"><use stroke-width="4" mask="url(#3)" xlink:href="#2"/><circle cx="44" cy="30" r="2" stroke-width="2"/></g></g><g transform="translate(188 29)"><circle cx="36.5" cy="41.5" r="36.5" fill="#f9f9f9"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><rect width="27" height="4" x="23" y="27" fill="#d2caea" rx="2"/><rect width="10.5" height="4" x="23" y="27" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="36" fill="#d2caea" rx="2"/><rect width="19" height="4" x="23" y="36" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="45" fill="#d2caea" rx="2"/><rect width="7" height="4" x="23" y="45" fill="#6b4fbb" rx="2"/></g><path fill="#eee" fill-rule="nonzero" d="m247 292v1c0 5.519-4.469 9.993-10.01 9.993h-125.99c-5.177 0-9.436-3.927-9.954-8.96 1.348.998 2.957 1.666 4.705 1.883 1.027 1.835 2.992 3.077 5.248 3.077h125.99c2.485 0 4.611-1.497 5.526-3.637 1.796-.675 3.347-1.852 4.48-3.359m1.947-8.962c-.518 5.03-4.774 8.958-9.95 8.958h-131.99c-4.929 0-9.03-3.563-9.851-8.25 1.382.767 2.964 1.216 4.649 1.248 1.037 1.794 2.978 3 5.202 3h131.99c2.255 0 4.219-1.241 5.245-3.076 1.748-.216 3.356-.883 4.705-1.882"/><g transform="translate(79)"><ellipse cx="43.5" cy="47.5" fill="#f9f9f9" rx="43.5" ry="43.5"/><g fill="#fff"><g stroke="#eee"><use stroke-width="8" mask="url(#7)" xlink:href="#6"/><path stroke-width="4" d="m18.595 49c2.515 11.44 12.71 20 24.905 20 14.08 0 25.5-11.417 25.5-25.5 0-12.195-8.56-22.391-20-24.905v15.959c3 1.848 5 5.164 5 8.946 0 5.799-4.701 10.5-10.5 10.5-3.782 0-7.098-2-8.946-5h-15.959" stroke-linejoin="round"/></g><path stroke="#d2caea" stroke-width="4" d="m18 44c-.003-.166-.005-.333-.005-.5 0-14.08 11.417-25.5 25.5-25.5.167 0 .334.002.5.005v15.01c-.166-.008-.332-.012-.5-.012-5.799 0-10.5 4.701-10.5 10.5 0 .168.004.334.012.5h-15.01" stroke-linejoin="round"/></g></g><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#9)" xlink:href="#8"/><use mask="url(#B)" xlink:href="#A"/><use mask="url(#D)" xlink:href="#C"/></g><g fill="#eee"><rect width="15" height="2" x="226" y="247" rx="1"/><rect width="15" height="2" x="226" y="242" rx="1"/><rect width="15" height="2" x="226" y="252" rx="1"/></g><rect width="10" height="52" x="118" y="196" fill="#d2caea" rx="2"/><rect width="10" height="47" x="154" y="196" fill="#6b4fbb" rx="2"/><rect width="10" height="37" x="190" y="196" fill="#d2caea" rx="2"/><g fill="#fee8dc"><rect width="10" height="52" x="132" y="185" rx="2"/><rect width="10" height="38" x="168" y="185" rx="2"/></g><rect width="10" height="58" x="204" y="185" fill="#fb722e" rx="2"/><g transform="translate(76 128)"><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#F)" xlink:href="#E"/><use mask="url(#H)" xlink:href="#G"/></g><g fill="#d2caea"><rect width="16" height="4" x="156" y="35" rx="2"/><rect width="16" height="4" x="156" y="43" rx="2"/></g><g fill="#fff" stroke-width="8"><use stroke="#fee8dc" mask="url(#J)" xlink:href="#I"/><use stroke="#fb722e" mask="url(#L)" xlink:href="#K"/></g></g><g fill="#fb722e"><path d="m6.226 220.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" opacity=".2" transform="matrix(.70711.70711-.70711.70711 155.43 59.22)"/><path d="m256.23 9.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" opacity=".2" transform="matrix(.70711.70711-.70711.70711 79.45-179.36)"/></g><path fill="#fee8dc" d="m312.78 150.43l-3.634.807c-1.296.288-2.115-.52-1.825-1.825l.807-3.634-.807-3.634c-.288-1.296.52-2.115 1.825-1.825l3.634.807 3.634-.807c1.296-.288 2.115.52 1.825 1.825l-.807 3.634.807 3.634c.288 1.296-.52 2.115-1.825 1.825l-3.634-.807" transform="matrix(.70711.70711-.70711.70711 194.69-178.47)"/><path fill="#6b4fbb" d="m43.778 80.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" opacity=".2" transform="matrix(.70711-.70711.70711.70711-40.761 53.15)"/></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="c" width="161" height="100" x="92" y="181" rx="10"/><rect id="d" width="151" height="32" x="20" rx="10"/><rect id="a" width="191" height="62" y="10" rx="10"/><circle id="b" cx="23" cy="41" r="9"/><circle id="k" cx="36.5" cy="36.5" r="36.5"/><circle id="e" cx="262.5" cy="169.5" r="15.5"/><circle id="g" cx="79.5" cy="169.5" r="15.5"/><circle id="j" cx="45" cy="41" r="9"/><circle id="f" cx="30.5" cy="30.5" r="30.5"/><circle id="h" cx="18" cy="34" r="3"/><ellipse id="i" cx="43.5" cy="43.5" rx="43.5" ry="43.5"/><mask id="t" width="191" height="62" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><mask id="u" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><mask id="r" width="161" height="100" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><mask id="s" width="151" height="32" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><mask id="p" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><mask id="l" width="61" height="61" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><mask id="q" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask><mask id="m" width="6" height="6" x="0" y="0" fill="#fff"><use xlink:href="#h"/></mask><mask id="o" width="87" height="87" x="0" y="0" fill="#fff"><use xlink:href="#i"/></mask><mask id="v" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#j"/></mask><mask id="n" width="73" height="73" x="0" y="0" fill="#fff"><use xlink:href="#k"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(28 2)"><g transform="translate(133 87)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#l)" xlink:href="#f"/><path stroke="#d2caea" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" d="M19 32l2-9 5 17 4-12 4 5 6-10 3 5"/><g fill="#fff" stroke="#fb722e"><use stroke-width="4" mask="url(#m)" xlink:href="#h"/><circle cx="44" cy="30" r="2" stroke-width="2"/></g></g><g transform="translate(188 29)"><circle cx="36.5" cy="41.5" r="36.5" fill="#f9f9f9"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#n)" xlink:href="#k"/><rect width="27" height="4" x="23" y="27" fill="#d2caea" rx="2"/><rect width="10.5" height="4" x="23" y="27" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="36" fill="#d2caea" rx="2"/><rect width="19" height="4" x="23" y="36" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="45" fill="#d2caea" rx="2"/><rect width="7" height="4" x="23" y="45" fill="#6b4fbb" rx="2"/></g><path fill="#eee" fill-rule="nonzero" d="M247 292v1c0 5.519-4.469 9.993-10.01 9.993H111c-5.177 0-9.436-3.927-9.954-8.96a9.96 9.96 0 0 0 4.705 1.883 6.008 6.008 0 0 0 5.248 3.077h125.99a6 6 0 0 0 5.526-3.637 10.027 10.027 0 0 0 4.48-3.359m1.947-8.962a10.001 10.001 0 0 1-9.95 8.958h-131.99a10 10 0 0 1-9.851-8.25 9.942 9.942 0 0 0 4.649 1.248 6 6 0 0 0 5.202 3h131.99a6.002 6.002 0 0 0 5.245-3.076 9.943 9.943 0 0 0 4.705-1.882"/><g transform="translate(79)"><ellipse cx="43.5" cy="47.5" fill="#f9f9f9" rx="43.5" ry="43.5"/><g fill="#fff"><g stroke="#eee"><use stroke-width="8" mask="url(#o)" xlink:href="#i"/><path stroke-width="4" d="M18.595 49C21.11 60.44 31.305 69 43.5 69 57.58 69 69 57.583 69 43.5c0-12.195-8.56-22.391-20-24.905v15.959c3 1.848 5 5.164 5 8.946C54 49.299 49.299 54 43.5 54c-3.782 0-7.098-2-8.946-5H18.595" stroke-linejoin="round"/></g><path stroke="#d2caea" stroke-width="4" d="M18 44a27.69 27.69 0 0 1-.005-.5c0-14.08 11.417-25.5 25.5-25.5.167 0 .334.002.5.005v15.01a10.365 10.365 0 0 0-.5-.012c-5.799 0-10.5 4.701-10.5 10.5 0 .168.004.334.012.5h-15.01" stroke-linejoin="round"/></g></g><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#p)" xlink:href="#e"/><use mask="url(#q)" xlink:href="#g"/><use mask="url(#r)" xlink:href="#c"/></g><g fill="#eee"><rect width="15" height="2" x="226" y="247" rx="1"/><rect width="15" height="2" x="226" y="242" rx="1"/><rect width="15" height="2" x="226" y="252" rx="1"/></g><rect width="10" height="52" x="118" y="196" fill="#d2caea" rx="2"/><rect width="10" height="47" x="154" y="196" fill="#6b4fbb" rx="2"/><rect width="10" height="37" x="190" y="196" fill="#d2caea" rx="2"/><g fill="#fee8dc"><rect width="10" height="52" x="132" y="185" rx="2"/><rect width="10" height="38" x="168" y="185" rx="2"/></g><rect width="10" height="58" x="204" y="185" fill="#fb722e" rx="2"/><g fill="#fff" stroke="#eee" stroke-width="8" transform="translate(76 128)"><use mask="url(#s)" xlink:href="#d"/><use mask="url(#t)" xlink:href="#a"/></g><g fill="#d2caea" transform="translate(76 128)"><rect width="16" height="4" x="156" y="35" rx="2"/><rect width="16" height="4" x="156" y="43" rx="2"/></g><g fill="#fff" stroke-width="8" transform="translate(76 128)"><use stroke="#fee8dc" mask="url(#u)" xlink:href="#b"/><use stroke="#fb722e" mask="url(#v)" xlink:href="#j"/></g><g fill="#fb722e"><path d="M3.597 219.858l-2.455-1.562c-.929-.59-.924-1.553 0-2.142l2.455-1.562 1.562-2.455c.59-.929 1.553-.924 2.142 0l1.562 2.455 2.454 1.562c.93.591.925 1.553 0 2.142l-2.454 1.562-1.562 2.455c-.591.929-1.553.924-2.142 0l-1.562-2.455M253.597 8.859l-2.454-1.562c-.93-.592-.925-1.554 0-2.143l2.454-1.562 1.562-2.454c.591-.93 1.554-.925 2.143 0l1.562 2.454 2.454 1.562c.93.591.924 1.554 0 2.143l-2.454 1.562-1.562 2.454c-.592.93-1.554.924-2.143 0l-1.562-2.454" opacity=".2"/></g><path fill="#fee8dc" d="M309.49 149.07l-3.141-1.999c-1.12-.712-1.128-1.863 0-2.58l3.14-2 2-3.14c.712-1.12 1.863-1.128 2.58 0l2 3.14 3.14 2c1.12.712 1.127 1.863 0 2.58l-3.14 2-2 3.14c-.713 1.12-1.863 1.128-2.58 0l-2-3.14"/><path fill="#6b4fbb" d="M47.068 79.067l-1.99 3.126c-.718 1.129-1.88 1.13-2.6 0l-1.99-3.126-3.125-1.99c-1.129-.718-1.131-1.88 0-2.6l3.126-1.989 1.989-3.126c.718-1.129 1.88-1.13 2.6 0l1.99 3.126 3.126 1.99c1.128.718 1.13 1.88 0 2.6l-3.126 1.989" opacity=".2"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/monitoring/unable_to_connect.svg b/app/assets/images/illustrations/monitoring/unable_to_connect.svg
index 62537d87d5d..314c052f931 100644
--- a/app/assets/images/illustrations/monitoring/unable_to_connect.svg
+++ b/app/assets/images/illustrations/monitoring/unable_to_connect.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><use id="0" xlink:href="#E"/><use id="2" xlink:href="#E"/><use id="4" xlink:href="#E"/><path id="6" d="m74 93h26v47h-26z"/><path id="8" d="m74 93h26v47h-26z"/><rect id="A" width="65" height="14" x="55" y="135" rx="4"/><rect id="C" width="175" height="118" rx="10"/><rect id="E" width="159" rx="10" height="56"/><rect id="F" width="160" y="2" rx="10" height="56" fill="#f9f9f9"/><mask id="B" width="65" height="14" x="0" y="0" fill="#fff"><use xlink:href="#A"/></mask><mask id="9" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#8"/></mask><mask id="D" width="175" height="118" x="0" y="0" fill="#fff"><use xlink:href="#C"/></mask><mask id="7" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#6"/></mask><mask id="3" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="1" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="5" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(1 65)"><g transform="translate(244)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><g fill-rule="nonzero"><path fill="#fb722e" d="m134 31c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m117 31c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6m-17-4c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><g fill="#d2caea"><rect width="50" height="4" x="19" y="20" rx="2"/><rect width="50" height="4" x="19" y="34" rx="2"/></g><g transform="translate(0 59)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#3)" xlink:href="#2"/><g fill-rule="nonzero"><path fill="#fee8dc" d="m134 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fb722e" d="m117 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m100 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><rect width="50" height="4" x="19" y="19" fill="#d2caea" rx="2" id="G"/><rect width="50" height="4" x="19" y="33" fill="#d2caea" rx="2" id="H"/></g><g transform="translate(0 118)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><g fill-rule="nonzero"><path fill="#fb722e" d="m134 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m117 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6m-17-4c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><use xlink:href="#G"/><use xlink:href="#H"/></g></g><g transform="translate(163 55)"><g fill="#eee"><rect width="29" height="4" y="29" rx="2"/><rect width="28" height="4" x="55" y="29" rx="2"/></g><g transform="translate(16)"><circle cx="30" cy="30" r="24" fill="#fef0ea"/><g fill="#fb722e"><circle cx="30.5" cy="30.5" r="30.5" opacity=".1"/><circle cx="30.5" cy="30.5" r="19.5" opacity=".1"/></g><circle cx="30.5" cy="30.5" r="13.5" fill="#fff"/><path fill="#fb722e" d="m32.621 30.5l2.481-2.481c.586-.586.58-1.529-.006-2.115-.59-.59-1.533-.589-2.115-.006l-2.481 2.481-2.481-2.481c-.586-.586-1.529-.58-2.115.006-.59.59-.589 1.533-.006 2.115l2.481 2.481-2.481 2.481c-.586.586-.58 1.529.006 2.115.59.59 1.533.589 2.115.006l2.481-2.481 2.481 2.481c.586.586 1.529.58 2.115-.006.59-.59.589-1.533.006-2.115l-2.481-2.481"/></g></g><g transform="translate(0 13)"><rect width="65" height="14" x="55" y="137" fill="#f9f9f9" rx="4"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#7)" xlink:href="#6"/><rect width="175" height="118" y="3" fill="#f9f9f9" rx="10"/><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#9)" xlink:href="#8"/><use mask="url(#B)" xlink:href="#A"/><use mask="url(#D)" xlink:href="#C"/></g><g fill-rule="nonzero"><path fill="#eee" d="m163 105v-93h-152v93h152m-156-93.01c0-2.204 1.797-3.99 3.995-3.99h152.01c2.206 0 3.995 1.796 3.995 3.99v93.02c0 2.204-1.797 3.99-3.995 3.99h-152.01c-2.206 0-3.995-1.796-3.995-3.99v-93.02"/><path fill="#d2caea" d="m86 92c-11.598 0-21-9.402-21-21 0-11.598 9.402-21 21-21 11.598 0 21 9.402 21 21 0 11.598-9.402 21-21 21m0-4c9.389 0 17-7.611 17-17 0-9.389-7.611-17-17-17-9.389 0-17 7.611-17 17 0 9.389 7.611 17 17 17"/></g><path fill="#6b4fbb" d="m83 63c0-1.659 1.347-3 3-3 1.657 0 3 1.342 3 3v7.993c0 1.659-1.347 3-3 3-1.657 0-3-1.342-3-3v-7.993m3 18.997c-1.657 0-3-1.343-3-3 0-1.657 1.343-3 3-3 1.657 0 3 1.343 3 3 0 1.657-1.343 3-3 3"/><g fill="#eee"><rect width="134" height="4" x="20" y="30" rx="2"/><rect width="14" height="4" x="20" y="20" rx="2"/><circle cx="87" cy="21" r="5"/></g></g></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><use id="g" xlink:href="#a"/><use id="f" xlink:href="#a"/><use id="h" xlink:href="#a"/><path id="e" d="M74 93h26v47H74z"/><path id="c" d="M74 93h26v47H74z"/><rect id="b" width="65" height="14" x="55" y="135" rx="4"/><rect id="d" width="175" height="118" rx="10"/><rect id="a" width="159" rx="10" height="56"/><rect id="i" width="160" y="2" rx="10" height="56" fill="#f9f9f9"/><mask id="q" width="65" height="14" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><mask id="p" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><mask id="r" width="175" height="118" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask><mask id="o" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#e"/></mask><mask id="k" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#f"/></mask><mask id="j" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#g"/></mask><mask id="l" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#h"/></mask></defs><g fill="none" fill-rule="evenodd"><g transform="translate(245 65)"><use xlink:href="#i"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#j)" xlink:href="#g"/><g fill-rule="nonzero"><path fill="#fb722e" d="M134 31a2 2 0 1 0 .001-3.999A2 2 0 0 0 134 31m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/><path fill="#fee8dc" d="M117 31a2 2 0 1 0 .001-3.999A2 2 0 0 0 117 31m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12m-17-4a2 2 0 1 0 .001-3.999A2 2 0 0 0 100 31m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/></g><g fill="#d2caea"><rect width="50" height="4" x="19" y="20" rx="2"/><rect width="50" height="4" x="19" y="34" rx="2"/></g><g transform="translate(0 59)"><use xlink:href="#i"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#k)" xlink:href="#f"/><g fill-rule="nonzero"><path fill="#fee8dc" d="M134 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 134 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/><path fill="#fb722e" d="M117 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 117 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/><path fill="#fee8dc" d="M100 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 100 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/></g><rect width="50" height="4" x="19" y="19" fill="#d2caea" rx="2" id="m"/><rect width="50" height="4" x="19" y="33" fill="#d2caea" rx="2" id="n"/></g><g transform="translate(0 118)"><use xlink:href="#i"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#l)" xlink:href="#h"/><g fill-rule="nonzero"><path fill="#fb722e" d="M134 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 134 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/><path fill="#fee8dc" d="M117 30a2 2 0 1 0 .001-3.999A2 2 0 0 0 117 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12m-17-4a2 2 0 1 0 .001-3.999A2 2 0 0 0 100 30m0 4a6 6 0 1 1 0-12 6 6 0 0 1 0 12"/></g><use xlink:href="#m"/><use xlink:href="#n"/></g></g><g fill="#eee" transform="translate(164 120)"><rect width="29" height="4" y="29" rx="2"/><rect width="28" height="4" x="55" y="29" rx="2"/></g><g transform="translate(180 120)"><circle cx="30" cy="30" r="24" fill="#fef0ea"/><g fill="#fb722e"><circle cx="30.5" cy="30.5" r="30.5" opacity=".1"/><circle cx="30.5" cy="30.5" r="19.5" opacity=".1"/></g><circle cx="30.5" cy="30.5" r="13.5" fill="#fff"/><path fill="#fb722e" d="M32.621 30.5l2.481-2.481a1.492 1.492 0 0 0-.006-2.115 1.491 1.491 0 0 0-2.115-.006L30.5 28.379l-2.481-2.481a1.492 1.492 0 0 0-2.115.006 1.491 1.491 0 0 0-.006 2.115l2.481 2.481-2.481 2.481a1.492 1.492 0 0 0 .006 2.115c.59.59 1.533.589 2.115.006l2.481-2.481 2.481 2.481c.586.586 1.529.58 2.115-.006.59-.59.589-1.533.006-2.115L32.621 30.5"/></g><g transform="translate(1 78)"><rect width="65" height="14" x="55" y="137" fill="#f9f9f9" rx="4"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#o)" xlink:href="#e"/><rect width="175" height="118" y="3" fill="#f9f9f9" rx="10"/><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#p)" xlink:href="#c"/><use mask="url(#q)" xlink:href="#b"/><use mask="url(#r)" xlink:href="#d"/></g><g fill-rule="nonzero"><path fill="#eee" d="M163 105V12H11v93h152M7 11.99A3.998 3.998 0 0 1 10.995 8h152.01A3.999 3.999 0 0 1 167 11.99v93.02a3.998 3.998 0 0 1-3.995 3.99H10.995A3.999 3.999 0 0 1 7 105.01V11.99"/><path fill="#d2caea" d="M86 92c-11.598 0-21-9.402-21-21s9.402-21 21-21 21 9.402 21 21-9.402 21-21 21m0-4c9.389 0 17-7.611 17-17s-7.611-17-17-17-17 7.611-17 17 7.611 17 17 17"/></g><path fill="#6b4fbb" d="M83 63a3.001 3.001 0 0 1 6 0v7.993a3.001 3.001 0 0 1-6 0V63m3 18.997a3 3 0 1 1 0-6 3 3 0 0 1 0 6"/><g fill="#eee"><rect width="134" height="4" x="20" y="30" rx="2"/><rect width="14" height="4" x="20" y="20" rx="2"/><circle cx="87" cy="21" r="5"/></g></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/no_commits.svg b/app/assets/images/illustrations/no_commits.svg
new file mode 100644
index 00000000000..76fa25156dd
--- /dev/null
+++ b/app/assets/images/illustrations/no_commits.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="M4.01 2h1.102a1 1 0 0 0 0-2H4.01A4.001 4.001 0 0 0 0 4a1 1 0 0 0 2 0c0-1.108.892-2 2.01-2m12.702 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7m11.6 0a1 1 0 0 0 0-2h-5.7a1 1 0 0 0 0 2h5.7M164 2c.822 0 1.554.503 1.86 1.254a1 1 0 1 0 1.853-.753 4.01 4.01 0 0 0-3.712-2.5h-2.188a1 1 0 0 0 0 2h2.188m2.01 12.518a1 1 0 0 0 2 0v-5.7a1 1 0 0 0-2 0v5.7m0 11.6a1 1 0 0 0 2 0v-5.7a1 1 0 0 0-2 0v5.7m0 11.6a1 1 0 0 0 2 0v-5.7a1 1 0 0 0-2 0v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72a1 1 0 0 0 0 2h.72a4.001 4.001 0 0 0 4.01-4v-.382a1 1 0 0 0-2 0v.382m-14.325 2a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-11.6 0a1 1 0 0 0 0 2h5.7a1 1 0 0 0 0-2h-5.7m-8.47 0a2.01 2.01 0 0 1-1.782-1.085 1 1 0 0 0-1.775.923 4.007 4.007 0 0 0 3.556 2.162h2.57a1 1 0 0 0 0-2h-2.57m-2.01-12.136a1 1 0 0 0-2 0v5.7a1 1 0 0 0 2 0v-5.7m0-11.6a1 1 0 0 0-2 0v5.7a1 1 0 0 0 2 0v-5.7m0-11.6a1 1 0 0 0-2 0v5.7a1 1 0 0 0 2 0v-5.7m0-6.664a1 1 0 0 0-2 0v.764a1 1 0 0 0 2 0v-.764" id="a"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="b"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="c"/><path d="M131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9a.998.998 0 0 0-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01a2.998 2.998 0 0 1 2.996 2.999v9a3.003 3.003 0 0 1-2.996 2.999h-22.01A2.998 2.998 0 0 1 129 28.999v-9A3.003 3.003 0 0 1 131.996 17" id="d"/><g transform="translate(0 59)"><use xlink:href="#a"/><circle cx="21" cy="24" r="10"/><use xlink:href="#b"/><use xlink:href="#c"/><use xlink:href="#d"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/pipelines_pending.svg b/app/assets/images/illustrations/pipelines_pending.svg
new file mode 100644
index 00000000000..25038366e92
--- /dev/null
+++ b/app/assets/images/illustrations/pipelines_pending.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="430" height="220" viewBox="0 0 430 220"><g fill="none" fill-rule="evenodd"><path fill="#EEE" fill-rule="nonzero" d="M189.8 182l2.4-12H114c-5.523 0-10-4.477-10-10V34c0-5.523 4.477-10 10-10h200c5.523 0 10 4.477 10 10v126c0 5.523-4.477 10-10 10h-78.2l2.4 12h22.52a9.651 9.651 0 0 1 9.28 7 5.491 5.491 0 0 1-5.28 7H164.159a5.787 5.787 0 0 1-5.659-7 8.855 8.855 0 0 1 8.659-7H189.8zM114 28a6 6 0 0 0-6 6v126a6 6 0 0 0 6 6h200a6 6 0 0 0 6-6V34a6 6 0 0 0-6-6H114zm5 6h190a5 5 0 0 1 5 5v116a5 5 0 0 1-5 5H119a5 5 0 0 1-5-5V39a5 5 0 0 1 5-5zm0 4a1 1 0 0 0-1 1v116a1 1 0 0 0 1 1h190a1 1 0 0 0 1-1V39a1 1 0 0 0-1-1H119zm112.72 132h-35.44l-2.4 12h40.24l-2.4-12zm-64.561 16c-2.29 0-4.268 1.6-4.748 3.838A1.787 1.787 0 0 0 164.16 192h100.56a1.491 1.491 0 0 0 1.435-1.901A5.651 5.651 0 0 0 260.72 186h-93.561z"/><path fill="#FEF0E8" d="M177.965 99H194a2 2 0 1 1 0 4h-16.322c-1.374 6.29-6.976 11-13.678 11-6.702 0-12.304-4.71-13.678-11h-3.365l-7.395 9.249a2 2 0 0 1-3.049.089L128.11 103h-5.844a2 2 0 1 1 0-4H129a2 2 0 0 1 1.487.662l7.423 8.248 6.523-8.159a2 2 0 0 1 1.562-.751h4.04c.513-7.265 6.57-13 13.965-13 7.396 0 13.452 5.735 13.965 13zM164 110c5.523 0 10-4.477 10-10s-4.477-10-10-10-10 4.477-10 10 4.477 10 10 10z"/><path fill="#EFEDF8" d="M273.847 103c-.962 6.23-6.347 11-12.847 11-6.5 0-11.885-4.77-12.847-11H232a2 2 0 0 1 0-4h16.153c.962-6.23 6.347-11 12.847-11 6.5 0 11.885 4.77 12.847 11h3.998l8.404-9.338a2 2 0 0 1 3.048.09L296.692 99H305a2 2 0 0 1 0 4h-9.27a2 2 0 0 1-1.562-.751l-6.523-8.16-7.423 8.249a2 2 0 0 1-1.487.662h-4.888zM261 110a9 9 0 1 0 0-18 9 9 0 0 0 0 18z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M213 119c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19zm0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15z"/><path fill="#FC6D26" d="M211.586 101.828L208.757 99a2 2 0 1 0-2.828 2.828l4.243 4.243c.39.39.902.586 1.414.586.512 0 1.023-.195 1.414-.586L220.071 99a2 2 0 1 0-2.828-2.828l-5.657 5.656z"/><path fill="#FDC4A8" d="M162.95 101.07l-1.768-1.767a1.5 1.5 0 0 0-2.121 2.121l2.828 2.829c.293.293.677.439 1.06.439.385 0 .769-.146 1.062-.44l4.242-4.242a1.5 1.5 0 1 0-2.121-2.121l-3.182 3.182z"/><path fill="#6B4FBB" d="M256.39 104.841A6 6 0 1 0 261 95v6l-4.61 3.841z"/><path fill="#FEF0E8" fill-rule="nonzero" d="M99 99h-5a2 2 0 1 0 0 4h5a2 2 0 1 0 0-4zm-16 0h-5a2 2 0 1 0 0 4h5a2 2 0 1 0 0-4zm-14.384-.078l-3.643-3.425a2 2 0 1 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zm-11.657-10.96l-3.642-3.425a2 2 0 1 0-2.74 2.914l3.642 3.425a2 2 0 0 0 2.74-2.914zm-11.656-10.96l-3.643-3.425a2 2 0 0 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zm-14.367-3.885l-3.593 3.477a2 2 0 0 0 2.782 2.875l3.593-3.477a2 2 0 0 0-2.782-2.875zM19.44 84.244l-3.593 3.477a2 2 0 1 0 2.781 2.874l3.593-3.477a2 2 0 0 0-2.781-2.874zM7.94 95.371l-3.593 3.477a2 2 0 1 0 2.782 2.874l3.593-3.477a2 2 0 1 0-2.782-2.874z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M423.611 99.56l-3.598 3.472a2 2 0 0 0 2.777 2.879l3.599-3.472a2 2 0 0 0-2.778-2.878zm-11.514 11.11l-3.598 3.472a2 2 0 0 0 2.777 2.878l3.598-3.471a2 2 0 0 0-2.777-2.879zm-11.514 11.11l-3.599 3.471a2 2 0 1 0 2.778 2.879l3.598-3.472a2 2 0 1 0-2.777-2.879zm-8.799 4.48l-3.642-3.426a2 2 0 0 0-2.74 2.915l3.642 3.425a2 2 0 0 0 2.74-2.915zm-11.656-10.96l-3.643-3.426a2 2 0 1 0-2.74 2.914l3.643 3.426a2 2 0 1 0 2.74-2.915zm-11.657-10.96l-3.643-3.426a2 2 0 1 0-2.74 2.914l3.643 3.425a2 2 0 1 0 2.74-2.914zM353.001 99h-5a2 2 0 1 0 0 4h5a2 2 0 0 0 0-4zm-16 0h-5a2 2 0 1 0 0 4h5a2 2 0 0 0 0-4z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/slack_logo.svg b/app/assets/images/illustrations/slack_logo.svg
new file mode 100644
index 00000000000..b8d7906c2e1
--- /dev/null
+++ b/app/assets/images/illustrations/slack_logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" viewBox="0 0 121.94154 121.84154" width="121.942" height="121.842"><style id="style200">.st0{fill:#ecb32d}.st1{fill:#63c1a0}.st2{fill:#e01a59}.st3{fill:#331433}.st4{fill:#d62027}.st5{fill:#89d3df}.st6{fill:#258b74}.st7{fill:#819c3c}</style><path class="st0" d="M79.03 7.511c-1.9-5.7-8-8.8-13.7-7-5.7 1.9-8.8 8-7 13.7l28.1 86.4c1.9 5.3 7.7 8.3 13.2 6.7 5.8-1.7 9.3-7.8 7.4-13.4 0-.2-28-86.4-28-86.4z" id="path202" fill="#ecb32d"/><path class="st1" d="M35.53 21.611c-1.9-5.7-8-8.8-13.7-7-5.7 1.9-8.8 8-7 13.7l28.1 86.4c1.9 5.3 7.7 8.3 13.2 6.7 5.8-1.7 9.3-7.8 7.4-13.4 0-.2-28-86.4-28-86.4z" id="path204" fill="#63c1a0"/><path class="st2" d="M114.43 79.011c5.7-1.9 8.8-8 7-13.7-1.9-5.7-8-8.8-13.7-7l-86.5 28.2c-5.3 1.9-8.3 7.7-6.7 13.2 1.7 5.8 7.8 9.3 13.4 7.4.2 0 86.5-28.1 86.5-28.1z" id="path206" fill="#e01a59"/><path class="st3" d="M39.23 103.511c5.6-1.8 12.9-4.2 20.7-6.7-1.8-5.6-4.2-12.9-6.7-20.7l-20.7 6.7z" id="path208" fill="#331433"/><path class="st4" d="M82.83 89.311c7.8-2.5 15.1-4.9 20.7-6.7-1.8-5.6-4.2-12.9-6.7-20.7l-20.7 6.7z" id="path210" fill="#d62027"/><path class="st5" d="M100.23 35.511c5.7-1.9 8.8-8 7-13.7-1.9-5.7-8-8.8-13.7-7l-86.4 28.1c-5.3 1.9-8.3 7.7-6.7 13.2 1.7 5.8 7.8 9.3 13.4 7.4.2 0 86.4-28 86.4-28z" id="path212" fill="#89d3df"/><path class="st6" d="M25.13 59.911c5.6-1.8 12.9-4.2 20.7-6.7-2.5-7.8-4.9-15.1-6.7-20.7l-20.7 6.7z" id="path214" fill="#258b74"/><path class="st7" d="M68.63 45.811c7.8-2.5 15.1-4.9 20.7-6.7-2.5-7.8-4.9-15.1-6.7-20.7l-20.7 6.7z" id="path216" fill="#819c3c"/></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/add_new_group.svg b/app/assets/images/illustrations/welcome/add_new_group.svg
new file mode 100644
index 00000000000..b10a3ae8812
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/add_new_group.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M59.65 32.65H60l-2-2.42-2 2.4-2-2.4-2 2.4-2-2.4-2 2.4-2-2.4-2 2.42h.77C45.57 34.6 46 36.75 46 39c0 2.84-.7 5.5-1.92 7.86 1.97 2.28 4.83 3.64 7.92 3.64 5.8 0 10.5-4.74 10.5-10.6 0-2.8-1.08-5.36-2.85-7.25zM43.18 29.6c2.4-2.1 5.52-3.3 8.82-3.3 7.46 0 13.5 6.1 13.5 13.6S59.46 53.5 52 53.5c-3.68 0-7.1-1.5-9.6-4.04C39.3 53.44 34.44 56 29 56c-9.4 0-17-7.6-17-17s7.6-17 17-17c3.22 0 6.23.9 8.8 2.45 2.13 1.3 3.97 3.05 5.38 5.16zM17 34c-.65 1.54-1 3.23-1 5 0 7.18 5.82 13 13 13s13-5.82 13-13c0-1.77-.35-3.46-1-5h-9c-.53 0-1.04-.2-1.4-.6L29 31.84l-1.6 1.58c-.36.4-.87.6-1.4.6h-9zm21.38-4a12.996 12.996 0 0 0-18.76 0h5.55l2.42-2.4c.74-.8 2-.8 2.8 0l2.4 2.4h5.54z"/><path fill="#6B4FBB" d="M47.6 42.32c-.66 0-1.2-.54-1.2-1.2 0-.68.54-1.22 1.2-1.22.66 0 1.2.54 1.2 1.2 0 .68-.54 1.22-1.2 1.22zm8.8 0c-.66 0-1.2-.54-1.2-1.2 0-.68.54-1.22 1.2-1.22.66 0 1.2.54 1.2 1.2 0 .68-.54 1.22-1.2 1.22zM25 44h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-1c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/add_new_project.svg b/app/assets/images/illustrations/welcome/add_new_project.svg
new file mode 100644
index 00000000000..4b8dc34c088
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/add_new_project.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 24c-2.21 0-4 1.79-4 4v22c0 2.21 1.79 4 4 4h18c2.21 0 4-1.79 4-4V28c0-2.21-1.79-4-4-4H30zm0-4h18a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V28a8 8 0 0 1 8-8z"/><path fill="#6B4FBB" d="M33 30h8a2 2 0 1 1 0 4h-8a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/add_new_user.svg b/app/assets/images/illustrations/welcome/add_new_user.svg
new file mode 100644
index 00000000000..d4c184989bf
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/add_new_user.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" d="M44 31l-2.5-3-2.5 3-2.5-3-2.5 3-2.5-3-2.5 3h-2.72c2.65-4.2 7.36-7 12.72-7s10.07 2.8 12.72 7H49l-2.5-3-2.5 3z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M39 57c-9.4 0-17-7.6-17-17s7.6-17 17-17 17 7.6 17 17-7.6 17-17 17zm0-4c7.18 0 13-5.82 13-13s-5.82-13-13-13-13 5.82-13 13 5.82 13 13 13z"/><path fill="#6B4FBB" d="M35 45h8c0 2.2-1.8 4-4 4s-4-1.8-4-4zm-1.5-2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm11 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/configure_server.svg b/app/assets/images/illustrations/welcome/configure_server.svg
new file mode 100644
index 00000000000..f9dda816f11
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/configure_server.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M2.12 42C2.04 43 2 44 2 45c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 61.03 58.42 76 39 76S3.65 61.03 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M24.92 35.15a4.012 4.012 0 0 1-.6-5.63l1.26-1.55c1.4-1.72 3.9-2 5.63-.6l.7.56c.7-.4 1.4-.73 2.1-1V26c0-2.2 1.8-4 4-4h2c2.2 0 4 1.8 4 4v.92c.8.28 1.5.62 2.1 1l.7-.55c1.7-1.4 4.3-1.12 5.7.6l1.3 1.55c1.4 1.72 1.2 4.23-.6 5.63l-.7.6c.3.74.4 1.5.5 2.3l.9.2c2.2.5 3.5 2.64 3 4.8L56.4 45c-.5 2.15-2.64 3.5-4.8 3l-.88-.2c-.44.63-.92 1.24-1.46 1.8l.4.82c.9 1.98.1 4.38-1.9 5.35l-1.8.87c-2 .97-4.37.15-5.34-1.84l-.46-.85c-.34.03-.74.05-1.13.05-.4 0-.8-.02-1.2-.05l-.4.85c-.95 2-3.34 2.8-5.33 1.84l-1.8-.87a4.011 4.011 0 0 1-1.83-5.35l.4-.8c-.54-.58-1.02-1.2-1.46-1.83l-.8.2c-2.2.5-4.3-.9-4.8-3l-.4-2c-.5-2.2.85-4.3 3-4.8l.9-.2c.1-.8.3-1.6.5-2.3l-.7-.6zm4.95.77c-.53 1.2-.83 2.47-.87 3.8-.02.9-.66 1.68-1.55 1.9l-2.32.53.45 1.94 2.3-.6c.9-.2 1.8.2 2.23 1 .7 1.1 1.5 2.2 2.5 3 .7.6.9 1.6.5 2.4l-1 2.1 1.8.9 1.1-2.1c.4-.8 1.3-1.3 2.2-1.1.7.1 1.3.2 2 .2s1.3-.1 2-.2c.9-.2 1.8.3 2.2 1.1l1 2.1 1.8-.9-1.2-2c-.4-.8-.2-1.8.5-2.4 1-.85 1.84-1.88 2.45-3.05.4-.82 1.33-1.24 2.2-1.04l2.33.54.45-1.95-2.32-.54c-.9-.2-1.52-.97-1.54-1.88-.03-1.4-.33-2.6-.86-3.8-.4-.9-.2-1.8.5-2.4l1.9-1.5-1.3-1.6-1.8 1.5c-.8.5-1.8.6-2.5 0-1.1-.8-2.3-1.4-3.5-1.7-.9-.2-1.5-1-1.5-1.9V26h-2v2.38c0 .9-.6 1.7-1.5 1.93-1.3.4-2.5 1-3.5 1.7-.8.6-1.8.6-2.5 0l-1.9-1.5-1.26 1.6 1.8 1.5c.7.6.94 1.6.6 2.4z"/><path fill="#FC6D26" fill-rule="nonzero" d="M39 46c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/ee_trial.svg b/app/assets/images/illustrations/welcome/ee_trial.svg
new file mode 100644
index 00000000000..6d0dcf0020c
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/ee_trial.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="330" height="132" viewBox="0 0 330 132"><g fill="none" fill-rule="evenodd"><path fill="#000" fill-opacity=".03" d="M174.12 42c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M211 78c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S230.33 4 211 4s-35 15.67-35 35 15.67 35 35 35z"/><g fill-rule="nonzero"><path fill="#FEE1D3" d="M211.5 51c-6.42 0-12.26-2.84-17.43-8.4a4.008 4.008 0 0 1-.27-5.13C199 30.57 204.92 27 211.5 27s12.5 3.56 17.7 10.47a3.994 3.994 0 0 1-.27 5.12c-5.17 5.53-11 8.4-17.43 8.4zm0-4c5.25 0 10.05-2.34 14.5-7.13-4.5-5.98-9.3-8.87-14.5-8.87-5.2 0-10 2.9-14.5 8.87 4.45 4.8 9.25 7.13 14.5 7.13z"/><path fill="#FC6D26" d="M211 47c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-4c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4zm0-1c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/></g><path fill="#000" fill-opacity=".03" d="M88.12 83c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M125 119c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M116 86.34c2.33.83 4 3.05 4 5.66 0 3.3-2.7 6-6 6s-6-2.7-6-6c0-2.6 1.67-4.83 4-5.66V72h4v14.34zM128 66c5.52 0 10 4.48 10 10v12h-4V76c0-3.3-2.7-6-6-6v1.83c0 .55-.45 1-1 1-.24 0-.47-.1-.65-.24l-4.46-3.87c-.46-.36-.5-1-.15-1.4.03-.05.07-.1.1-.12l4.47-3.82c.42-.35 1.05-.3 1.4.1.16.2.25.43.25.66V66zm-14 28c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/><path fill="#FC6D26" fill-rule="nonzero" d="M114 74c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm22 28c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm0-4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/><path fill="#000" fill-opacity=".03" d="M2.12 52C2.04 53 2 54 2 55c0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3C74.35 71.03 58.42 86 39 86S3.65 71.03 2.12 52z"/><path fill="#EEE" fill-rule="nonzero" d="M39 88C17.46 88 0 70.54 0 49s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 14 39 14 4 29.67 4 49s15.67 35 35 35z"/><path fill="#6B4FBB" fill-rule="nonzero" d="M48 41h-4c0-2.76-2.24-5-5-5s-5 2.24-5 5h-4a9 9 0 0 1 18 0zm-18 0h4v3h-4v-3zm14 0h4v3h-4v-3z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 47c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V48c0-.55-.45-1-1-1H30zm0-4h18c2.76 0 5 2.24 5 5v12c0 2.76-2.24 5-5 5H30c-2.76 0-5-2.24-5-5V48c0-2.76 2.24-5 5-5z"/><path fill="#6B4FBB" d="M38 53.73c-.6-.34-1-1-1-1.73 0-1.1.9-2 2-2s2 .9 2 2c0 .74-.4 1.4-1 1.73V55c0 .55-.45 1-1 1s-1-.45-1-1v-1.27z"/><path fill="#000" fill-opacity=".03" d="M254.12 92c-.08 1-.12 2-.12 3 0 20.43 16.57 37 37 37s37-16.57 37-37c0-1-.04-2-.12-3-1.53 19.03-17.46 34-36.88 34s-35.35-14.97-36.88-34z"/><path fill="#EEE" fill-rule="nonzero" d="M291 128c-21.54 0-39-17.46-39-39s17.46-39 39-39 39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35s-15.67-35-35-35-35 15.67-35 35 15.67 35 35 35z"/><path fill="#6B4BBE" fill-rule="nonzero" d="M292 78c5.52 0 10 4.48 10 10 0 2.28-.76 4.43-2.14 6.18-1.03 1.3-.8 3.2.5 4.22 1.3 1.02 3.2.8 4.2-.5 2.22-2.8 3.44-6.26 3.44-9.9 0-8.84-7.16-16-16-16v-3.13c0-.2-.06-.4-.17-.56-.3-.42-.93-.54-1.38-.23l-9.2 6.13c-.1.06-.2.16-.28.27-.3.45-.18 1.08.28 1.38l9.2 6.13c.16.1.35.17.55.17.55 0 1-.45 1-1V78z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M290 100c-5.52 0-10-4.48-10-10 0-2.25.74-4.38 2.1-6.12 1-1.3.77-3.2-.54-4.2-1.3-1.02-3.2-.78-4.2.53A15.796 15.796 0 0 0 274 90c0 8.84 7.16 16 16 16v3.13c0 .55.45 1 1 1 .2 0 .4-.06.55-.17l9.2-6.13c.46-.3.6-.93.28-1.38-.07-.1-.17-.2-.28-.28l-9.2-6.13c-.45-.3-1.08-.2-1.38.27-.1.2-.17.4-.17.6v3.1z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/globe.svg b/app/assets/images/illustrations/welcome/globe.svg
new file mode 100644
index 00000000000..c2daae5f317
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/globe.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" d="M30.24 27.823A14.98 14.98 0 0 0 24 40c0 2.549.636 4.949 1.757 7.051-.297-2.684.644-4.026 2.823-4.026 3.707 0 2.462 5.365 4.473 5.761 2.01.396 4.175.396 4.267 3.29.04 1.257-.265 2.157-.917 2.7a15.095 15.095 0 0 0 8.555-1.006c.035-1.91.303-4.941 2.21-5.61 2.373-.833-.55-1.431.734-3.368 1.17-1.762-3.297-5.2 0-4.832 3.477.388 5.044-.816 6.024-1.456a14.903 14.903 0 0 0-1.373-4.94c-.873.4-2.19.465-3.702-.538-.757-.502-1.084-3.944-2.107-3.944-3.823 0-4.065 3.17-5.994 3.944-1.076.431-4.193 3.773-5.614 3.596-1.126-.14-1.071-4.417-2.45-5.166-1.359-.738-2.174-1.948-2.447-3.633zM39 59c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/welcome/lightbulb.svg b/app/assets/images/illustrations/welcome/lightbulb.svg
new file mode 100644
index 00000000000..fce10312085
--- /dev/null
+++ b/app/assets/images/illustrations/welcome/lightbulb.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#6B4FBB" d="M33 52h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm1 5h10a2 2 0 1 1 0 4H34a2 2 0 1 1 0-4z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M45.542 46.932l.346-2.36a8.004 8.004 0 0 1 1.566-3.705c3.025-3.946 4.485-7.29 4.547-9.96C52.153 24.41 46.843 20 39 20c-7.777 0-13 4.374-13 11 0 2.4 1.462 5.73 4.573 9.846a8.009 8.009 0 0 1 1.536 3.683l.353 2.456 13.08-.054zm-17.038.624L28.15 45.1a3.997 3.997 0 0 0-.768-1.842C23.794 38.51 22 34.424 22 31c0-9.39 7.61-15 17-15s17.218 5.614 17 15c-.085 3.64-1.875 7.74-5.37 12.3a3.99 3.99 0 0 0-.784 1.853l-.346 2.36a4.003 4.003 0 0 1-3.942 3.42l-13.08.053a4 4 0 0 1-3.974-3.43z"/><path fill="#6B4FBB" d="M41 38.732a2 2 0 1 1 2 0V42a1 1 0 0 1-2 0v-3.268zm-6 0a2 2 0 1 1 2 0V42a1 1 0 0 1-2 0v-3.268z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/wiki-fro-logged-out-users.svg b/app/assets/images/illustrations/wiki-fro-logged-out-users.svg
new file mode 100644
index 00000000000..c71841f72e5
--- /dev/null
+++ b/app/assets/images/illustrations/wiki-fro-logged-out-users.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="412" height="260" viewBox="0 0 412 260" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M6.447.894L12 12H0L5.553.894a.5.5 0 0 1 .894 0z"/></defs><g fill="none" fill-rule="evenodd"><path fill="#FEF0E8" fill-rule="nonzero" d="M338 50.287C322.695 41.45 303.124 46.694 294.287 62c-8.836 15.305-3.592 34.876 11.713 43.712 15.306 8.837 34.877 3.593 43.713-11.712 8.837-15.306 3.593-34.877-11.713-43.713zm2-3.464C357.22 56.763 363.118 78.78 353.177 96c-9.941 17.218-31.958 23.118-49.177 13.176-17.218-9.94-23.118-31.958-13.177-49.176C300.764 42.78 322.782 36.88 340 46.823z"/><g transform="rotate(-150 171.003 8.53)"><path fill="#FC6D26" fill-rule="nonzero" d="M4 16v25a2 2 0 1 0 4 0V16H4zm8-4v29a6 6 0 1 1-12 0V12h12z"/><use fill="#D8D8D8" xlink:href="#a"/><path stroke="#FDC4A8" stroke-width="4" d="M6 4.472L3.236 10h5.528L6 4.472z"/><path fill="#FC6D26" d="M9 6L6.447.894a.5.5 0 0 0-.894 0L3 6c.836.628 1.874 1 3 1a4.978 4.978 0 0 0 3-1z"/></g><path fill="#F9F9F9" d="M263.116 237.116A10.002 10.002 0 0 1 254 243h-86c-11.046 0-20-8.954-20-20V121c0-4.056 2.414-7.547 5.884-9.116A9.964 9.964 0 0 0 153 116v106c0 8.837 7.163 16 16 16h90c1.467 0 2.86-.316 4.116-.884z"/><path fill="#EEE" fill-rule="nonzero" d="M214.5 106H163c-5.523 0-10 4.477-10 10v106c0 8.837 7.163 16 16 16h90c5.523 0 10-4.477 10-10v-17.999a10.036 10.036 0 0 1-4 3.167V228a6 6 0 0 1-6 6h-90c-6.627 0-12-5.373-12-12V116a6 6 0 0 1 6-6h7v-4h44.5z"/><path fill="#EEE" fill-rule="nonzero" d="M260 218.268V214h-90a6 6 0 0 0 0 12h86a4 4 0 0 0 4-4v-.268a1.99 1.99 0 0 1-1 .268h-50a2 2 0 0 1 0-4h50c.364 0 .706.097 1 .268zM170 210h90.5a3.5 3.5 0 0 1 3.5 3.5v8.5a8 8 0 0 1-8 8h-86c-5.523 0-10-4.477-10-10s4.477-10 10-10z"/><path fill="#EEE" fill-rule="nonzero" d="M174 110v100h87a6 6 0 0 0 6-6v-88a6 6 0 0 0-6-6h-87zm-4-4h91c5.523 0 10 4.477 10 10v88c0 5.523-4.477 10-10 10h-91V106z"/><path fill="#EFEDF8" d="M230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M236.182 129.207a5.5 5.5 0 0 1 6.102.04l7.716 5.219V105a2 2 0 0 0-2-2h-18a2 2 0 0 0-2 2v29.584l8.182-5.377zM230 99h18a6 6 0 0 1 6 6v31.35a3 3 0 0 1-4.68 2.484l-9.277-6.274a1.5 1.5 0 0 0-1.664-.01l-9.731 6.395a3 3 0 0 1-4.648-2.507V105a6 6 0 0 1 6-6z"/><g fill-rule="nonzero"><path fill="#EFEDF8" d="M156 74c14.912 0 27-12.088 27-27s-12.088-27-27-27-27 12.088-27 27 12.088 27 27 27zm0 4c-17.12 0-31-13.88-31-31s13.88-31 31-31 31 13.88 31 31-13.88 31-31 31z"/><path fill="#6B4FBB" d="M147.535 44.916l-.116 1.086a8.446 8.446 0 0 0 .093 2.44l.2 1.08-2.262 1.202a.495.495 0 0 0-.213.678l.941 1.77c.128.239.434.332.68.201l2.25-1.196.785.775a8.544 8.544 0 0 0 1.967 1.45l.975.522-.486 2.5a.495.495 0 0 0 .392.59l1.968.383a.504.504 0 0 0 .585-.401l.489-2.515 1.086-.13a8.584 8.584 0 0 0 2.363-.633l1.005-.43 1.68 1.933a.495.495 0 0 0 .708.055l1.513-1.315a.504.504 0 0 0 .044-.708l-1.67-1.922.583-.94c.431-.696.761-1.45.978-2.239l.292-1.063 2.547-.089a.495.495 0 0 0 .488-.515l-.07-2.003a.504.504 0 0 0-.523-.48l-2.56.09-.367-1.037a8.446 8.446 0 0 0-1.139-2.159l-.644-.882 1.509-2.076a.495.495 0 0 0-.106-.702l-1.621-1.178a.504.504 0 0 0-.7.116l-1.494 2.057-1.05-.362a8.459 8.459 0 0 0-2.398-.455l-1.1-.047-.66-2.466a.495.495 0 0 0-.613-.36l-1.936.519a.504.504 0 0 0-.35.617l.661 2.466-.93.59a8.459 8.459 0 0 0-1.848 1.594l-.728.838-2.322-1.034a.495.495 0 0 0-.665.25l-.815 1.83a.504.504 0 0 0 .26.661l2.344 1.044zm-3.565 1.697a3.504 3.504 0 0 1-1.78-4.622l.815-1.83a3.495 3.495 0 0 1 4.626-1.77l.346.154c.259-.245.529-.477.81-.697l-.106-.394a3.504 3.504 0 0 1 2.471-4.292l1.936-.519a3.495 3.495 0 0 1 4.286 2.481l.106.395c.353.05.703.116 1.05.198l.222-.306a3.504 3.504 0 0 1 4.89-.78l1.622 1.178a3.495 3.495 0 0 1 .769 4.892l-.258.355c.184.312.354.633.508.962l.42-.014a3.504 3.504 0 0 1 3.625 3.373l.07 2.003a3.495 3.495 0 0 1-3.382 3.618l-.4.014c-.127.332-.27.659-.426.978l.256.294a3.504 3.504 0 0 1-.34 4.941l-1.512 1.315a3.495 3.495 0 0 1-4.94-.351l-.283-.325a11.669 11.669 0 0 1-1.05.28l-.082.424a3.504 3.504 0 0 1-4.103 2.774l-1.967-.382a3.495 3.495 0 0 1-2.765-4.11l.075-.383a11.547 11.547 0 0 1-.858-.633l-.354.188a3.504 3.504 0 0 1-4.738-1.442l-.94-1.77a3.495 3.495 0 0 1 1.453-4.734l.37-.197a11.436 11.436 0 0 1-.041-1.088l-.4-.178zm13.326 5.608a5.5 5.5 0 1 1-2.847-10.625 5.5 5.5 0 0 1 2.847 10.625zm-.776-2.898a2.5 2.5 0 1 0-1.294-4.83 2.5 2.5 0 0 0 1.294 4.83z"/></g><g fill-rule="nonzero"><path fill="#EFEDF8" d="M326.979 222.047c14.403 3.86 29.209-4.688 33.068-19.092 3.86-14.403-4.688-29.209-19.092-33.068-14.403-3.86-29.209 4.688-33.068 19.092-3.86 14.404 4.688 29.209 19.092 33.068zm-1.035 3.864c-16.538-4.431-26.352-21.43-21.92-37.967 4.43-16.538 21.429-26.352 37.966-21.92 16.538 4.43 26.352 21.429 21.92 37.966-4.43 16.538-21.429 26.352-37.966 21.92z"/><path fill="#6B4FBB" d="M329.376 201.598c-4.668-2.621-7.155-8.157-5.706-13.566 1.715-6.402 8.295-10.201 14.697-8.486 6.402 1.716 10.2 8.296 8.485 14.697-1.45 5.41-6.371 8.96-11.725 8.897a3.03 3.03 0 0 1-.074.365l-1.812 6.761a3 3 0 0 1-5.795-1.552l1.812-6.762a3.03 3.03 0 0 1 .118-.354zm3.815-2.733a8 8 0 1 0 4.14-15.455 8 8 0 0 0-4.14 15.455z"/></g><path fill="#FEF0E8" fill-rule="nonzero" d="M91.373 193c17.071-4.574 27.202-22.12 22.628-39.191-4.575-17.071-22.121-27.202-39.192-22.628-17.071 4.574-27.202 22.121-22.628 39.192 4.574 17.071 22.121 27.202 39.192 22.627zm1.035 3.864c-19.204 5.146-38.945-6.25-44.09-25.456-5.146-19.204 6.25-38.945 25.455-44.09 19.205-5.146 38.945 6.25 44.091 25.455 5.146 19.205-6.25 38.945-25.456 44.091z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M70.067 152.122l6.73 25.114 19.318-5.176-6.73-25.114-19.318 5.176zm-1.035-3.864l19.318-5.176a4 4 0 0 1 4.9 2.828l6.729 25.114a4 4 0 0 1-2.829 4.9L77.832 181.1a4 4 0 0 1-4.9-2.829l-6.729-25.114a4 4 0 0 1 2.829-4.899z"/><path fill="#FC6D26" d="M76.898 154.433l7.727-2.07a2 2 0 0 1 1.036 3.863l-7.728 2.07a2 2 0 1 1-1.035-3.863zm1.812 6.761l5.795-1.553a2 2 0 0 1 1.035 3.864l-5.795 1.553a2 2 0 1 1-1.035-3.864zm1.811 6.762l7.728-2.07a2 2 0 0 1 1.035 3.863l-7.727 2.07a2 2 0 1 1-1.036-3.863z"/></g></svg> \ No newline at end of file
diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js
index 3de192d56eb..d2d3a257c0d 100644
--- a/app/assets/javascripts/abuse_reports.js
+++ b/app/assets/javascripts/abuse_reports.js
@@ -1,3 +1,5 @@
+import { truncate } from './lib/utils/text_utility';
+
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
@@ -15,7 +17,7 @@ export default class AbuseReports {
if (reportMessage.length > MAX_MESSAGE_LENGTH) {
$messageCellElement.data('original-message', reportMessage);
$messageCellElement.data('message-truncated', 'true');
- $messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
+ $messageCellElement.text(truncate(reportMessage, MAX_MESSAGE_LENGTH));
}
}
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 38d1effc77c..d963101028a 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -15,6 +15,8 @@ const Api = {
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits',
+ branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
+ createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
@@ -123,6 +125,19 @@ const Api = {
});
},
+ branchSingle(id, branch) {
+ const url = Api.buildUrl(Api.branchSinglePath)
+ .replace(':id', id)
+ .replace(':branch', branch);
+
+ return this.wrapAjaxCall({
+ url,
+ type: 'GET',
+ contentType: 'application/json; charset=utf-8',
+ dataType: 'json',
+ });
+ },
+
// Return text for a specific license
licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath)
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 4d2d4db7c0e..0f28bd233ac 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,8 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */
+/* eslint-disable no-param-reassign, prefer-template, no-var, no-void, consistent-return */
+
import AccessorUtilities from './lib/utils/accessor';
-window.Autosave = (function() {
- function Autosave(field, key, resource) {
+export default class Autosave {
+ constructor(field, key, resource) {
this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.resource = resource;
@@ -12,14 +13,10 @@ window.Autosave = (function() {
this.key = 'autosave/' + key;
this.field.data('autosave', this);
this.restore();
- this.field.on('input', (function(_this) {
- return function() {
- return _this.save();
- };
- })(this));
+ this.field.on('input', () => this.save());
}
- Autosave.prototype.restore = function() {
+ restore() {
var text;
if (!this.isLocalStorageAvailable) return;
@@ -40,9 +37,9 @@ window.Autosave = (function() {
field.dispatchEvent(event);
}
}
- };
+ }
- Autosave.prototype.save = function() {
+ save() {
var text;
text = this.field.val();
@@ -51,15 +48,11 @@ window.Autosave = (function() {
}
return this.reset();
- };
+ }
- Autosave.prototype.reset = function() {
+ reset() {
if (!this.isLocalStorageAvailable) return;
return window.localStorage.removeItem(this.key);
- };
-
- return Autosave;
-})();
-
-export default window.Autosave;
+ }
+}
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 4f01345ee3b..622764107ad 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,8 +1,8 @@
/* eslint-disable class-methods-use-this */
-/* global Flash */
import _ from 'underscore';
import Cookies from 'js-cookie';
import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils';
+import Flash from './flash';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index e00af4b2fa8..add43b81f6d 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,8 +1,8 @@
-import autosize from 'vendor/autosize';
+import Autosize from 'autosize';
document.addEventListener('DOMContentLoaded', () => {
const autosizeEls = document.querySelectorAll('.js-autosize');
- autosize(autosizeEls);
- autosize.update(autosizeEls);
+ Autosize(autosizeEls);
+ Autosize.update(autosizeEls);
});
diff --git a/app/assets/javascripts/behaviors/copy_as_gfm.js b/app/assets/javascripts/behaviors/copy_as_gfm.js
new file mode 100644
index 00000000000..e7dc4ef8304
--- /dev/null
+++ b/app/assets/javascripts/behaviors/copy_as_gfm.js
@@ -0,0 +1,481 @@
+/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
+
+import _ from 'underscore';
+import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils';
+import { placeholderImage } from '../lazy_loader';
+
+const gfmRules = {
+ // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
+ // GitLab Flavored Markdown (GFM) to HTML.
+ // These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
+ // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
+ // from GFM should have a handler here, in reverse order.
+ // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
+ InlineDiffFilter: {
+ 'span.idiff.addition'(el, text) {
+ return `{+${text}+}`;
+ },
+ 'span.idiff.deletion'(el, text) {
+ return `{-${text}-}`;
+ },
+ },
+ TaskListFilter: {
+ 'input[type=checkbox].task-list-item-checkbox'(el) {
+ return `[${el.checked ? 'x' : ' '}]`;
+ },
+ },
+ ReferenceFilter: {
+ '.tooltip'(el) {
+ return '';
+ },
+ 'a.gfm:not([data-link=true])'(el, text) {
+ return el.dataset.original || text;
+ },
+ },
+ AutolinkFilter: {
+ 'a'(el, text) {
+ // Fallback on the regular MarkdownFilter's `a` handler.
+ if (text !== el.getAttribute('href')) return false;
+
+ return text;
+ },
+ },
+ TableOfContentsFilter: {
+ 'ul.section-nav'(el) {
+ return '[[_TOC_]]';
+ },
+ },
+ EmojiFilter: {
+ 'img.emoji'(el) {
+ return el.getAttribute('alt');
+ },
+ 'gl-emoji'(el) {
+ return `:${el.getAttribute('data-name')}:`;
+ },
+ },
+ ImageLinkFilter: {
+ 'a.no-attachment-icon'(el, text) {
+ return text;
+ },
+ },
+ ImageLazyLoadFilter: {
+ 'img'(el, text) {
+ return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
+ },
+ },
+ VideoLinkFilter: {
+ '.video-container'(el) {
+ const videoEl = el.querySelector('video');
+ if (!videoEl) return false;
+
+ return CopyAsGFM.nodeToGFM(videoEl);
+ },
+ 'video'(el) {
+ return `![${el.dataset.title}](${el.getAttribute('src')})`;
+ },
+ },
+ MathFilter: {
+ 'pre.code.math[data-math-style=display]'(el, text) {
+ return `\`\`\`math\n${text.trim()}\n\`\`\``;
+ },
+ 'code.code.math[data-math-style=inline]'(el, text) {
+ return `$\`${text}\`$`;
+ },
+ 'span.katex-display span.katex-mathml'(el) {
+ const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
+ if (!mathAnnotation) return false;
+
+ return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
+ },
+ 'span.katex-mathml'(el) {
+ const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
+ if (!mathAnnotation) return false;
+
+ return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
+ },
+ 'span.katex-html'(el) {
+ // We don't want to include the content of this element in the copied text.
+ return '';
+ },
+ 'annotation[encoding="application/x-tex"]'(el, text) {
+ return text.trim();
+ },
+ },
+ SanitizationFilter: {
+ 'a[name]:not([href]):empty'(el) {
+ return el.outerHTML;
+ },
+ 'dl'(el, text) {
+ let lines = text.trim().split('\n');
+ // Add two spaces to the front of subsequent list items lines,
+ // or leave the line entirely blank.
+ lines = lines.map((l) => {
+ const line = l.trim();
+ if (line.length === 0) return '';
+
+ return ` ${line}`;
+ });
+
+ return `<dl>\n${lines.join('\n')}\n</dl>`;
+ },
+ 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) {
+ const tag = el.nodeName.toLowerCase();
+ return `<${tag}>${text}</${tag}>`;
+ },
+ },
+ SyntaxHighlightFilter: {
+ 'pre.code.highlight'(el, t) {
+ const text = t.trimRight();
+
+ let lang = el.getAttribute('lang');
+ if (!lang || lang === 'plaintext') {
+ lang = '';
+ }
+
+ // Prefixes lines with 4 spaces if the code contains triple backticks
+ if (lang === '' && text.match(/^```/gm)) {
+ return text.split('\n').map((l) => {
+ const line = l.trim();
+ if (line.length === 0) return '';
+
+ return ` ${line}`;
+ }).join('\n');
+ }
+
+ return `\`\`\`${lang}\n${text}\n\`\`\``;
+ },
+ 'pre > code'(el, text) {
+ // Don't wrap code blocks in ``
+ return text;
+ },
+ },
+ MarkdownFilter: {
+ 'br'(el) {
+ // Two spaces at the end of a line are turned into a BR
+ return ' ';
+ },
+ 'code'(el, text) {
+ let backtickCount = 1;
+ const backtickMatch = text.match(/`+/);
+ if (backtickMatch) {
+ backtickCount = backtickMatch[0].length + 1;
+ }
+
+ const backticks = Array(backtickCount + 1).join('`');
+ const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
+
+ return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks;
+ },
+ 'blockquote'(el, text) {
+ return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
+ },
+ 'img'(el) {
+ const imageSrc = el.src;
+ const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : (el.dataset.src || '');
+ return `![${el.getAttribute('alt')}](${imageUrl})`;
+ },
+ 'a.anchor'(el, text) {
+ // Don't render a Markdown link for the anchor link inside a heading
+ return text;
+ },
+ 'a'(el, text) {
+ return `[${text}](${el.getAttribute('href')})`;
+ },
+ 'li'(el, text) {
+ const lines = text.trim().split('\n');
+ const firstLine = `- ${lines.shift()}`;
+ // Add four spaces to the front of subsequent list items lines,
+ // or leave the line entirely blank.
+ const nextLines = lines.map((s) => {
+ if (s.trim().length === 0) return '';
+
+ return ` ${s}`;
+ });
+
+ return `${firstLine}\n${nextLines.join('\n')}`;
+ },
+ 'ul'(el, text) {
+ return text;
+ },
+ 'ol'(el, text) {
+ // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
+ return text.replace(/^- /mg, '1. ');
+ },
+ 'h1'(el, text) {
+ return `# ${text.trim()}`;
+ },
+ 'h2'(el, text) {
+ return `## ${text.trim()}`;
+ },
+ 'h3'(el, text) {
+ return `### ${text.trim()}`;
+ },
+ 'h4'(el, text) {
+ return `#### ${text.trim()}`;
+ },
+ 'h5'(el, text) {
+ return `##### ${text.trim()}`;
+ },
+ 'h6'(el, text) {
+ return `###### ${text.trim()}`;
+ },
+ 'strong'(el, text) {
+ return `**${text}**`;
+ },
+ 'em'(el, text) {
+ return `_${text}_`;
+ },
+ 'del'(el, text) {
+ return `~~${text}~~`;
+ },
+ 'sup'(el, text) {
+ return `^${text}`;
+ },
+ 'hr'(el) {
+ return '-----';
+ },
+ 'table'(el) {
+ const theadEl = el.querySelector('thead');
+ const tbodyEl = el.querySelector('tbody');
+ if (!theadEl || !tbodyEl) return false;
+
+ const theadText = CopyAsGFM.nodeToGFM(theadEl);
+ const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
+
+ return [theadText, tbodyText].join('\n');
+ },
+ 'thead'(el, text) {
+ const cells = _.map(el.querySelectorAll('th'), (cell) => {
+ let chars = CopyAsGFM.nodeToGFM(cell).length + 2;
+
+ let before = '';
+ let after = '';
+ switch (cell.style.textAlign) {
+ case 'center':
+ before = ':';
+ after = ':';
+ chars -= 2;
+ break;
+ case 'right':
+ after = ':';
+ chars -= 1;
+ break;
+ default:
+ break;
+ }
+
+ chars = Math.max(chars, 3);
+
+ const middle = Array(chars + 1).join('-');
+
+ return before + middle + after;
+ });
+
+ const separatorRow = `|${cells.join('|')}|`;
+
+ return [text, separatorRow].join('\n');
+ },
+ 'tr'(el) {
+ const cellEls = el.querySelectorAll('td, th');
+ if (cellEls.length === 0) return false;
+
+ const cells = _.map(cellEls, cell => CopyAsGFM.nodeToGFM(cell));
+ return `| ${cells.join(' | ')} |`;
+ },
+ },
+};
+
+export class CopyAsGFM {
+ constructor() {
+ $(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
+ $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
+ $(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM);
+ }
+
+ static copyAsGFM(e, transformer) {
+ const clipboardData = e.originalEvent.clipboardData;
+ if (!clipboardData) return;
+
+ const documentFragment = getSelectedFragment();
+ if (!documentFragment) return;
+
+ const el = transformer(documentFragment.cloneNode(true), e.currentTarget);
+ if (!el) return;
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ clipboardData.setData('text/plain', el.textContent);
+ clipboardData.setData('text/x-gfm', this.nodeToGFM(el));
+ }
+
+ static pasteGFM(e) {
+ const clipboardData = e.originalEvent.clipboardData;
+ if (!clipboardData) return;
+
+ const text = clipboardData.getData('text/plain');
+ const gfm = clipboardData.getData('text/x-gfm');
+ if (!gfm) return;
+
+ e.preventDefault();
+
+ window.gl.utils.insertText(e.target, (textBefore, textAfter) => {
+ // If the text before the cursor contains an odd number of backticks,
+ // we are either inside an inline code span that starts with 1 backtick
+ // or a code block that starts with 3 backticks.
+ // This logic still holds when there are one or more _closed_ code spans
+ // or blocks that will have 2 or 6 backticks.
+ // This will break down when the actual code block contains an uneven
+ // number of backticks, but this is a rare edge case.
+ const backtickMatch = textBefore.match(/`/g);
+ const insideCodeBlock = backtickMatch && (backtickMatch.length % 2) === 1;
+
+ if (insideCodeBlock) {
+ return text;
+ }
+
+ return gfm;
+ });
+ }
+
+ static transformGFMSelection(documentFragment) {
+ const gfmElements = documentFragment.querySelectorAll('.md, .wiki');
+ switch (gfmElements.length) {
+ case 0: {
+ return documentFragment;
+ }
+ case 1: {
+ return gfmElements[0];
+ }
+ default: {
+ const allGfmElement = document.createElement('div');
+
+ for (let i = 0; i < gfmElements.length; i += 1) {
+ const gfmElement = gfmElements[i];
+ allGfmElement.appendChild(gfmElement);
+ allGfmElement.appendChild(document.createTextNode('\n\n'));
+ }
+
+ return allGfmElement;
+ }
+ }
+ }
+
+ static transformCodeSelection(documentFragment, target) {
+ let lineSelector = '.line';
+
+ if (target) {
+ const lineClass = ['left-side', 'right-side'].filter(name => target.classList.contains(name))[0];
+ if (lineClass) {
+ lineSelector = `.line_content.${lineClass} ${lineSelector}`;
+ }
+ }
+
+ const lineElements = documentFragment.querySelectorAll(lineSelector);
+
+ let codeElement;
+ if (lineElements.length > 1) {
+ codeElement = document.createElement('pre');
+ codeElement.className = 'code highlight';
+
+ const lang = lineElements[0].getAttribute('lang');
+ if (lang) {
+ codeElement.setAttribute('lang', lang);
+ }
+ } else {
+ codeElement = document.createElement('code');
+ }
+
+ if (lineElements.length > 0) {
+ for (let i = 0; i < lineElements.length; i += 1) {
+ const lineElement = lineElements[i];
+ codeElement.appendChild(lineElement);
+ codeElement.appendChild(document.createTextNode('\n'));
+ }
+ } else {
+ codeElement.appendChild(documentFragment);
+ }
+
+ return codeElement;
+ }
+
+ static nodeToGFM(node, respectWhitespaceParam = false) {
+ if (node.nodeType === Node.COMMENT_NODE) {
+ return '';
+ }
+
+ if (node.nodeType === Node.TEXT_NODE) {
+ return node.textContent;
+ }
+
+ const respectWhitespace = respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE');
+
+ const text = this.innerGFM(node, respectWhitespace);
+
+ if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
+ return text;
+ }
+
+ for (const filter in gfmRules) {
+ const rules = gfmRules[filter];
+
+ for (const selector in rules) {
+ const func = rules[selector];
+
+ if (!nodeMatchesSelector(node, selector)) continue;
+
+ let result;
+ if (func.length === 2) {
+ // if `func` takes 2 arguments, it depends on text.
+ // if there is no text, we don't need to generate GFM for this node.
+ if (text.length === 0) continue;
+
+ result = func(node, text);
+ } else {
+ result = func(node);
+ }
+
+ if (result === false) continue;
+
+ return result;
+ }
+ }
+
+ return text;
+ }
+
+ static innerGFM(parentNode, respectWhitespace = false) {
+ const nodes = parentNode.childNodes;
+
+ const clonedParentNode = parentNode.cloneNode(true);
+ const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
+
+ for (let i = 0; i < nodes.length; i += 1) {
+ const node = nodes[i];
+ const clonedNode = clonedNodes[i];
+
+ const text = this.nodeToGFM(node, respectWhitespace);
+
+ // `clonedNode.replaceWith(text)` is not yet widely supported
+ clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
+ }
+
+ let nodeText = clonedParentNode.innerText || clonedParentNode.textContent;
+
+ if (!respectWhitespace) {
+ nodeText = nodeText.trim();
+ }
+
+ return nodeText;
+ }
+}
+
+// Export CopyAsGFM as a global for rspec to access
+// see /spec/features/copy_as_gfm_spec.rb
+if (process.env.NODE_ENV !== 'production') {
+ window.CopyAsGFM = CopyAsGFM;
+}
+
+export default function initCopyAsGFM() {
+ return new CopyAsGFM();
+}
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
new file mode 100644
index 00000000000..cdea625fc8c
--- /dev/null
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -0,0 +1,73 @@
+import Clipboard from 'clipboard';
+
+function showTooltip(target, title) {
+ const $target = $(target);
+ const originalTitle = $target.data('original-title');
+
+ if (!$target.data('hideTooltip')) {
+ $target
+ .attr('title', title)
+ .tooltip('fixTitle')
+ .tooltip('show')
+ .attr('title', originalTitle)
+ .tooltip('fixTitle');
+ }
+}
+
+function genericSuccess(e) {
+ showTooltip(e.trigger, 'Copied');
+ // Clear the selection and blur the trigger so it loses its border
+ e.clearSelection();
+ $(e.trigger).blur();
+}
+
+/**
+ * Safari > 10 doesn't support `execCommand`, so instead we inform the user to copy manually.
+ * See http://clipboardjs.com/#browser-support
+ */
+function genericError(e) {
+ let key;
+ if (/Mac/i.test(navigator.userAgent)) {
+ key = '&#8984;'; // Command
+ } else {
+ key = 'Ctrl';
+ }
+ showTooltip(e.trigger, `Press ${key}-C to copy`);
+}
+
+export default function initCopyToClipboard() {
+ const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
+ clipboard.on('success', genericSuccess);
+ clipboard.on('error', genericError);
+
+ /**
+ * This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting
+ * of plain text or GFM. The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and
+ * `gfm` keys into the `data-clipboard-text` attribute that ClipboardJS reads from.
+ * When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly`
+ * attribute`), sets its value to the value of this data attribute, focusses on it, and finally
+ * programmatically issues the 'Copy' command, this code intercepts the copy command/event at
+ * the last minute to deconstruct this JSON hash and set the `text/plain` and `text/x-gfm` copy
+ * data types to the intended values.
+ */
+ $(document).on('copy', 'body > textarea[readonly]', (e) => {
+ const clipboardData = e.originalEvent.clipboardData;
+ if (!clipboardData) return;
+
+ const text = e.target.value;
+
+ let json;
+ try {
+ json = JSON.parse(text);
+ } catch (ex) {
+ return;
+ }
+
+ if (!json.text || !json.gfm) return;
+
+ e.preventDefault();
+
+ clipboardData.setData('text/plain', json.text);
+ clipboardData.setData('text/x-gfm', json.gfm);
+ });
+}
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 44b2c974b9e..34e905222b4 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,5 +1,7 @@
import './autosize';
import './bind_in_out';
+import initCopyAsGFM from './copy_as_gfm';
+import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior';
import installGlEmojiElement from './gl_emoji';
import './quick_submit';
@@ -7,3 +9,5 @@ import './requires_input';
import './toggler_behavior';
installGlEmojiElement();
+initCopyAsGFM();
+initCopyToClipboard();
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index 8641a6fdae6..062577af385 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -1,9 +1,8 @@
-/* global Flash */
-
+import Flash from '../flash';
import BalsamiqViewer from './balsamiq/balsamiq_viewer';
function onError() {
- const flash = new window.Flash('Balsamiq file could not be loaded.');
+ const flash = new Flash('Balsamiq file could not be loaded.');
return flash;
}
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index ddd1fea3aca..0d590a9dbc4 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,6 +1,5 @@
/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
-/* global Dropzone */
-
+import Dropzone from 'dropzone';
import '../lib/utils/url_utility';
import { HIDDEN_CLASS } from '../lib/utils/constants';
import csrf from '../lib/utils/csrf';
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index a20c6ca7a21..583e5faa506 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -1,6 +1,5 @@
/* eslint-disable class-methods-use-this */
-/* global Flash */
-
+import Flash from '../flash';
import FileTemplateTypeSelector from './template_selectors/type_selector';
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index e0b73f13d36..54132e8537b 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,4 +1,4 @@
-/* global Flash */
+import Flash from '../../flash';
import { handleLocationHash } from '../../lib/utils/common_utils';
export default class BlobViewer {
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index b5500ac116f..6b06344f5ba 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -1,7 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
/* global EditBlob */
-/* global NewCommitForm */
-
+import NewCommitForm from '../new_commit_form';
import EditBlob from './edit_blob';
import BlobFileDropzone from '../blob/blob_file_dropzone';
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index 815248f38ee..20d23162940 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -1,12 +1,13 @@
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
-/* global BoardService */
-/* global Flash */
import _ from 'underscore';
import Vue from 'vue';
import VueResource from 'vue-resource';
+import Flash from '../flash';
+import { __ } from '../locale';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
+import sidebarEventHub from '../sidebar/event_hub';
import './models/issue';
import './models/label';
import './models/list';
@@ -14,7 +15,7 @@ import './models/milestone';
import './models/assignee';
import './stores/boards_store';
import './stores/modal_store';
-import './services/board_service';
+import BoardService from './services/board_service';
import './mixins/modal_mixins';
import './mixins/sortable_default_options';
import './filters/due_date_filters';
@@ -77,11 +78,16 @@ $(() => {
});
Store.rootPath = this.boardsEndpoint;
- // Listen for updateTokens event
eventHub.$on('updateTokens', this.updateTokens);
+ eventHub.$on('newDetailIssue', this.updateDetailIssue);
+ eventHub.$on('clearDetailIssue', this.clearDetailIssue);
+ sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
+ eventHub.$off('newDetailIssue', this.updateDetailIssue);
+ eventHub.$off('clearDetailIssue', this.clearDetailIssue);
+ sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
},
mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true);
@@ -112,6 +118,46 @@ $(() => {
methods: {
updateTokens() {
this.filterManager.updateTokens();
+ },
+ updateDetailIssue(newIssue) {
+ const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint;
+ if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
+ newIssue.setFetchingState('subscriptions', true);
+ BoardService.getIssueInfo(sidebarInfoEndpoint)
+ .then(res => res.json())
+ .then((data) => {
+ newIssue.setFetchingState('subscriptions', false);
+ newIssue.updateData({
+ subscribed: data.subscribed,
+ });
+ })
+ .catch(() => {
+ newIssue.setFetchingState('subscriptions', false);
+ Flash(__('An error occurred while fetching sidebar data'));
+ });
+ }
+
+ Store.detail.issue = newIssue;
+ },
+ clearDetailIssue() {
+ Store.detail.issue = {};
+ },
+ toggleSubscription(id) {
+ const issue = Store.detail.issue;
+ if (issue.id === id && issue.toggleSubscriptionEndpoint) {
+ issue.setFetchingState('subscriptions', true);
+ BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
+ .then(() => {
+ issue.setFetchingState('subscriptions', false);
+ issue.updateData({
+ subscribed: !issue.subscribed,
+ });
+ })
+ .catch(() => {
+ issue.setFetchingState('subscriptions', false);
+ Flash(__('An error occurred when toggling the notification subscription'));
+ });
+ }
}
},
});
diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js
deleted file mode 100644
index 079fb6438b9..00000000000
--- a/app/assets/javascripts/boards/components/board_card.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import './issue_card_inner';
-
-const Store = gl.issueBoards.BoardsStore;
-
-export default {
- name: 'BoardsIssueCard',
- template: `
- <li class="card"
- :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
- :index="index"
- :data-issue-id="issue.id"
- @mousedown="mouseDown"
- @mousemove="mouseMove"
- @mouseup="showIssue($event)">
- <issue-card-inner
- :list="list"
- :issue="issue"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"
- :update-filters="true" />
- </li>
- `,
- components: {
- 'issue-card-inner': gl.issueBoards.IssueCardInner,
- },
- props: {
- list: Object,
- issue: Object,
- issueLinkBase: String,
- disabled: Boolean,
- index: Number,
- rootPath: String,
- },
- data() {
- return {
- showDetail: false,
- detailIssue: Store.detail,
- };
- },
- computed: {
- issueDetailVisible() {
- return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
- },
- },
- methods: {
- mouseDown() {
- this.showDetail = true;
- },
- mouseMove() {
- this.showDetail = false;
- },
- showIssue(e) {
- if (e.target.classList.contains('js-no-trigger')) return;
-
- if (this.showDetail) {
- this.showDetail = false;
-
- if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
- Store.detail.issue = {};
- } else {
- Store.detail.issue = this.issue;
- Store.detail.list = this.list;
- }
- }
- },
- },
-};
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
new file mode 100644
index 00000000000..0b220a56e0b
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -0,0 +1,71 @@
+<script>
+import './issue_card_inner';
+import eventHub from '../eventhub';
+
+const Store = gl.issueBoards.BoardsStore;
+
+export default {
+ name: 'BoardsIssueCard',
+ components: {
+ 'issue-card-inner': gl.issueBoards.IssueCardInner,
+ },
+ props: {
+ list: Object,
+ issue: Object,
+ issueLinkBase: String,
+ disabled: Boolean,
+ index: Number,
+ rootPath: String,
+ },
+ data() {
+ return {
+ showDetail: false,
+ detailIssue: Store.detail,
+ };
+ },
+ computed: {
+ issueDetailVisible() {
+ return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
+ },
+ },
+ methods: {
+ mouseDown() {
+ this.showDetail = true;
+ },
+ mouseMove() {
+ this.showDetail = false;
+ },
+ showIssue(e) {
+ if (e.target.classList.contains('js-no-trigger')) return;
+
+ if (this.showDetail) {
+ this.showDetail = false;
+
+ if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
+ eventHub.$emit('clearDetailIssue');
+ } else {
+ eventHub.$emit('newDetailIssue', this.issue);
+ Store.detail.list = this.list;
+ }
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="card"
+ :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
+ :index="index"
+ :data-issue-id="issue.id"
+ @mousedown="mouseDown"
+ @mousemove="mouseMove"
+ @mouseup="showIssue($event)">
+ <issue-card-inner
+ :list="list"
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ :update-filters="true" />
+ </li>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
index 6159680f1e6..29aeb8e84aa 100644
--- a/app/assets/javascripts/boards/components/board_list.js
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -1,6 +1,6 @@
/* global Sortable */
import boardNewIssue from './board_new_issue';
-import boardCard from './board_card';
+import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 590b7be36e3..faa76da964f 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -1,15 +1,17 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
-/* global IssuableContext */
/* global MilestoneSelect */
-/* global LabelsSelect */
/* global Sidebar */
-/* global Flash */
import Vue from 'vue';
+import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub';
-import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
-import Assignees from '../../sidebar/components/assignees/assignees';
+import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
+import assignees from '../../sidebar/components/assignees/assignees';
+import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
+import IssuableContext from '../../issuable_context';
+import LabelsSelect from '../../labels_select';
+import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
const Store = gl.issueBoards.BoardsStore;
@@ -113,14 +115,14 @@ gl.issueBoards.BoardSidebar = Vue.extend({
mounted () {
new IssuableContext(this.currentUser);
new MilestoneSelect();
- new gl.DueDateSelectors();
+ new DueDateSelectors();
new LabelsSelect();
new Sidebar();
- gl.Subscription.bindAll('.subscription');
},
components: {
+ assigneeTitle,
+ assignees,
removeBtn: gl.issueBoards.RemoveIssueBtn,
- 'assignee-title': AssigneeTitle,
- assignees: Assignees,
+ subscriptions,
},
});
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
index a656f0546c0..182957113a2 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -1,8 +1,9 @@
/* eslint-disable no-new */
-/* global Flash */
import Vue from 'vue';
+import Flash from '../../../flash';
import './lists_dropdown';
+import { pluralize } from '../../../lib/utils/text_utility';
const ModalStore = gl.issueBoards.ModalStore;
@@ -21,7 +22,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
submitText() {
const count = ModalStore.selectedCount();
- return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
+ return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`;
},
},
methods: {
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
index 1e623cf58b7..1ad97211934 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
@@ -1,7 +1,7 @@
/* eslint-disable no-new */
-/* global Flash */
import Vue from 'vue';
+import Flash from '../../../flash';
const Store = gl.issueBoards.BoardsStore;
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 3f083655f95..184665f395c 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -11,7 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true;
- this.cantEdit = cantEdit;
+ this.cantEdit = cantEdit.filter(i => typeof i === 'string');
+ this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object');
}
updateObject(path) {
@@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new Event('input'));
}
- canEdit(tokenName) {
- return this.cantEdit.indexOf(tokenName) === -1;
+ canEdit(tokenName, tokenValue) {
+ if (this.cantEdit.includes(tokenName)) return false;
+ return this.cantEditWithValue.findIndex(token => token.name === tokenName &&
+ token.value === tokenValue) === -1;
}
}
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 407db176446..10f85c1d676 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -17,6 +17,11 @@ class ListIssue {
this.assignees = [];
this.selected = false;
this.position = obj.relative_position || Infinity;
+ this.isFetching = {
+ subscriptions: true,
+ };
+ this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
+ this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
@@ -73,6 +78,14 @@ class ListIssue {
return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
}
+ updateData(newData) {
+ Object.assign(this, newData);
+ }
+
+ setFetchingState(key, value) {
+ this.isFetching[key] = value;
+ }
+
update (url) {
const data = {
issue: {
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 38eea38f949..fa7ddd25e1f 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -2,12 +2,12 @@
import Vue from 'vue';
-class BoardService {
+export default class BoardService {
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
method: 'GET',
- url: `${gon.relative_url_root}/boards/${boardId}/issues.json`,
+ url: `${gon.relative_url_root}/-/boards/${boardId}/issues.json`,
}
});
this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, {
@@ -16,7 +16,7 @@ class BoardService {
url: `${listsEndpoint}/generate.json`
}
});
- this.issue = Vue.resource(`${gon.relative_url_root}/boards/${boardId}/issues{/id}`, {});
+ this.issue = Vue.resource(`${gon.relative_url_root}/-/boards/${boardId}/issues{/id}`, {});
this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
bulkUpdate: {
method: 'POST',
@@ -88,6 +88,14 @@ class BoardService {
return this.issues.bulkUpdate(data);
}
+
+ static getIssueInfo(endpoint) {
+ return Vue.http.get(endpoint);
+ }
+
+ static toggleIssueSubscription(endpoint) {
+ return Vue.http.post(endpoint);
+ }
}
window.BoardService = BoardService;
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index ea82958e80d..798d7e0d147 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -14,16 +14,18 @@ gl.issueBoards.BoardsStore = {
},
state: {},
detail: {
- issue: {}
+ issue: {},
},
moving: {
issue: {},
- list: {}
+ list: {},
},
create () {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
- this.detail = { issue: {} };
+ this.detail = {
+ issue: {},
+ };
},
addList (listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
index f73e489e7b2..ff88083a4b4 100644
--- a/app/assets/javascripts/broadcast_message.js
+++ b/app/assets/javascripts/broadcast_message.js
@@ -1,33 +1,28 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */
-
-$(function() {
- var previewPath;
- $('input#broadcast_message_color').on('input', function() {
- var previewColor;
- previewColor = $(this).val();
- return $('div.broadcast-message-preview').css('background-color', previewColor);
+export default function initBroadcastMessagesForm() {
+ $('input#broadcast_message_color').on('input', function onMessageColorInput() {
+ const previewColor = $(this).val();
+ $('div.broadcast-message-preview').css('background-color', previewColor);
});
- $('input#broadcast_message_font').on('input', function() {
- var previewColor;
- previewColor = $(this).val();
- return $('div.broadcast-message-preview').css('color', previewColor);
+
+ $('input#broadcast_message_font').on('input', function onMessageFontInput() {
+ const previewColor = $(this).val();
+ $('div.broadcast-message-preview').css('color', previewColor);
});
- previewPath = $('textarea#broadcast_message_message').data('preview-path');
- return $('textarea#broadcast_message_message').on('input', function() {
- var message;
- message = $(this).val();
+
+ const previewPath = $('textarea#broadcast_message_message').data('preview-path');
+
+ $('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() {
+ const message = $(this).val();
if (message === '') {
- return $('.js-broadcast-message-preview').text("Your message here");
+ $('.js-broadcast-message-preview').text('Your message here');
} else {
- return $.ajax({
+ $.ajax({
url: previewPath,
- type: "POST",
+ type: 'POST',
data: {
- broadcast_message: {
- message: message
- }
- }
+ broadcast_message: { message },
+ },
});
}
- });
-});
+ }, 250));
+}
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
deleted file mode 100644
index 3d27a3544eb..00000000000
--- a/app/assets/javascripts/build.js
+++ /dev/null
@@ -1,289 +0,0 @@
-/* eslint-disable func-names, wrap-iife, no-use-before-define,
-consistent-return, prefer-rest-params */
-import _ from 'underscore';
-import bp from './breakpoints';
-import { bytesToKiB } from './lib/utils/number_utils';
-import { setCiStatusFavicon } from './lib/utils/common_utils';
-
-window.Build = (function () {
- Build.timeout = null;
- Build.state = null;
-
- function Build(options) {
- this.options = options || $('.js-build-options').data();
-
- this.pageUrl = this.options.pageUrl;
- this.buildStatus = this.options.buildStatus;
- this.state = this.options.logState;
- this.buildStage = this.options.buildStage;
- this.$document = $(document);
- this.logBytes = 0;
- this.hasBeenScrolled = false;
-
- this.updateDropdown = this.updateDropdown.bind(this);
- this.getBuildTrace = this.getBuildTrace.bind(this);
-
- this.$buildTrace = $('#build-trace');
- this.$buildRefreshAnimation = $('.js-build-refresh');
- this.$truncatedInfo = $('.js-truncated-info');
- this.$buildTraceOutput = $('.js-build-output');
- this.$topBar = $('.js-top-bar');
-
- // Scroll controllers
- this.$scrollTopBtn = $('.js-scroll-up');
- this.$scrollBottomBtn = $('.js-scroll-down');
-
- clearTimeout(Build.timeout);
-
- this.initSidebar();
- this.populateJobs(this.buildStage);
- this.updateStageDropdownText(this.buildStage);
- this.sidebarOnResize();
-
- this.$document
- .off('click', '.js-sidebar-build-toggle')
- .on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
-
- this.$document
- .off('click', '.stage-item')
- .on('click', '.stage-item', this.updateDropdown);
-
- // add event listeners to the scroll buttons
- this.$scrollTopBtn
- .off('click')
- .on('click', this.scrollToTop.bind(this));
-
- this.$scrollBottomBtn
- .off('click')
- .on('click', this.scrollToBottom.bind(this));
-
- this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
-
- $(window)
- .off('scroll')
- .on('scroll', () => {
- const contentHeight = this.$buildTraceOutput.height();
- if (contentHeight > this.windowSize) {
- // means the user did not scroll, the content was updated.
- this.windowSize = contentHeight;
- } else {
- // User scrolled
- this.hasBeenScrolled = true;
- this.toggleScrollAnimation(false);
- }
-
- this.scrollThrottled();
- });
-
- $(window)
- .off('resize.build')
- .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
-
- this.updateArtifactRemoveDate();
- this.initAffixTopArea();
-
- this.getBuildTrace();
- }
-
- Build.prototype.initAffixTopArea = function () {
- /**
- If the browser does not support position sticky, it returns the position as static.
- If the browser does support sticky, then we allow the browser to handle it, if not
- then we default back to Bootstraps affix
- **/
- if (this.$topBar.css('position') !== 'static') return;
-
- const offsetTop = this.$buildTrace.offset().top;
-
- this.$topBar.affix({
- offset: {
- top: offsetTop,
- },
- });
- };
-
- Build.prototype.canScroll = function () {
- return $(document).height() > $(window).height();
- };
-
- Build.prototype.toggleScroll = function () {
- const currentPosition = $(document).scrollTop();
- const scrollHeight = $(document).height();
-
- const windowHeight = $(window).height();
- if (this.canScroll()) {
- if (currentPosition > 0 &&
- (scrollHeight - currentPosition !== windowHeight)) {
- // User is in the middle of the log
-
- this.toggleDisableButton(this.$scrollTopBtn, false);
- this.toggleDisableButton(this.$scrollBottomBtn, false);
- } else if (currentPosition === 0) {
- // User is at Top of Build Log
-
- this.toggleDisableButton(this.$scrollTopBtn, true);
- this.toggleDisableButton(this.$scrollBottomBtn, false);
- } else if (scrollHeight - currentPosition === windowHeight) {
- // User is at the bottom of the build log.
-
- this.toggleDisableButton(this.$scrollTopBtn, false);
- this.toggleDisableButton(this.$scrollBottomBtn, true);
- }
- } else {
- this.toggleDisableButton(this.$scrollTopBtn, true);
- this.toggleDisableButton(this.$scrollBottomBtn, true);
- }
- };
-
- Build.prototype.scrollDown = function () {
- $(document).scrollTop($(document).height());
- };
-
- Build.prototype.scrollToBottom = function () {
- this.scrollDown();
- this.hasBeenScrolled = true;
- this.toggleScroll();
- };
-
- Build.prototype.scrollToTop = function () {
- $(document).scrollTop(0);
- this.hasBeenScrolled = true;
- this.toggleScroll();
- };
-
- Build.prototype.toggleDisableButton = function ($button, disable) {
- if (disable && $button.prop('disabled')) return;
- $button.prop('disabled', disable);
- };
-
- Build.prototype.toggleScrollAnimation = function (toggle) {
- this.$scrollBottomBtn.toggleClass('animate', toggle);
- };
-
- Build.prototype.initSidebar = function () {
- this.$sidebar = $('.js-build-sidebar');
- };
-
- Build.prototype.getBuildTrace = function () {
- return $.ajax({
- url: `${this.pageUrl}/trace.json`,
- data: { state: this.state },
- })
- .done((log) => {
- setCiStatusFavicon(`${this.pageUrl}/status.json`);
-
- if (log.state) {
- this.state = log.state;
- }
-
- this.windowSize = this.$buildTraceOutput.height();
-
- if (log.append) {
- this.$buildTraceOutput.append(log.html);
- this.logBytes += log.size;
- } else {
- this.$buildTraceOutput.html(log.html);
- this.logBytes = log.size;
- }
-
- // if the incremental sum of logBytes we received is less than the total
- // we need to show a message warning the user about that.
- if (this.logBytes < log.total) {
- // size is in bytes, we need to calculate KiB
- const size = bytesToKiB(this.logBytes);
- $('.js-truncated-info-size').html(`${size}`);
- this.$truncatedInfo.removeClass('hidden');
- } else {
- this.$truncatedInfo.addClass('hidden');
- }
-
- if (!log.complete) {
- if (!this.hasBeenScrolled) {
- this.toggleScrollAnimation(true);
- } else {
- this.toggleScrollAnimation(false);
- }
-
- Build.timeout = setTimeout(() => {
- this.getBuildTrace();
- }, 4000);
- } else {
- this.$buildRefreshAnimation.remove();
- this.toggleScrollAnimation(false);
- }
-
- if (log.status !== this.buildStatus) {
- gl.utils.visitUrl(this.pageUrl);
- }
- })
- .fail(() => {
- this.$buildRefreshAnimation.remove();
- })
- .then(() => {
- if (!this.hasBeenScrolled) {
- this.scrollDown();
- }
- })
- .then(() => this.toggleScroll());
- };
-
- Build.prototype.shouldHideSidebarForViewport = function () {
- const bootstrapBreakpoint = bp.getBreakpointSize();
- return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
- };
-
- Build.prototype.toggleSidebar = function (shouldHide) {
- const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
- const $toggleButton = $('.js-sidebar-build-toggle-header');
-
- this.$sidebar
- .toggleClass('right-sidebar-expanded', shouldShow)
- .toggleClass('right-sidebar-collapsed', shouldHide);
-
- this.$topBar
- .toggleClass('sidebar-expanded', shouldShow)
- .toggleClass('sidebar-collapsed', shouldHide);
-
- if (this.$sidebar.hasClass('right-sidebar-expanded')) {
- $toggleButton.addClass('hidden');
- } else {
- $toggleButton.removeClass('hidden');
- }
- };
-
- Build.prototype.sidebarOnResize = function () {
- this.toggleSidebar(this.shouldHideSidebarForViewport());
- };
-
- Build.prototype.sidebarOnClick = function () {
- if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
- };
-
- Build.prototype.updateArtifactRemoveDate = function () {
- const $date = $('.js-artifacts-remove');
- if ($date.length) {
- const date = $date.text();
- return $date.text(
- gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '),
- );
- }
- };
-
- Build.prototype.populateJobs = function (stage) {
- $('.build-job').hide();
- $(`.build-job[data-stage="${stage}"]`).show();
- };
-
- Build.prototype.updateStageDropdownText = function (stage) {
- $('.stage-selection').text(stage);
- };
-
- Build.prototype.updateDropdown = function (e) {
- e.preventDefault();
- const stage = e.currentTarget.text;
- this.updateStageDropdownText(stage);
- this.populateJobs(stage);
- };
-
- return Build;
-})();
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index 19388f1f9ae..ace89398943 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,30 +1,30 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */
+/* eslint-disable func-names, prefer-arrow-callback, no-return-assign */
import { visitUrl } from './lib/utils/url_utility';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
-window.BuildArtifacts = (function() {
- function BuildArtifacts() {
+export default class BuildArtifacts {
+ constructor() {
this.disablePropagation();
this.setupEntryClick();
this.setupTooltips();
}
-
- BuildArtifacts.prototype.disablePropagation = function() {
- $('.top-block').on('click', '.download', function(e) {
+ // eslint-disable-next-line class-methods-use-this
+ disablePropagation() {
+ $('.top-block').on('click', '.download', function (e) {
return e.stopPropagation();
});
- return $('.tree-holder').on('click', 'tr[data-link] a', function(e) {
+ return $('.tree-holder').on('click', 'tr[data-link] a', function (e) {
return e.stopImmediatePropagation();
});
- };
-
- BuildArtifacts.prototype.setupEntryClick = function() {
- return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
+ }
+ // eslint-disable-next-line class-methods-use-this
+ setupEntryClick() {
+ return $('.tree-holder').on('click', 'tr[data-link]', function () {
visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink));
});
- };
-
- BuildArtifacts.prototype.setupTooltips = function() {
+ }
+ // eslint-disable-next-line class-methods-use-this
+ setupTooltips() {
$('.js-artifact-tree-tooltip').tooltip({
placement: 'bottom',
// Stop the tooltip from hiding when we stop hovering the element directly
@@ -41,7 +41,5 @@ window.BuildArtifacts = (function() {
.on('mouseleave', (e) => {
$(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide');
});
- };
-
- return BuildArtifacts;
-})();
+ }
+}
diff --git a/app/assets/javascripts/build_variables.js b/app/assets/javascripts/build_variables.js
index c955a9ac2ea..35edf3e0017 100644
--- a/app/assets/javascripts/build_variables.js
+++ b/app/assets/javascripts/build_variables.js
@@ -1,8 +1,10 @@
-/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren */
+/* eslint-disable func-names*/
-$(function() {
- $('.reveal-variables').off('click').on('click', function() {
- $('.js-build-variables').toggle();
- $(this).hide();
- });
-});
+export default function handleRevealVariables() {
+ $('.js-reveal-variables')
+ .off('click')
+ .on('click', function () {
+ $('.js-build-variables').toggle();
+ $(this).hide();
+ });
+}
diff --git a/app/assets/javascripts/ci_lint_editor.js b/app/assets/javascripts/ci_lint_editor.js
index dd4a08a2f31..b9469e5b7cb 100644
--- a/app/assets/javascripts/ci_lint_editor.js
+++ b/app/assets/javascripts/ci_lint_editor.js
@@ -1,7 +1,4 @@
-
-window.gl = window.gl || {};
-
-class CILintEditor {
+export default class CILintEditor {
constructor() {
this.editor = window.ace.edit('ci-editor');
this.textarea = document.querySelector('#content');
@@ -13,5 +10,3 @@ class CILintEditor {
});
}
}
-
-gl.CILintEditor = CILintEditor;
diff --git a/app/assets/javascripts/clusters.js b/app/assets/javascripts/clusters.js
deleted file mode 100644
index 50dbeb06362..00000000000
--- a/app/assets/javascripts/clusters.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/* globals Flash */
-import Visibility from 'visibilityjs';
-import axios from 'axios';
-import Poll from './lib/utils/poll';
-import { s__ } from './locale';
-import './flash';
-
-/**
- * Cluster page has 2 separate parts:
- * Toggle button
- *
- * - Polling status while creating or scheduled
- * -- Update status area with the response result
- */
-
-class ClusterService {
- constructor(options = {}) {
- this.options = options;
- }
- fetchData() {
- return axios.get(this.options.endpoint);
- }
-}
-
-export default class Clusters {
- constructor() {
- const dataset = document.querySelector('.js-edit-cluster-form').dataset;
-
- this.state = {
- statusPath: dataset.statusPath,
- clusterStatus: dataset.clusterStatus,
- clusterStatusReason: dataset.clusterStatusReason,
- toggleStatus: dataset.toggleStatus,
- };
-
- this.service = new ClusterService({ endpoint: this.state.statusPath });
- this.toggleButton = document.querySelector('.js-toggle-cluster');
- this.toggleInput = document.querySelector('.js-toggle-input');
- this.errorContainer = document.querySelector('.js-cluster-error');
- this.successContainer = document.querySelector('.js-cluster-success');
- this.creatingContainer = document.querySelector('.js-cluster-creating');
- this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
-
- this.toggleButton.addEventListener('click', this.toggle.bind(this));
-
- if (this.state.clusterStatus !== 'created') {
- this.updateContainer(this.state.clusterStatus, this.state.clusterStatusReason);
- }
-
- if (this.state.statusPath) {
- this.initPolling();
- }
- }
-
- toggle() {
- this.toggleButton.classList.toggle('checked');
- this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
- }
-
- initPolling() {
- this.poll = new Poll({
- resource: this.service,
- method: 'fetchData',
- successCallback: (data) => {
- const { status, status_reason } = data.data;
- this.updateContainer(status, status_reason);
- },
- errorCallback: () => {
- Flash(s__('ClusterIntegration|Something went wrong on our end.'));
- },
- });
-
- if (!Visibility.hidden()) {
- this.poll.makeRequest();
- } else {
- this.service.fetchData();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- this.poll.restart();
- } else {
- this.poll.stop();
- }
- });
- }
-
- hideAll() {
- this.errorContainer.classList.add('hidden');
- this.successContainer.classList.add('hidden');
- this.creatingContainer.classList.add('hidden');
- }
-
- updateContainer(status, error) {
- this.hideAll();
- switch (status) {
- case 'created':
- this.successContainer.classList.remove('hidden');
- break;
- case 'errored':
- this.errorContainer.classList.remove('hidden');
- this.errorReasonContainer.textContent = error;
- break;
- case 'scheduled':
- case 'creating':
- this.creatingContainer.classList.remove('hidden');
- break;
- default:
- this.hideAll();
- }
- }
-}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
new file mode 100644
index 00000000000..dc443475952
--- /dev/null
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -0,0 +1,221 @@
+import Visibility from 'visibilityjs';
+import Vue from 'vue';
+import { s__, sprintf } from '../locale';
+import Flash from '../flash';
+import Poll from '../lib/utils/poll';
+import initSettingsPanels from '../settings_panels';
+import eventHub from './event_hub';
+import {
+ APPLICATION_INSTALLED,
+ REQUEST_LOADING,
+ REQUEST_SUCCESS,
+ REQUEST_FAILURE,
+} from './constants';
+import ClustersService from './services/clusters_service';
+import ClustersStore from './stores/clusters_store';
+import applications from './components/applications.vue';
+
+/**
+ * Cluster page has 2 separate parts:
+ * Toggle button and applications section
+ *
+ * - Polling status while creating or scheduled
+ * - Update status area with the response result
+ */
+
+export default class Clusters {
+ constructor() {
+ const {
+ statusPath,
+ installHelmPath,
+ installIngressPath,
+ installRunnerPath,
+ clusterStatus,
+ clusterStatusReason,
+ helpPath,
+ } = document.querySelector('.js-edit-cluster-form').dataset;
+
+ this.store = new ClustersStore();
+ this.store.setHelpPath(helpPath);
+ this.store.updateStatus(clusterStatus);
+ this.store.updateStatusReason(clusterStatusReason);
+ this.service = new ClustersService({
+ endpoint: statusPath,
+ installHelmEndpoint: installHelmPath,
+ installIngressEndpoint: installIngressPath,
+ installRunnerEndpoint: installRunnerPath,
+ });
+
+ this.toggle = this.toggle.bind(this);
+ this.installApplication = this.installApplication.bind(this);
+
+ this.toggleButton = document.querySelector('.js-toggle-cluster');
+ this.toggleInput = document.querySelector('.js-toggle-input');
+ this.errorContainer = document.querySelector('.js-cluster-error');
+ this.successContainer = document.querySelector('.js-cluster-success');
+ this.creatingContainer = document.querySelector('.js-cluster-creating');
+ this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
+ this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
+
+ initSettingsPanels();
+ this.initApplications();
+
+ if (this.store.state.status !== 'created') {
+ this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
+ }
+
+ this.addListeners();
+ if (statusPath) {
+ this.initPolling();
+ }
+ }
+
+ initApplications() {
+ const store = this.store;
+ const el = document.querySelector('#js-cluster-applications');
+
+ this.applications = new Vue({
+ el,
+ components: {
+ applications,
+ },
+ data() {
+ return {
+ state: store.state,
+ };
+ },
+ render(createElement) {
+ return createElement('applications', {
+ props: {
+ applications: this.state.applications,
+ helpPath: this.state.helpPath,
+ },
+ });
+ },
+ });
+ }
+
+ addListeners() {
+ this.toggleButton.addEventListener('click', this.toggle);
+ eventHub.$on('installApplication', this.installApplication);
+ }
+
+ removeListeners() {
+ this.toggleButton.removeEventListener('click', this.toggle);
+ eventHub.$off('installApplication', this.installApplication);
+ }
+
+ initPolling() {
+ this.poll = new Poll({
+ resource: this.service,
+ method: 'fetchData',
+ successCallback: data => this.handleSuccess(data),
+ errorCallback: () => Clusters.handleError(),
+ });
+
+ if (!Visibility.hidden()) {
+ this.poll.makeRequest();
+ } else {
+ this.service.fetchData()
+ .then(data => this.handleSuccess(data))
+ .catch(() => Clusters.handleError());
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden() && !this.destroyed) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ }
+
+ static handleError() {
+ Flash(s__('ClusterIntegration|Something went wrong on our end.'));
+ }
+
+ handleSuccess(data) {
+ const prevStatus = this.store.state.status;
+ const prevApplicationMap = Object.assign({}, this.store.state.applications);
+
+ this.store.updateStateFromServer(data.data);
+
+ this.checkForNewInstalls(prevApplicationMap, this.store.state.applications);
+ this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason);
+ }
+
+ toggle() {
+ this.toggleButton.classList.toggle('checked');
+ this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
+ }
+
+ hideAll() {
+ this.errorContainer.classList.add('hidden');
+ this.successContainer.classList.add('hidden');
+ this.creatingContainer.classList.add('hidden');
+ }
+
+ checkForNewInstalls(prevApplicationMap, newApplicationMap) {
+ const appTitles = Object.keys(newApplicationMap)
+ .filter(appId => newApplicationMap[appId].status === APPLICATION_INSTALLED &&
+ prevApplicationMap[appId].status !== APPLICATION_INSTALLED &&
+ prevApplicationMap[appId].status !== null)
+ .map(appId => newApplicationMap[appId].title);
+
+ if (appTitles.length > 0) {
+ const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your cluster'), {
+ appList: appTitles.join(', '),
+ });
+ Flash(text, 'notice', this.successApplicationContainer);
+ }
+ }
+
+ updateContainer(prevStatus, status, error) {
+ this.hideAll();
+
+ // We poll all the time but only want the `created` banner to show when newly created
+ if (this.store.state.status !== 'created' || prevStatus !== this.store.state.status) {
+ switch (status) {
+ case 'created':
+ this.successContainer.classList.remove('hidden');
+ break;
+ case 'errored':
+ this.errorContainer.classList.remove('hidden');
+ this.errorReasonContainer.textContent = error;
+ break;
+ case 'scheduled':
+ case 'creating':
+ this.creatingContainer.classList.remove('hidden');
+ break;
+ default:
+ this.hideAll();
+ }
+ }
+ }
+
+ installApplication(appId) {
+ this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
+ this.store.updateAppProperty(appId, 'requestReason', null);
+
+ this.service.installApplication(appId)
+ .then(() => {
+ this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
+ })
+ .catch(() => {
+ this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
+ this.store.updateAppProperty(appId, 'requestReason', s__('ClusterIntegration|Request to begin installing failed'));
+ });
+ }
+
+ destroy() {
+ this.destroyed = true;
+
+ this.removeListeners();
+
+ if (this.poll) {
+ this.poll.stop();
+ }
+
+ this.applications.$destroy();
+ }
+}
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
new file mode 100644
index 00000000000..872abf03ef1
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -0,0 +1,185 @@
+<script>
+import { s__, sprintf } from '../../locale';
+import eventHub from '../event_hub';
+import loadingButton from '../../vue_shared/components/loading_button.vue';
+import {
+ APPLICATION_NOT_INSTALLABLE,
+ APPLICATION_SCHEDULED,
+ APPLICATION_INSTALLABLE,
+ APPLICATION_INSTALLING,
+ APPLICATION_INSTALLED,
+ APPLICATION_ERROR,
+ REQUEST_LOADING,
+ REQUEST_SUCCESS,
+ REQUEST_FAILURE,
+} from '../constants';
+
+export default {
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ titleLink: {
+ type: String,
+ required: false,
+ },
+ description: {
+ type: String,
+ required: true,
+ },
+ status: {
+ type: String,
+ required: false,
+ },
+ statusReason: {
+ type: String,
+ required: false,
+ },
+ requestStatus: {
+ type: String,
+ required: false,
+ },
+ requestReason: {
+ type: String,
+ required: false,
+ },
+ },
+ components: {
+ loadingButton,
+ },
+ computed: {
+ rowJsClass() {
+ return `js-cluster-application-row-${this.id}`;
+ },
+ installButtonLoading() {
+ return !this.status ||
+ this.status === APPLICATION_SCHEDULED ||
+ this.status === APPLICATION_INSTALLING ||
+ this.requestStatus === REQUEST_LOADING;
+ },
+ installButtonDisabled() {
+ // Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but
+ // we already made a request to install and are just waiting for the real-time
+ // to sync up.
+ return (this.status !== APPLICATION_INSTALLABLE && this.status !== APPLICATION_ERROR) ||
+ this.requestStatus === REQUEST_LOADING ||
+ this.requestStatus === REQUEST_SUCCESS;
+ },
+ installButtonLabel() {
+ let label;
+ if (
+ this.status === APPLICATION_NOT_INSTALLABLE ||
+ this.status === APPLICATION_INSTALLABLE ||
+ this.status === APPLICATION_ERROR
+ ) {
+ label = s__('ClusterIntegration|Install');
+ } else if (this.status === APPLICATION_SCHEDULED || this.status === APPLICATION_INSTALLING) {
+ label = s__('ClusterIntegration|Installing');
+ } else if (this.status === APPLICATION_INSTALLED) {
+ label = s__('ClusterIntegration|Installed');
+ }
+
+ return label;
+ },
+ hasError() {
+ return this.status === APPLICATION_ERROR || this.requestStatus === REQUEST_FAILURE;
+ },
+ generalErrorDescription() {
+ return sprintf(
+ s__('ClusterIntegration|Something went wrong while installing %{title}'), {
+ title: this.title,
+ },
+ );
+ },
+ },
+ methods: {
+ installClicked() {
+ eventHub.$emit('installApplication', this.id);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-responsive-table-row gl-responsive-table-row-col-span"
+ :class="rowJsClass"
+ >
+ <div
+ class="gl-responsive-table-row-layout"
+ role="row"
+ >
+ <a
+ v-if="titleLink"
+ :href="titleLink"
+ target="blank"
+ rel="noopener noreferrer"
+ role="gridcell"
+ class="table-section section-15 section-align-top js-cluster-application-title"
+ >
+ {{ title }}
+ </a>
+ <span
+ v-else
+ class="table-section section-15 section-align-top js-cluster-application-title"
+ >
+ {{ title }}
+ </span>
+ <div
+ class="table-section section-wrap"
+ role="gridcell"
+ >
+ <div v-html="description"></div>
+ </div>
+ <div
+ class="table-section table-button-footer section-15 section-align-top"
+ role="gridcell"
+ >
+ <div class="btn-group table-action-buttons">
+ <loading-button
+ class="js-cluster-application-install-button"
+ :loading="installButtonLoading"
+ :disabled="installButtonDisabled"
+ :label="installButtonLabel"
+ @click="installClicked"
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ v-if="hasError"
+ class="gl-responsive-table-row-layout"
+ role="row"
+ >
+ <div
+ class="alert alert-danger alert-block append-bottom-0 table-section section-100"
+ role="gridcell"
+ >
+ <div>
+ <p class="js-cluster-application-general-error-message">
+ {{ generalErrorDescription }}
+ </p>
+ <ul v-if="statusReason || requestReason">
+ <li
+ v-if="statusReason"
+ class="js-cluster-application-status-error-message"
+ >
+ {{ statusReason }}
+ </li>
+ <li
+ v-if="requestReason"
+ class="js-cluster-application-request-error-message"
+ >
+ {{ requestReason }}
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
new file mode 100644
index 00000000000..e5ae439d26e
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -0,0 +1,114 @@
+<script>
+import _ from 'underscore';
+import { s__, sprintf } from '../../locale';
+import applicationRow from './application_row.vue';
+
+export default {
+ props: {
+ applications: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ helpPath: {
+ type: String,
+ required: false,
+ },
+ },
+ components: {
+ applicationRow,
+ },
+ computed: {
+ generalApplicationDescription() {
+ return sprintf(
+ _.escape(s__('ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}')), {
+ helpLink: `<a href="${this.helpPath}">
+ ${_.escape(s__('ClusterIntegration|installing applications'))}
+ </a>`,
+ },
+ false,
+ );
+ },
+ helmTillerDescription() {
+ return _.escape(s__(
+ `ClusterIntegration|Helm streamlines installing and managing Kubernets applications.
+ Tiller runs inside of your Kubernetes Cluster, and manages
+ releases of your charts.`,
+ ));
+ },
+ ingressDescription() {
+ const descriptionParagraph = _.escape(s__(
+ `ClusterIntegration|Ingress gives you a way to route requests to services based on the
+ request host or path, centralizing a number of services into a single entrypoint.`,
+ ));
+
+ const extraCostParagraph = sprintf(
+ _.escape(s__('ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}')), {
+ boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
+ pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
+ ${_.escape(s__('ClusterIntegration|GKE pricing'))}
+ </a>`,
+ },
+ false,
+ );
+
+ return `
+ <p>
+ ${descriptionParagraph}
+ </p>
+ <p class="append-bottom-0">
+ ${extraCostParagraph}
+ </p>
+ `;
+ },
+ gitlabRunnerDescription() {
+ return _.escape(s__(
+ `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs
+ and send the results back to GitLab.`,
+ ));
+ },
+ },
+};
+</script>
+
+<template>
+ <section class="settings no-animate expanded">
+ <div class="settings-header">
+ <h4>
+ {{ s__('ClusterIntegration|Applications') }}
+ </h4>
+ <p
+ class="append-bottom-0"
+ v-html="generalApplicationDescription"
+ >
+ </p>
+ </div>
+
+ <div class="settings-content">
+ <div class="append-bottom-20">
+ <application-row
+ id="helm"
+ :title="applications.helm.title"
+ title-link="https://docs.helm.sh/"
+ :description="helmTillerDescription"
+ :status="applications.helm.status"
+ :status-reason="applications.helm.statusReason"
+ :request-status="applications.helm.requestStatus"
+ :request-reason="applications.helm.requestReason"
+ />
+ <application-row
+ id="ingress"
+ :title="applications.ingress.title"
+ title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
+ :description="ingressDescription"
+ :status="applications.ingress.status"
+ :status-reason="applications.ingress.statusReason"
+ :request-status="applications.ingress.requestStatus"
+ :request-reason="applications.ingress.requestReason"
+ />
+ <!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests -->
+ <!-- Add GitLab Runner row, all other plumbing is complete -->
+ </div>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
new file mode 100644
index 00000000000..93223aefff8
--- /dev/null
+++ b/app/assets/javascripts/clusters/constants.js
@@ -0,0 +1,12 @@
+// These need to match what is returned from the server
+export const APPLICATION_NOT_INSTALLABLE = 'not_installable';
+export const APPLICATION_INSTALLABLE = 'installable';
+export const APPLICATION_SCHEDULED = 'scheduled';
+export const APPLICATION_INSTALLING = 'installing';
+export const APPLICATION_INSTALLED = 'installed';
+export const APPLICATION_ERROR = 'errored';
+
+// These are only used client-side
+export const REQUEST_LOADING = 'request-loading';
+export const REQUEST_SUCCESS = 'request-success';
+export const REQUEST_FAILURE = 'request-failure';
diff --git a/app/assets/javascripts/clusters/event_hub.js b/app/assets/javascripts/clusters/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/clusters/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
new file mode 100644
index 00000000000..ce14c9a9945
--- /dev/null
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -0,0 +1,20 @@
+import axios from '../../lib/utils/axios_utils';
+
+export default class ClusterService {
+ constructor(options = {}) {
+ this.options = options;
+ this.appInstallEndpointMap = {
+ helm: this.options.installHelmEndpoint,
+ ingress: this.options.installIngressEndpoint,
+ runner: this.options.installRunnerEndpoint,
+ };
+ }
+
+ fetchData() {
+ return axios.get(this.options.endpoint);
+ }
+
+ installApplication(appId) {
+ return axios.post(this.appInstallEndpointMap[appId]);
+ }
+}
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
new file mode 100644
index 00000000000..e731cdc3042
--- /dev/null
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -0,0 +1,68 @@
+import { s__ } from '../../locale';
+
+export default class ClusterStore {
+ constructor() {
+ this.state = {
+ helpPath: null,
+ status: null,
+ statusReason: null,
+ applications: {
+ helm: {
+ title: s__('ClusterIntegration|Helm Tiller'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ },
+ ingress: {
+ title: s__('ClusterIntegration|Ingress'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ },
+ runner: {
+ title: s__('ClusterIntegration|GitLab Runner'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ },
+ },
+ };
+ }
+
+ setHelpPath(helpPath) {
+ this.state.helpPath = helpPath;
+ }
+
+ updateStatus(status) {
+ this.state.status = status;
+ }
+
+ updateStatusReason(reason) {
+ this.state.statusReason = reason;
+ }
+
+ updateAppProperty(appId, prop, value) {
+ this.state.applications[appId][prop] = value;
+ }
+
+ updateStateFromServer(serverState = {}) {
+ this.state.status = serverState.status;
+ this.state.statusReason = serverState.status_reason;
+ serverState.applications.forEach((serverAppEntry) => {
+ const {
+ name: appId,
+ status,
+ status_reason: statusReason,
+ } = serverAppEntry;
+
+ this.state.applications[appId] = {
+ ...(this.state.applications[appId] || {}),
+ status,
+ statusReason,
+ };
+ });
+ }
+}
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 0661087a1ba..e9a0dbaa59d 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -25,6 +25,11 @@
type: String,
required: true,
},
+ viewType: {
+ type: String,
+ required: false,
+ default: 'child',
+ },
},
mixins: [
pipelinesMixin,
@@ -110,6 +115,7 @@
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
+ :view-type="viewType"
/>
</div>
</div>
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 047544b1762..9b952ea7b60 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,17 +1,21 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */
+/* eslint-disable func-names, wrap-iife, consistent-return,
+ no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars,
+ prefer-template, object-shorthand, prefer-arrow-callback */
/* global Pager */
-window.CommitsList = (function() {
- var CommitsList = {};
+import { pluralize } from './lib/utils/text_utility';
+
+export default (function () {
+ const CommitsList = {};
CommitsList.timer = null;
- CommitsList.init = function(limit) {
+ CommitsList.init = function (limit) {
this.$contentList = $('.content_list');
- $("body").on("click", ".day-commits-table li.commit", function(e) {
- if (e.target.nodeName !== "A") {
- location.href = $(this).attr("url");
+ $('body').on('click', '.day-commits-table li.commit', function (e) {
+ if (e.target.nodeName !== 'A') {
+ location.href = $(this).attr('url');
e.stopPropagation();
return false;
}
@@ -19,48 +23,47 @@ window.CommitsList = (function() {
Pager.init(parseInt(limit, 10), false, false, this.processCommits);
- this.content = $("#commits-list");
- this.searchField = $("#commits-search");
+ this.content = $('#commits-list');
+ this.searchField = $('#commits-search');
this.lastSearch = this.searchField.val();
return this.initSearch();
};
- CommitsList.initSearch = function() {
+ CommitsList.initSearch = function () {
this.timer = null;
- return this.searchField.keyup((function(_this) {
- return function() {
+ return this.searchField.keyup((function (_this) {
+ return function () {
clearTimeout(_this.timer);
return _this.timer = setTimeout(_this.filterResults, 500);
};
})(this));
};
- CommitsList.filterResults = function() {
- var commitsUrl, form, search;
- form = $(".commits-search-form");
- search = CommitsList.searchField.val();
+ CommitsList.filterResults = function () {
+ const form = $('.commits-search-form');
+ const search = CommitsList.searchField.val();
if (search === CommitsList.lastSearch) return;
- commitsUrl = form.attr("action") + '?' + form.serialize();
+ const commitsUrl = form.attr('action') + '?' + form.serialize();
CommitsList.content.fadeTo('fast', 0.5);
return $.ajax({
- type: "GET",
- url: form.attr("action"),
+ type: 'GET',
+ url: form.attr('action'),
data: form.serialize(),
- complete: function() {
+ complete: function () {
return CommitsList.content.fadeTo('fast', 1.0);
},
- success: function(data) {
+ success: function (data) {
CommitsList.lastSearch = search;
CommitsList.content.html(data.html);
return history.replaceState({
- page: commitsUrl
+ page: commitsUrl,
// Change url so if user reload a page - search results are saved
}, document.title, commitsUrl);
},
- error: function() {
+ error: function () {
CommitsList.lastSearch = null;
},
- dataType: "json"
+ dataType: 'json',
});
};
@@ -81,11 +84,11 @@ window.CommitsList = (function() {
commitsCount = $commitsHeadersLast.nextUntil('li.js-commit-header').find('li.commit').length;
// Remove duplicate of commits header.
- processedData = $processedData.not(`li.js-commit-header[data-day="${loadedShownDayFirst}"]`);
+ processedData = $processedData.not(`li.js-commit-header[data-day='${loadedShownDayFirst}']`);
// Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
- $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${gl.text.pluralize('commit', commitsCount)}`);
+ $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
}
gl.utils.localTimeAgo($processedData.find('.js-timeago'));
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index cb5a9a9f6b5..ff9e4485916 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -10,6 +10,7 @@ import 'core-js/fn/string/from-code-point';
import 'core-js/fn/symbol';
// Browser polyfills
+import 'classlist-polyfill';
import './polyfills/custom_event';
import './polyfills/element';
import './polyfills/event';
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
new file mode 100644
index 00000000000..46b68ebe158
--- /dev/null
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -0,0 +1,81 @@
+import Cookies from 'js-cookie';
+import _ from 'underscore';
+import bp from './breakpoints';
+
+export default class ContextualSidebar {
+ constructor() {
+ this.initDomElements();
+ this.render();
+ }
+
+ initDomElements() {
+ this.$page = $('.page-with-sidebar');
+ this.$sidebar = $('.nav-sidebar');
+ this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar);
+ this.$overlay = $('.mobile-overlay');
+ this.$openSidebar = $('.toggle-mobile-nav');
+ this.$closeSidebar = $('.close-nav-button');
+ this.$sidebarToggle = $('.js-toggle-sidebar');
+ }
+
+ bindEvents() {
+ document.addEventListener('click', (e) => {
+ if (!e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')) {
+ this.toggleCollapsedSidebar(true);
+ }
+ });
+ this.$openSidebar.on('click', () => this.toggleSidebarNav(true));
+ this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
+ this.$overlay.on('click', () => this.toggleSidebarNav(false));
+ this.$sidebarToggle.on('click', () => {
+ const value = !this.$sidebar.hasClass('sidebar-icons-only');
+ this.toggleCollapsedSidebar(value);
+ });
+
+ $(window).on('resize', () => _.debounce(this.render(), 100));
+ }
+
+ static setCollapsedCookie(value) {
+ if (bp.getBreakpointSize() !== 'lg') {
+ return;
+ }
+ Cookies.set('sidebar_collapsed', value, { expires: 365 * 10 });
+ }
+
+ toggleSidebarNav(show) {
+ this.$sidebar.toggleClass('nav-sidebar-expanded', show);
+ this.$overlay.toggleClass('mobile-nav-open', show);
+ this.$sidebar.removeClass('sidebar-icons-only');
+ }
+
+ toggleCollapsedSidebar(collapsed) {
+ const breakpoint = bp.getBreakpointSize();
+
+ if (this.$sidebar.length) {
+ this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
+ this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
+ }
+ ContextualSidebar.setCollapsedCookie(collapsed);
+
+ this.toggleSidebarOverflow();
+ }
+
+ toggleSidebarOverflow() {
+ if (this.$innerScroll.prop('scrollHeight') > this.$innerScroll.prop('offsetHeight')) {
+ this.$innerScroll.css('overflow-y', 'scroll');
+ } else {
+ this.$innerScroll.css('overflow-y', '');
+ }
+ }
+
+ render() {
+ const breakpoint = bp.getBreakpointSize();
+
+ if (breakpoint === 'sm' || breakpoint === 'md') {
+ this.toggleCollapsedSidebar(true);
+ } else if (breakpoint === 'lg') {
+ const collapse = Cookies.get('sidebar_collapsed') === 'true';
+ this.toggleCollapsedSidebar(collapse);
+ }
+ }
+}
diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js
deleted file mode 100644
index 93b0cbf4209..00000000000
--- a/app/assets/javascripts/copy_as_gfm.js
+++ /dev/null
@@ -1,475 +0,0 @@
-/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
-import _ from 'underscore';
-import { insertText, getSelectedFragment, nodeMatchesSelector } from './lib/utils/common_utils';
-import { placeholderImage } from './lazy_loader';
-
-const gfmRules = {
- // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
- // GitLab Flavored Markdown (GFM) to HTML.
- // These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
- // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
- // from GFM should have a handler here, in reverse order.
- // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
- InlineDiffFilter: {
- 'span.idiff.addition'(el, text) {
- return `{+${text}+}`;
- },
- 'span.idiff.deletion'(el, text) {
- return `{-${text}-}`;
- },
- },
- TaskListFilter: {
- 'input[type=checkbox].task-list-item-checkbox'(el) {
- return `[${el.checked ? 'x' : ' '}]`;
- },
- },
- ReferenceFilter: {
- '.tooltip'(el) {
- return '';
- },
- 'a.gfm:not([data-link=true])'(el, text) {
- return el.dataset.original || text;
- },
- },
- AutolinkFilter: {
- 'a'(el, text) {
- // Fallback on the regular MarkdownFilter's `a` handler.
- if (text !== el.getAttribute('href')) return false;
-
- return text;
- },
- },
- TableOfContentsFilter: {
- 'ul.section-nav'(el) {
- return '[[_TOC_]]';
- },
- },
- EmojiFilter: {
- 'img.emoji'(el) {
- return el.getAttribute('alt');
- },
- 'gl-emoji'(el) {
- return `:${el.getAttribute('data-name')}:`;
- },
- },
- ImageLinkFilter: {
- 'a.no-attachment-icon'(el, text) {
- return text;
- },
- },
- ImageLazyLoadFilter: {
- 'img'(el, text) {
- return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
- },
- },
- VideoLinkFilter: {
- '.video-container'(el) {
- const videoEl = el.querySelector('video');
- if (!videoEl) return false;
-
- return CopyAsGFM.nodeToGFM(videoEl);
- },
- 'video'(el) {
- return `![${el.dataset.title}](${el.getAttribute('src')})`;
- },
- },
- MathFilter: {
- 'pre.code.math[data-math-style=display]'(el, text) {
- return `\`\`\`math\n${text.trim()}\n\`\`\``;
- },
- 'code.code.math[data-math-style=inline]'(el, text) {
- return `$\`${text}\`$`;
- },
- 'span.katex-display span.katex-mathml'(el) {
- const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
- if (!mathAnnotation) return false;
-
- return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
- },
- 'span.katex-mathml'(el) {
- const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
- if (!mathAnnotation) return false;
-
- return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
- },
- 'span.katex-html'(el) {
- // We don't want to include the content of this element in the copied text.
- return '';
- },
- 'annotation[encoding="application/x-tex"]'(el, text) {
- return text.trim();
- },
- },
- SanitizationFilter: {
- 'a[name]:not([href]):empty'(el) {
- return el.outerHTML;
- },
- 'dl'(el, text) {
- let lines = text.trim().split('\n');
- // Add two spaces to the front of subsequent list items lines,
- // or leave the line entirely blank.
- lines = lines.map((l) => {
- const line = l.trim();
- if (line.length === 0) return '';
-
- return ` ${line}`;
- });
-
- return `<dl>\n${lines.join('\n')}\n</dl>`;
- },
- 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) {
- const tag = el.nodeName.toLowerCase();
- return `<${tag}>${text}</${tag}>`;
- },
- },
- SyntaxHighlightFilter: {
- 'pre.code.highlight'(el, t) {
- const text = t.trimRight();
-
- let lang = el.getAttribute('lang');
- if (!lang || lang === 'plaintext') {
- lang = '';
- }
-
- // Prefixes lines with 4 spaces if the code contains triple backticks
- if (lang === '' && text.match(/^```/gm)) {
- return text.split('\n').map((l) => {
- const line = l.trim();
- if (line.length === 0) return '';
-
- return ` ${line}`;
- }).join('\n');
- }
-
- return `\`\`\`${lang}\n${text}\n\`\`\``;
- },
- 'pre > code'(el, text) {
- // Don't wrap code blocks in ``
- return text;
- },
- },
- MarkdownFilter: {
- 'br'(el) {
- // Two spaces at the end of a line are turned into a BR
- return ' ';
- },
- 'code'(el, text) {
- let backtickCount = 1;
- const backtickMatch = text.match(/`+/);
- if (backtickMatch) {
- backtickCount = backtickMatch[0].length + 1;
- }
-
- const backticks = Array(backtickCount + 1).join('`');
- const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
-
- return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks;
- },
- 'blockquote'(el, text) {
- return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
- },
- 'img'(el) {
- const imageSrc = el.src;
- const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : (el.dataset.src || '');
- return `![${el.getAttribute('alt')}](${imageUrl})`;
- },
- 'a.anchor'(el, text) {
- // Don't render a Markdown link for the anchor link inside a heading
- return text;
- },
- 'a'(el, text) {
- return `[${text}](${el.getAttribute('href')})`;
- },
- 'li'(el, text) {
- const lines = text.trim().split('\n');
- const firstLine = `- ${lines.shift()}`;
- // Add four spaces to the front of subsequent list items lines,
- // or leave the line entirely blank.
- const nextLines = lines.map((s) => {
- if (s.trim().length === 0) return '';
-
- return ` ${s}`;
- });
-
- return `${firstLine}\n${nextLines.join('\n')}`;
- },
- 'ul'(el, text) {
- return text;
- },
- 'ol'(el, text) {
- // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
- return text.replace(/^- /mg, '1. ');
- },
- 'h1'(el, text) {
- return `# ${text.trim()}`;
- },
- 'h2'(el, text) {
- return `## ${text.trim()}`;
- },
- 'h3'(el, text) {
- return `### ${text.trim()}`;
- },
- 'h4'(el, text) {
- return `#### ${text.trim()}`;
- },
- 'h5'(el, text) {
- return `##### ${text.trim()}`;
- },
- 'h6'(el, text) {
- return `###### ${text.trim()}`;
- },
- 'strong'(el, text) {
- return `**${text}**`;
- },
- 'em'(el, text) {
- return `_${text}_`;
- },
- 'del'(el, text) {
- return `~~${text}~~`;
- },
- 'sup'(el, text) {
- return `^${text}`;
- },
- 'hr'(el) {
- return '-----';
- },
- 'table'(el) {
- const theadEl = el.querySelector('thead');
- const tbodyEl = el.querySelector('tbody');
- if (!theadEl || !tbodyEl) return false;
-
- const theadText = CopyAsGFM.nodeToGFM(theadEl);
- const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
-
- return [theadText, tbodyText].join('\n');
- },
- 'thead'(el, text) {
- const cells = _.map(el.querySelectorAll('th'), (cell) => {
- let chars = CopyAsGFM.nodeToGFM(cell).length + 2;
-
- let before = '';
- let after = '';
- switch (cell.style.textAlign) {
- case 'center':
- before = ':';
- after = ':';
- chars -= 2;
- break;
- case 'right':
- after = ':';
- chars -= 1;
- break;
- default:
- break;
- }
-
- chars = Math.max(chars, 3);
-
- const middle = Array(chars + 1).join('-');
-
- return before + middle + after;
- });
-
- const separatorRow = `|${cells.join('|')}|`;
-
- return [text, separatorRow].join('\n');
- },
- 'tr'(el) {
- const cellEls = el.querySelectorAll('td, th');
- if (cellEls.length === 0) return false;
-
- const cells = _.map(cellEls, cell => CopyAsGFM.nodeToGFM(cell));
- return `| ${cells.join(' | ')} |`;
- },
- },
-};
-
-class CopyAsGFM {
- constructor() {
- $(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
- $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
- $(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM);
- }
-
- static copyAsGFM(e, transformer) {
- const clipboardData = e.originalEvent.clipboardData;
- if (!clipboardData) return;
-
- const documentFragment = getSelectedFragment();
- if (!documentFragment) return;
-
- const el = transformer(documentFragment.cloneNode(true), e.currentTarget);
- if (!el) return;
-
- e.preventDefault();
- e.stopPropagation();
-
- clipboardData.setData('text/plain', el.textContent);
- clipboardData.setData('text/x-gfm', this.nodeToGFM(el));
- }
-
- static pasteGFM(e) {
- const clipboardData = e.originalEvent.clipboardData;
- if (!clipboardData) return;
-
- const text = clipboardData.getData('text/plain');
- const gfm = clipboardData.getData('text/x-gfm');
- if (!gfm) return;
-
- e.preventDefault();
-
- window.gl.utils.insertText(e.target, (textBefore, textAfter) => {
- // If the text before the cursor contains an odd number of backticks,
- // we are either inside an inline code span that starts with 1 backtick
- // or a code block that starts with 3 backticks.
- // This logic still holds when there are one or more _closed_ code spans
- // or blocks that will have 2 or 6 backticks.
- // This will break down when the actual code block contains an uneven
- // number of backticks, but this is a rare edge case.
- const backtickMatch = textBefore.match(/`/g);
- const insideCodeBlock = backtickMatch && (backtickMatch.length % 2) === 1;
-
- if (insideCodeBlock) {
- return text;
- }
-
- return gfm;
- });
- }
-
- static transformGFMSelection(documentFragment) {
- const gfmElements = documentFragment.querySelectorAll('.md, .wiki');
- switch (gfmElements.length) {
- case 0: {
- return documentFragment;
- }
- case 1: {
- return gfmElements[0];
- }
- default: {
- const allGfmElement = document.createElement('div');
-
- for (let i = 0; i < gfmElements.length; i += 1) {
- const gfmElement = gfmElements[i];
- allGfmElement.appendChild(gfmElement);
- allGfmElement.appendChild(document.createTextNode('\n\n'));
- }
-
- return allGfmElement;
- }
- }
- }
-
- static transformCodeSelection(documentFragment, target) {
- let lineSelector = '.line';
-
- if (target) {
- const lineClass = ['left-side', 'right-side'].filter(name => target.classList.contains(name))[0];
- if (lineClass) {
- lineSelector = `.line_content.${lineClass} ${lineSelector}`;
- }
- }
-
- const lineElements = documentFragment.querySelectorAll(lineSelector);
-
- let codeElement;
- if (lineElements.length > 1) {
- codeElement = document.createElement('pre');
- codeElement.className = 'code highlight';
-
- const lang = lineElements[0].getAttribute('lang');
- if (lang) {
- codeElement.setAttribute('lang', lang);
- }
- } else {
- codeElement = document.createElement('code');
- }
-
- if (lineElements.length > 0) {
- for (let i = 0; i < lineElements.length; i += 1) {
- const lineElement = lineElements[i];
- codeElement.appendChild(lineElement);
- codeElement.appendChild(document.createTextNode('\n'));
- }
- } else {
- codeElement.appendChild(documentFragment);
- }
-
- return codeElement;
- }
-
- static nodeToGFM(node, respectWhitespaceParam = false) {
- if (node.nodeType === Node.COMMENT_NODE) {
- return '';
- }
-
- if (node.nodeType === Node.TEXT_NODE) {
- return node.textContent;
- }
-
- const respectWhitespace = respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE');
-
- const text = this.innerGFM(node, respectWhitespace);
-
- if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
- return text;
- }
-
- for (const filter in gfmRules) {
- const rules = gfmRules[filter];
-
- for (const selector in rules) {
- const func = rules[selector];
-
- if (!nodeMatchesSelector(node, selector)) continue;
-
- let result;
- if (func.length === 2) {
- // if `func` takes 2 arguments, it depends on text.
- // if there is no text, we don't need to generate GFM for this node.
- if (text.length === 0) continue;
-
- result = func(node, text);
- } else {
- result = func(node);
- }
-
- if (result === false) continue;
-
- return result;
- }
- }
-
- return text;
- }
-
- static innerGFM(parentNode, respectWhitespace = false) {
- const nodes = parentNode.childNodes;
-
- const clonedParentNode = parentNode.cloneNode(true);
- const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
-
- for (let i = 0; i < nodes.length; i += 1) {
- const node = nodes[i];
- const clonedNode = clonedNodes[i];
-
- const text = this.nodeToGFM(node, respectWhitespace);
-
- // `clonedNode.replaceWith(text)` is not yet widely supported
- clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
- }
-
- let nodeText = clonedParentNode.innerText || clonedParentNode.textContent;
-
- if (!respectWhitespace) {
- nodeText = nodeText.trim();
- }
-
- return nodeText;
- }
-}
-
-window.gl = window.gl || {};
-window.gl.CopyAsGFM = CopyAsGFM;
-
-new CopyAsGFM();
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
deleted file mode 100644
index 1f3c7e1772d..00000000000
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
-
-import Clipboard from 'vendor/clipboard';
-
-var genericError, genericSuccess, showTooltip;
-
-genericSuccess = function(e) {
- showTooltip(e.trigger, 'Copied');
- // Clear the selection and blur the trigger so it loses its border
- e.clearSelection();
- return $(e.trigger).blur();
-};
-
-// Safari doesn't support `execCommand`, so instead we inform the user to
-// copy manually.
-//
-// See http://clipboardjs.com/#browser-support
-genericError = function(e) {
- var key;
- if (/Mac/i.test(navigator.userAgent)) {
- key = '&#8984;'; // Command
- } else {
- key = 'Ctrl';
- }
- return showTooltip(e.trigger, "Press " + key + "-C to copy");
-};
-
-showTooltip = function(target, title) {
- var $target = $(target);
- var originalTitle = $target.data('original-title');
-
- if (!$target.data('hideTooltip')) {
- $target
- .attr('title', 'Copied')
- .tooltip('fixTitle')
- .tooltip('show')
- .attr('title', originalTitle)
- .tooltip('fixTitle');
- }
-};
-
-$(function() {
- const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
- clipboard.on('success', genericSuccess);
- clipboard.on('error', genericError);
-
- // This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM.
- // The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and `gfm` keys into the `data-clipboard-text`
- // attribute that ClipboardJS reads from.
- // When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly` attribute`), sets its value
- // to the value of this data attribute, focusses on it, and finally programmatically issues the 'Copy' command,
- // this code intercepts the copy command/event at the last minute to deconstruct this JSON hash and set the
- // `text/plain` and `text/x-gfm` copy data types to the intended values.
- $(document).on('copy', 'body > textarea[readonly]', function(e) {
- const clipboardData = e.originalEvent.clipboardData;
- if (!clipboardData) return;
-
- const text = e.target.value;
-
- let json;
- try {
- json = JSON.parse(text);
- } catch (ex) {
- return;
- }
-
- if (!json.text || !json.gfm) return;
-
- e.preventDefault();
-
- clipboardData.setData('text/plain', json.text);
- clipboardData.setData('text/x-gfm', json.gfm);
- });
-});
diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js
index 3bed0678350..9a4c9bfcc80 100644
--- a/app/assets/javascripts/create_label.js
+++ b/app/assets/javascripts/create_label.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, prefer-arrow-callback */
import Api from './api';
+import { humanize } from './lib/utils/text_utility';
export default class CreateLabelDropdown {
constructor($el, namespacePath, projectPath) {
@@ -107,7 +108,7 @@ export default class CreateLabelDropdown {
errors = label.message;
} else {
errors = Object.keys(label.message).map(key =>
- `${gl.text.humanize(key)} ${label.message[key].join(', ')}`,
+ `${humanize(key)} ${label.message[key].join(', ')}`,
).join('<br/>');
}
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index ff2f2c81971..23425672b16 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -1,7 +1,8 @@
/* eslint-disable no-new */
-/* global Flash */
+import Flash from './flash';
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
+import { __, sprintf } from './locale';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
@@ -12,28 +13,49 @@ const CREATE_BRANCH = 'create-branch';
export default class CreateMergeRequestDropdown {
constructor(wrapperEl) {
this.wrapperEl = wrapperEl;
+ this.availableButton = this.wrapperEl.querySelector('.available');
+ this.branchInput = this.wrapperEl.querySelector('.js-branch-name');
+ this.branchMessage = this.wrapperEl.querySelector('.js-branch-message');
this.createMergeRequestButton = this.wrapperEl.querySelector('.js-create-merge-request');
- this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
+ this.createTargetButton = this.wrapperEl.querySelector('.js-create-target');
this.dropdownList = this.wrapperEl.querySelector('.dropdown-menu');
- this.availableButton = this.wrapperEl.querySelector('.available');
+ this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
+ this.refInput = this.wrapperEl.querySelector('.js-ref');
+ this.refMessage = this.wrapperEl.querySelector('.js-ref-message');
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
this.unavailableButtonArrow = this.unavailableButton.querySelector('.fa');
this.unavailableButtonText = this.unavailableButton.querySelector('.text');
- this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
+ this.branchCreated = false;
+ this.branchIsValid = true;
this.canCreatePath = this.wrapperEl.dataset.canCreatePath;
+ this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
this.createMrPath = this.wrapperEl.dataset.createMrPath;
this.droplabInitialized = false;
+ this.isCreatingBranch = false;
this.isCreatingMergeRequest = false;
+ this.isGettingRef = false;
this.mergeRequestCreated = false;
- this.isCreatingBranch = false;
- this.branchCreated = false;
+ this.refDebounce = _.debounce((value, target) => this.getRef(value, target), 500);
+ this.refIsValid = true;
+ this.refsPath = this.wrapperEl.dataset.refsPath;
+ this.suggestedRef = this.refInput.value;
- this.init();
- }
+ // These regexps are used to replace
+ // a backend generated new branch name and its source (ref)
+ // with user's inputs.
+ this.regexps = {
+ branch: {
+ createBranchPath: new RegExp('(branch_name=)(.+?)(?=&issue)'),
+ createMrPath: new RegExp('(branch_name=)(.+?)(?=&ref)'),
+ },
+ ref: {
+ createBranchPath: new RegExp('(ref=)(.+?)$'),
+ createMrPath: new RegExp('(ref=)(.+?)$'),
+ },
+ };
- init() {
- this.checkAbilityToCreateBranch();
+ this.init();
}
available() {
@@ -41,41 +63,13 @@ export default class CreateMergeRequestDropdown {
this.unavailableButton.classList.add('hide');
}
- unavailable() {
- this.availableButton.classList.add('hide');
- this.unavailableButton.classList.remove('hide');
- }
-
- enable() {
- this.createMergeRequestButton.classList.remove('disabled');
- this.createMergeRequestButton.removeAttribute('disabled');
-
- this.dropdownToggle.classList.remove('disabled');
- this.dropdownToggle.removeAttribute('disabled');
- }
-
- disable() {
- this.createMergeRequestButton.classList.add('disabled');
- this.createMergeRequestButton.setAttribute('disabled', 'disabled');
-
- this.dropdownToggle.classList.add('disabled');
- this.dropdownToggle.setAttribute('disabled', 'disabled');
- }
-
- hide() {
- this.wrapperEl.classList.add('hide');
- }
-
- setUnavailableButtonState(isLoading = true) {
- if (isLoading) {
- this.unavailableButtonArrow.classList.add('fa-spinner', 'fa-spin');
- this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
- this.unavailableButtonText.textContent = 'Checking branch availability…';
- } else {
- this.unavailableButtonArrow.classList.remove('fa-spinner', 'fa-spin');
- this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
- this.unavailableButtonText.textContent = 'New branch unavailable';
- }
+ bindEvents() {
+ this.createMergeRequestButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
+ this.createTargetButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
+ this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
+ this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
+ this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
+ this.refInput.addEventListener('keydown', CreateMergeRequestDropdown.processTab.bind(this));
}
checkAbilityToCreateBranch() {
@@ -107,49 +101,233 @@ export default class CreateMergeRequestDropdown {
});
}
- initDroplab() {
- this.droplab = new DropLab();
+ createBranch() {
+ return $.ajax({
+ method: 'POST',
+ dataType: 'json',
+ url: this.createBranchPath,
+ beforeSend: () => (this.isCreatingBranch = true),
+ })
+ .done((data) => {
+ this.branchCreated = true;
+ window.location.href = data.url;
+ })
+ .fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
+ }
- this.droplab.init(this.dropdownToggle, this.dropdownList, [InputSetter],
- this.getDroplabConfig());
+ createMergeRequest() {
+ return $.ajax({
+ method: 'POST',
+ dataType: 'json',
+ url: this.createMrPath,
+ beforeSend: () => (this.isCreatingMergeRequest = true),
+ })
+ .done((data) => {
+ this.mergeRequestCreated = true;
+ window.location.href = data.url;
+ })
+ .fail(() => new Flash('Failed to create Merge Request. Please try again.'));
+ }
+
+ disable() {
+ this.disableCreateAction();
+
+ this.dropdownToggle.classList.add('disabled');
+ this.dropdownToggle.setAttribute('disabled', 'disabled');
+ }
+
+ disableCreateAction() {
+ this.createMergeRequestButton.classList.add('disabled');
+ this.createMergeRequestButton.setAttribute('disabled', 'disabled');
+
+ this.createTargetButton.classList.add('disabled');
+ this.createTargetButton.setAttribute('disabled', 'disabled');
+ }
+
+ enable() {
+ this.createMergeRequestButton.classList.remove('disabled');
+ this.createMergeRequestButton.removeAttribute('disabled');
+
+ this.createTargetButton.classList.remove('disabled');
+ this.createTargetButton.removeAttribute('disabled');
+
+ this.dropdownToggle.classList.remove('disabled');
+ this.dropdownToggle.removeAttribute('disabled');
+ }
+
+ static findByValue(objects, ref, returnFirstMatch = false) {
+ if (!objects || !objects.length) return false;
+ if (objects.indexOf(ref) > -1) return ref;
+ if (returnFirstMatch) return objects.find(item => new RegExp(`^${ref}`).test(item));
+
+ return false;
}
getDroplabConfig() {
return {
- InputSetter: [{
- input: this.createMergeRequestButton,
- valueAttribute: 'data-value',
- inputAttribute: 'data-action',
- }, {
- input: this.createMergeRequestButton,
- valueAttribute: 'data-text',
- }],
+ addActiveClassToDropdownButton: true,
+ InputSetter: [
+ {
+ input: this.createMergeRequestButton,
+ valueAttribute: 'data-value',
+ inputAttribute: 'data-action',
+ },
+ {
+ input: this.createMergeRequestButton,
+ valueAttribute: 'data-text',
+ },
+ {
+ input: this.createTargetButton,
+ valueAttribute: 'data-value',
+ inputAttribute: 'data-action',
+ },
+ {
+ input: this.createTargetButton,
+ valueAttribute: 'data-text',
+ },
+ ],
};
}
- bindEvents() {
- this.createMergeRequestButton
- .addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
+ static getInputSelectedText(input) {
+ const start = input.selectionStart;
+ const end = input.selectionEnd;
+
+ return input.value.substr(start, end - start);
+ }
+
+ getRef(ref, target = 'all') {
+ if (!ref) return false;
+
+ return $.ajax({
+ method: 'GET',
+ dataType: 'json',
+ url: this.refsPath + ref,
+ beforeSend: () => {
+ this.isGettingRef = true;
+ },
+ })
+ .always(() => {
+ this.isGettingRef = false;
+ })
+ .done((data) => {
+ const branches = data[Object.keys(data)[0]];
+ const tags = data[Object.keys(data)[1]];
+ let result;
+
+ if (target === 'branch') {
+ result = CreateMergeRequestDropdown.findByValue(branches, ref);
+ } else {
+ result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
+ CreateMergeRequestDropdown.findByValue(tags, ref, true);
+ this.suggestedRef = result;
+ }
+
+ return this.updateInputState(target, ref, result);
+ })
+ .fail(() => {
+ this.unavailable();
+ this.disable();
+ new Flash('Failed to get ref.');
+
+ return false;
+ });
+ }
+
+ getTargetData(target) {
+ return {
+ input: this[`${target}Input`],
+ message: this[`${target}Message`],
+ };
+ }
+
+ hide() {
+ this.wrapperEl.classList.add('hide');
+ }
+
+ init() {
+ this.checkAbilityToCreateBranch();
+ }
+
+ initDroplab() {
+ this.droplab = new DropLab();
+
+ this.droplab.init(
+ this.dropdownToggle,
+ this.dropdownList,
+ [InputSetter],
+ this.getDroplabConfig(),
+ );
+ }
+
+ inputsAreValid() {
+ return this.branchIsValid && this.refIsValid;
}
isBusy() {
return this.isCreatingMergeRequest ||
this.mergeRequestCreated ||
this.isCreatingBranch ||
- this.branchCreated;
+ this.branchCreated ||
+ this.isGettingRef;
}
- onClickCreateMergeRequestButton(e) {
+ onChangeInput(event) {
+ let target;
+ let value;
+
+ if (event.srcElement === this.branchInput) {
+ target = 'branch';
+ value = this.branchInput.value;
+ } else if (event.srcElement === this.refInput) {
+ target = 'ref';
+ value = event.srcElement.value.slice(0, event.srcElement.selectionStart) +
+ event.srcElement.value.slice(event.srcElement.selectionEnd);
+ } else {
+ return false;
+ }
+
+ if (this.isGettingRef) return false;
+
+ // `ENTER` key submits the data.
+ if (event.keyCode === 13 && this.inputsAreValid()) {
+ event.preventDefault();
+ return this.createMergeRequestButton.click();
+ }
+
+ // If the input is empty, use the original value generated by the backend.
+ if (!value) {
+ this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
+ this.createMrPath = this.wrapperEl.dataset.createMrPath;
+
+ if (target === 'branch') {
+ this.branchIsValid = true;
+ } else {
+ this.refIsValid = true;
+ }
+
+ this.enable();
+ this.showAvailableMessage(target);
+ return true;
+ }
+
+ this.showCheckingMessage(target);
+ this.refDebounce(value, target);
+
+ return true;
+ }
+
+ onClickCreateMergeRequestButton(event) {
let xhr = null;
- e.preventDefault();
+ event.preventDefault();
if (this.isBusy()) {
return;
}
- if (e.target.dataset.action === CREATE_MERGE_REQUEST) {
+ if (event.target.dataset.action === CREATE_MERGE_REQUEST) {
xhr = this.createMergeRequest();
- } else if (e.target.dataset.action === CREATE_BRANCH) {
+ } else if (event.target.dataset.action === CREATE_BRANCH) {
xhr = this.createBranch();
}
@@ -163,31 +341,131 @@ export default class CreateMergeRequestDropdown {
this.disable();
}
- createMergeRequest() {
- return $.ajax({
- method: 'POST',
- dataType: 'json',
- url: this.createMrPath,
- beforeSend: () => (this.isCreatingMergeRequest = true),
- })
- .done((data) => {
- this.mergeRequestCreated = true;
- window.location.href = data.url;
- })
- .fail(() => new Flash('Failed to create Merge Request. Please try again.'));
+ onClickSetFocusOnBranchNameInput() {
+ this.branchInput.focus();
}
- createBranch() {
- return $.ajax({
- method: 'POST',
- dataType: 'json',
- url: this.createBranchPath,
- beforeSend: () => (this.isCreatingBranch = true),
- })
- .done((data) => {
- this.branchCreated = true;
- window.location.href = data.url;
- })
- .fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
+ // `TAB` autocompletes the source.
+ static processTab(event) {
+ if (event.keyCode !== 9 || this.isGettingRef) return;
+
+ const selectedText = CreateMergeRequestDropdown.getInputSelectedText(this.refInput);
+
+ // if nothing selected, we don't need to autocomplete anything. Do the default TAB action.
+ // If a user manually selected text, don't autocomplete anything. Do the default TAB action.
+ if (!selectedText || this.refInput.dataset.value === this.suggestedRef) return;
+
+ event.preventDefault();
+ window.getSelection().removeAllRanges();
+ }
+
+ removeMessage(target) {
+ const { input, message } = this.getTargetData(target);
+ const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline'];
+ const messageClasses = ['gl-field-hint', 'gl-field-error-message', 'gl-field-success-message'];
+
+ inputClasses.forEach(cssClass => input.classList.remove(cssClass));
+ messageClasses.forEach(cssClass => message.classList.remove(cssClass));
+ message.style.display = 'none';
+ }
+
+ setUnavailableButtonState(isLoading = true) {
+ if (isLoading) {
+ this.unavailableButtonArrow.classList.add('fa-spin');
+ this.unavailableButtonArrow.classList.add('fa-spinner');
+ this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
+ this.unavailableButtonText.textContent = __('Checking branch availability...');
+ } else {
+ this.unavailableButtonArrow.classList.remove('fa-spin');
+ this.unavailableButtonArrow.classList.remove('fa-spinner');
+ this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
+ this.unavailableButtonText.textContent = __('New branch unavailable');
+ }
+ }
+
+ showAvailableMessage(target) {
+ const { input, message } = this.getTargetData(target);
+ const text = target === 'branch' ? __('Branch name') : __('Source');
+
+ this.removeMessage(target);
+ input.classList.add('gl-field-success-outline');
+ message.classList.add('gl-field-success-message');
+ message.textContent = sprintf(__('%{text} is available'), { text });
+ message.style.display = 'inline-block';
+ }
+
+ showCheckingMessage(target) {
+ const { message } = this.getTargetData(target);
+ const text = target === 'branch' ? __('branch name') : __('source');
+
+ this.removeMessage(target);
+ message.classList.add('gl-field-hint');
+ message.textContent = sprintf(__('Checking %{text} availability…'), { text });
+ message.style.display = 'inline-block';
+ }
+
+ showNotAvailableMessage(target) {
+ const { input, message } = this.getTargetData(target);
+ const text = target === 'branch' ? __('Branch is already taken') : __('Source is not available');
+
+ this.removeMessage(target);
+ input.classList.add('gl-field-error-outline');
+ message.classList.add('gl-field-error-message');
+ message.textContent = text;
+ message.style.display = 'inline-block';
+ }
+
+ unavailable() {
+ this.availableButton.classList.add('hide');
+ this.unavailableButton.classList.remove('hide');
+ }
+
+ updateInputState(target, ref, result) {
+ // target - 'branch' or 'ref' - which the input field we are searching a ref for.
+ // ref - string - what a user typed.
+ // result - string - what has been found on backend.
+
+ const pathReplacement = `$1${ref}`;
+
+ // If a found branch equals exact the same text a user typed,
+ // that means a new branch cannot be created as it already exists.
+ if (ref === result) {
+ if (target === 'branch') {
+ this.branchIsValid = false;
+ this.showNotAvailableMessage('branch');
+ } else {
+ this.refIsValid = true;
+ this.refInput.dataset.value = ref;
+ this.showAvailableMessage('ref');
+ this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
+ pathReplacement);
+ this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
+ pathReplacement);
+ }
+ } else if (target === 'branch') {
+ this.branchIsValid = true;
+ this.showAvailableMessage('branch');
+ this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
+ pathReplacement);
+ this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
+ pathReplacement);
+ } else {
+ this.refIsValid = false;
+ this.refInput.dataset.value = ref;
+ this.disableCreateAction();
+ this.showNotAvailableMessage('ref');
+
+ // Show ref hint.
+ if (result) {
+ this.refInput.value = result;
+ this.refInput.setSelectionRange(ref.length, result.length);
+ }
+ }
+
+ if (this.inputsAreValid()) {
+ this.enable();
+ } else {
+ this.disableCreateAction();
+ }
}
}
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index cdf5e3c0290..49bb6c52180 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -1,7 +1,6 @@
-/* global Flash */
-
import Vue from 'vue';
import Cookies from 'js-cookie';
+import Flash from '../flash';
import Translate from '../vue_shared/translate';
import banner from './components/banner.vue';
import stageCodeComponent from './components/stage_code_component.vue';
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
index 8bf9ae17de0..a8cd8c20f8f 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -1,7 +1,7 @@
/* eslint-disable no-param-reassign */
import { __ } from '../locale';
-import '../lib/utils/text_utility';
+import { dasherize } from '../lib/utils/text_utility';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const EMPTY_STAGE_TEXTS = {
@@ -36,7 +36,7 @@ export default {
});
newData.stages.forEach((item) => {
- const stageSlug = gl.text.dasherize(item.name.toLowerCase());
+ const stageSlug = dasherize(item.name.toLowerCase());
item.active = false;
item.isUserAllowed = data.permissions[stageSlug];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index a663e30dfd0..54e13b79a4f 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,5 +1,5 @@
<script>
- /* global Flash */
+ import Flash from '../../flash';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 6c78662baa7..c8874e48c09 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -1,5 +1,3 @@
-/* eslint-disable class-methods-use-this */
-
import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
@@ -8,7 +6,7 @@ import imageDiffHelper from './image_diff/helpers/index';
const UNFOLD_COUNT = 20;
let isBound = false;
-class Diff {
+export default class Diff {
constructor() {
const $diffFile = $('.files .diff-file');
@@ -104,7 +102,7 @@ class Diff {
}
this.highlightSelectedLine();
}
-
+ // eslint-disable-next-line class-methods-use-this
handleParallelLineDown(e) {
const line = $(e.currentTarget);
const table = line.closest('table');
@@ -116,11 +114,11 @@ class Diff {
table.addClass(`${lineClass}-selected`);
}
}
-
+ // eslint-disable-next-line class-methods-use-this
diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
-
+ // eslint-disable-next-line class-methods-use-this
lineNumbers(line) {
const children = line.find('.diff-line-num').toArray();
if (children.length !== 2) {
@@ -128,7 +126,7 @@ class Diff {
}
return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0);
}
-
+ // eslint-disable-next-line class-methods-use-this
highlightSelectedLine() {
const hash = gl.utils.getLocationHash();
const $diffFiles = $('.diff-file');
@@ -141,6 +139,3 @@ class Diff {
}
}
}
-
-window.gl = window.gl || {};
-window.gl.Diff = Diff;
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index efb6ced9f46..20ddcbfb8bd 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -1,9 +1,9 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, max-len */
/* global CommentsStore */
/* global ResolveService */
-/* global Flash */
import Vue from 'vue';
+import Flash from '../../flash';
const ResolveBtn = Vue.extend({
props: {
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index 2f063f6fe1f..6eae54f830b 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -1,7 +1,7 @@
-/* global Flash */
/* global CommentsStore */
import Vue from 'vue';
+import Flash from '../../flash';
import '../../vue_shared/vue_resource_interceptor';
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index c3349c382ad..a21c92f24d6 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,40 +1,38 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
-/* global ProjectSelect */
-/* global ShortcutsNavigation */
-/* global IssuableIndex */
-/* global ShortcutsIssuable */
-/* global Milestone */
-/* global IssuableForm */
-/* global LabelsSelect */
+import { s__ } from './locale';
+import projectSelect from './project_select';
+import IssuableIndex from './issuable_index';
+import Milestone from './milestone';
+import IssuableForm from './issuable_form';
+import LabelsSelect from './labels_select';
/* global MilestoneSelect */
-/* global CommitsList */
-/* global NewBranchForm */
+import NewBranchForm from './new_branch_form';
/* global NotificationsForm */
/* global NotificationsDropdown */
-/* global GroupAvatar */
+import groupAvatar from './group_avatar';
+import GroupLabelSubscription from './group_label_subscription';
/* global LineHighlighter */
-/* global BuildArtifacts */
-/* global GroupsSelect */
+import BuildArtifacts from './build_artifacts';
+import CILintEditor from './ci_lint_editor';
+import groupsSelect from './groups_select';
/* global Search */
/* global Admin */
-/* global NamespaceSelects */
-/* global NewCommitForm */
-/* global NewBranchForm */
-/* global Project */
-/* global ProjectAvatar */
+import NamespaceSelect from './namespace_select';
+import NewCommitForm from './new_commit_form';
+import Project from './project';
+import projectAvatar from './project_avatar';
/* global MergeRequest */
/* global Compare */
/* global CompareAutocomplete */
/* global ProjectFindFile */
-/* global ProjectNew */
-/* global ProjectShow */
-/* global ProjectImport */
-/* global Labels */
-/* global Shortcuts */
-/* global ShortcutsFindFile */
+import ProjectNew from './project_new';
+import projectImport from './project_import';
+import Labels from './labels';
+import LabelManager from './label_manager';
/* global Sidebar */
-/* global ShortcutsWiki */
+import Flash from './flash';
+import CommitsList from './commits';
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
import DeleteModal from './branches/branches_delete_modal';
@@ -68,6 +66,7 @@ import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me';
import PerformanceBar from './performance_bar';
+import initBroadcastMessagesForm from './broadcast_message';
import initNotes from './init_notes';
import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar';
@@ -75,9 +74,23 @@ import initProjectVisibilitySelector from './project_visibility';
import GpgBadges from './gpg_badges';
import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown';
+import NewGroupChild from './groups/new_group_child';
import AbuseReports from './abuse_reports';
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
import AjaxLoadingSpinner from './ajax_loading_spinner';
+import GlFieldErrors from './gl_field_errors';
+import GLForm from './gl_form';
+import Shortcuts from './shortcuts';
+import ShortcutsNavigation from './shortcuts_navigation';
+import ShortcutsFindFile from './shortcuts_find_file';
+import ShortcutsIssuable from './shortcuts_issuable';
+import U2FAuthenticate from './u2f/authenticate';
+import Members from './members';
+import memberExpirationDate from './member_expiration_date';
+import DueDateSelectors from './due_date_select';
+import Diff from './diff';
+import ProjectLabelSubscription from './project_label_subscription';
+import ProjectVariables from './project_variables';
(function() {
var Dispatcher;
@@ -90,8 +103,8 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
}
Dispatcher.prototype.initPageScripts = function() {
- var page, path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl;
- page = $('body').attr('data-page');
+ var path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl;
+ const page = $('body').attr('data-page');
if (!page) {
return false;
}
@@ -161,11 +174,8 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
- if (page === 'projects:merge_requests:index') {
- new UserCallout({ setCalloutPerProject: true });
- }
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
- IssuableIndex.init(pagePrefix);
+ new IssuableIndex(pagePrefix);
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
@@ -177,7 +187,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
initIssuableSidebar();
break;
case 'dashboard:milestones:index':
- new ProjectSelect();
+ projectSelect();
break;
case 'projects:milestones:show':
case 'groups:milestones:show':
@@ -187,7 +197,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
break;
case 'dashboard:issues':
case 'dashboard:merge_requests':
- new ProjectSelect();
+ projectSelect();
initLegacyFilters();
break;
case 'groups:issues':
@@ -196,7 +206,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
- new ProjectSelect();
+ projectSelect();
break;
case 'dashboard:todos:index':
new Todos();
@@ -223,16 +233,21 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
case 'projects:milestones:new':
case 'projects:milestones:edit':
case 'projects:milestones:update':
+ new ZenMode();
+ new DueDateSelectors();
+ new GLForm($('.milestone-form'), true);
+ break;
case 'groups:milestones:new':
case 'groups:milestones:edit':
case 'groups:milestones:update':
new ZenMode();
- new gl.DueDateSelectors();
- new gl.GLForm($('.milestone-form'), true);
+ new DueDateSelectors();
+ new GLForm($('.milestone-form'), false);
break;
case 'projects:compare:show':
- new gl.Diff();
- initChangesDropdown();
+ new Diff();
+ const paddingTop = 16;
+ initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
break;
case 'projects:branches:new':
case 'projects:branches:create':
@@ -245,7 +260,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
case 'projects:issues:new':
case 'projects:issues:edit':
shortcut_handler = new ShortcutsNavigation();
- new gl.GLForm($('.issue-form'), true);
+ new GLForm($('.issue-form'), true);
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
@@ -267,9 +282,9 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
}
case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit':
- new gl.Diff();
+ new Diff();
shortcut_handler = new ShortcutsNavigation();
- new gl.GLForm($('.merge-request-form'), true);
+ new GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
@@ -278,7 +293,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
break;
case 'projects:tags:new':
new ZenMode();
- new gl.GLForm($('.tag-form'), true);
+ new GLForm($('.tag-form'), true);
new RefSelectDropdown($('.js-branch-select'));
break;
case 'projects:snippets:show':
@@ -288,21 +303,20 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
case 'projects:snippets:edit':
case 'projects:snippets:create':
case 'projects:snippets:update':
- new gl.GLForm($('.snippet-form'), true);
+ new GLForm($('.snippet-form'), true);
break;
case 'snippets:new':
case 'snippets:edit':
case 'snippets:create':
case 'snippets:update':
- new gl.GLForm($('.snippet-form'), false);
+ new GLForm($('.snippet-form'), false);
break;
case 'projects:releases:edit':
new ZenMode();
- new gl.GLForm($('.release-form'), true);
+ new GLForm($('.release-form'), true);
break;
case 'projects:merge_requests:show':
- new gl.Diff();
- shortcut_handler = new ShortcutsIssuable(true);
+ new Diff();
new ZenMode();
initIssuableSidebar();
@@ -312,19 +326,22 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
window.mergeRequest = new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
+
+ shortcut_handler = new ShortcutsIssuable(true);
break;
case 'dashboard:activity':
new gl.Activities();
break;
case 'projects:commit:show':
- new gl.Diff();
+ new Diff();
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph',
}).bindEvents();
initNotes();
- initChangesDropdown();
+ const stickyBarPaddingTop = 16;
+ initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop);
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
break;
case 'projects:commit:pipelines':
@@ -345,7 +362,10 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
- new UserCallout({ setCalloutPerProject: true });
+ new UserCallout({
+ setCalloutPerProject: true,
+ className: 'js-autodevops-banner',
+ });
if ($('#tree-slider').length) new TreeView();
if ($('.blob-viewer').length) new BlobViewer();
@@ -360,14 +380,12 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
initSettingsPanels();
break;
case 'projects:imports:show':
- new ProjectImport();
+ projectImport();
break;
case 'projects:pipelines:new':
+ case 'projects:pipelines:create':
new NewBranchForm($('.js-new-pipeline-form'));
break;
- case 'projects:pipelines:index':
- new UserCallout({ setCalloutPerProject: true });
- break;
case 'projects:pipelines:builds':
case 'projects:pipelines:failures':
case 'projects:pipelines:show':
@@ -388,21 +406,26 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
new gl.Activities();
break;
case 'groups:show':
+ const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
new NotificationsDropdown();
new ProjectsList();
+
+ if (newGroupChildWrapper) {
+ new NewGroupChild(newGroupChildWrapper);
+ }
break;
case 'groups:group_members:index':
- new gl.MemberExpirationDate();
- new gl.Members();
+ memberExpirationDate();
+ new Members();
new UsersSelect();
break;
case 'projects:project_members:index':
- new gl.MemberExpirationDate('.js-access-expiration-date-groups');
- new GroupsSelect();
- new gl.MemberExpirationDate();
- new gl.Members();
+ memberExpirationDate('.js-access-expiration-date-groups');
+ groupsSelect();
+ memberExpirationDate();
+ new Members();
new UsersSelect();
break;
case 'groups:new':
@@ -411,11 +434,11 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
case 'admin:groups:create':
BindInOut.initAll();
new Group();
- new GroupAvatar();
+ groupAvatar();
break;
case 'groups:edit':
case 'admin:groups:edit':
- new GroupAvatar();
+ groupAvatar();
break;
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
@@ -425,7 +448,6 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
new TreeView();
new BlobViewer();
new NewCommitForm($('.js-create-dir-form'));
- new UserCallout({ setCalloutPerProject: true });
$('#tree-slider').waitForImages(function() {
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
@@ -457,15 +479,15 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
case 'groups:labels:index':
case 'projects:labels:index':
if ($('.prioritized-labels').length) {
- new gl.LabelManager();
+ new LabelManager();
}
$('.label-subscription').each((i, el) => {
const $el = $(el);
if ($el.find('.dropdown-group-label').length) {
- new gl.GroupLabelSubscription($el);
+ new GroupLabelSubscription($el);
} else {
- new gl.ProjectLabelSubscription($el);
+ new ProjectLabelSubscription($el);
}
});
break;
@@ -500,12 +522,19 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
case 'projects:settings:ci_cd:show':
// Initialize expandable settings panels
initSettingsPanels();
+
+ import(/* webpackChunkName: "ci-cd-settings" */ './projects/ci_cd_settings_bundle')
+ .then(ciCdSettings => ciCdSettings.default())
+ .catch((err) => {
+ Flash(s__('ProjectSettings|Problem setting up the CI/CD settings JavaScript'));
+ throw err;
+ });
case 'groups:settings:ci_cd:show':
- new gl.ProjectVariables();
+ new ProjectVariables();
break;
case 'ci:lints:create':
case 'ci:lints:show':
- new gl.CILintEditor();
+ new CILintEditor();
break;
case 'users:show':
new UserCallout();
@@ -523,29 +552,37 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
break;
case 'profiles:personal_access_tokens:index':
case 'admin:impersonation_tokens:index':
- new gl.DueDateSelectors();
+ new DueDateSelectors();
break;
case 'projects:clusters:show':
- import(/* webpackChunkName: "clusters" */ './clusters')
+ import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle')
.then(cluster => new cluster.default()) // eslint-disable-line new-cap
- .catch(() => {});
+ .catch((err) => {
+ Flash(s__('ClusterIntegration|Problem setting up the cluster JavaScript'));
+ throw err;
+ });
break;
}
switch (path[0]) {
case 'sessions':
case 'omniauth_callbacks':
if (!gon.u2f) break;
- gl.u2fAuthenticate = new gl.U2FAuthenticate(
+ const u2fAuthenticate = new U2FAuthenticate(
$('#js-authenticate-u2f'),
'#js-login-u2f-form',
gon.u2f,
document.querySelector('#js-login-2fa-device'),
document.querySelector('.js-2fa-form'),
);
- gl.u2fAuthenticate.start();
+ u2fAuthenticate.start();
+ // needed in rspec
+ gl.u2fAuthenticate = u2fAuthenticate;
case 'admin':
new Admin();
switch (path[1]) {
+ case 'broadcast_messages':
+ initBroadcastMessagesForm();
+ break;
case 'cohorts':
new UsagePing();
break;
@@ -553,7 +590,8 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
new UsersSelect();
break;
case 'projects':
- new NamespaceSelects();
+ document.querySelectorAll('.js-namespace-select')
+ .forEach(dropdown => new NamespaceSelect({ dropdown }));
break;
case 'labels':
switch (path[2]) {
@@ -576,7 +614,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
break;
case 'projects':
new Project();
- new ProjectAvatar();
+ projectAvatar();
switch (path[1]) {
case 'compare':
new CompareAutocomplete();
@@ -595,14 +633,13 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
case 'show':
new Star();
new ProjectNew();
- new ProjectShow();
new NotificationsDropdown();
break;
case 'wikis':
new Wikis();
shortcut_handler = new ShortcutsWiki();
new ZenMode();
- new gl.GLForm($('.wiki-form'), true);
+ new GLForm($('.wiki-form'), true);
break;
case 'snippets':
shortcut_handler = new ShortcutsNavigation();
@@ -627,12 +664,6 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
shortcut_handler = new ShortcutsNavigation();
}
break;
- case 'users':
- const action = path[1];
- import(/* webpackChunkName: 'user_profile' */ './users')
- .then(user => user.default(action))
- .catch(() => {});
- break;
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
@@ -653,7 +684,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner';
Dispatcher.prototype.initFieldErrors = function() {
$('.gl-show-field-errors').each((i, form) => {
- new gl.GlFieldErrors(form);
+ new GlFieldErrors(form);
});
};
diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/droplab/constants.js
index 868d47e91b3..673e9bb4c0f 100644
--- a/app/assets/javascripts/droplab/constants.js
+++ b/app/assets/javascripts/droplab/constants.js
@@ -3,6 +3,7 @@ const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active';
const IGNORE_CLASS = 'droplab-item-ignore';
+const IGNORE_HIDING_CLASS = 'droplab-item-ignore-hiding';
// Matches `{{anything}}` and `{{ everything }}`.
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
@@ -13,4 +14,5 @@ export {
ACTIVE_CLASS,
TEMPLATE_REGEX,
IGNORE_CLASS,
+ IGNORE_HIDING_CLASS,
};
diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js
index 3901bb177fe..5eb0a339a1c 100644
--- a/app/assets/javascripts/droplab/drop_down.js
+++ b/app/assets/javascripts/droplab/drop_down.js
@@ -1,15 +1,18 @@
import utils from './utils';
-import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
+import { SELECTED_CLASS, IGNORE_CLASS, IGNORE_HIDING_CLASS } from './constants';
class DropDown {
- constructor(list) {
+ constructor(list, config = {}) {
this.currentIndex = 0;
this.hidden = true;
this.list = typeof list === 'string' ? document.querySelector(list) : list;
this.items = [];
-
this.eventWrapper = {};
+ if (config.addActiveClassToDropdownButton) {
+ this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle');
+ }
+
this.getItems();
this.initTemplateString();
this.addEvents();
@@ -42,7 +45,7 @@ class DropDown {
this.addSelectedClass(selected);
e.preventDefault();
- this.hide();
+ if (!e.target.classList.contains(IGNORE_HIDING_CLASS)) this.hide();
const listEvent = new CustomEvent('click.dl', {
detail: {
@@ -67,7 +70,20 @@ class DropDown {
addEvents() {
this.eventWrapper.clickEvent = this.clickEvent.bind(this);
+ this.eventWrapper.closeDropdown = this.closeDropdown.bind(this);
+
this.list.addEventListener('click', this.eventWrapper.clickEvent);
+ this.list.addEventListener('keyup', this.eventWrapper.closeDropdown);
+ }
+
+ closeDropdown(event) {
+ // `ESC` key closes the dropdown.
+ if (event.keyCode === 27) {
+ event.preventDefault();
+ return this.toggle();
+ }
+
+ return true;
}
setData(data) {
@@ -110,6 +126,8 @@ class DropDown {
this.list.style.display = 'block';
this.currentIndex = 0;
this.hidden = false;
+
+ if (this.dropdownToggle) this.dropdownToggle.classList.add('active');
}
hide() {
@@ -117,6 +135,8 @@ class DropDown {
this.list.style.display = 'none';
this.currentIndex = 0;
this.hidden = true;
+
+ if (this.dropdownToggle) this.dropdownToggle.classList.remove('active');
}
toggle() {
@@ -128,6 +148,7 @@ class DropDown {
destroy() {
this.hide();
this.list.removeEventListener('click', this.eventWrapper.clickEvent);
+ this.list.removeEventListener('keyup', this.eventWrapper.closeDropdown);
}
static setImagesSrc(template) {
diff --git a/app/assets/javascripts/droplab/hook.js b/app/assets/javascripts/droplab/hook.js
index cf78165b0d8..8a8dcde9f88 100644
--- a/app/assets/javascripts/droplab/hook.js
+++ b/app/assets/javascripts/droplab/hook.js
@@ -3,7 +3,7 @@ import DropDown from './drop_down';
class Hook {
constructor(trigger, list, plugins, config) {
this.trigger = trigger;
- this.list = new DropDown(list);
+ this.list = new DropDown(list, config);
this.type = 'Hook';
this.event = 'click';
this.plugins = plugins || [];
diff --git a/app/assets/javascripts/droplab/plugins/filter.js b/app/assets/javascripts/droplab/plugins/filter.js
index d6a1aadd49c..404d707cf7a 100644
--- a/app/assets/javascripts/droplab/plugins/filter.js
+++ b/app/assets/javascripts/droplab/plugins/filter.js
@@ -79,8 +79,6 @@ const Filter = {
this.hook.trigger.addEventListener('keydown.dl', this.eventWrapper.debounceKeydown);
this.hook.trigger.addEventListener('mousedown.dl', this.eventWrapper.debounceKeydown);
-
- this.debounceKeydown({ detail: { hook: this.hook } });
},
destroy: function destroy() {
diff --git a/app/assets/javascripts/droplab/utils.js b/app/assets/javascripts/droplab/utils.js
index 4da7344604e..bfe056a0fcc 100644
--- a/app/assets/javascripts/droplab/utils.js
+++ b/app/assets/javascripts/droplab/utils.js
@@ -30,7 +30,7 @@ const utils = {
},
isDropDownParts(target) {
- if (!target || target.tagName === 'HTML') return false;
+ if (!target || !target.hasAttribute || target.tagName === 'HTML') return false;
return target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN);
},
};
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 1cba65d17cd..c84be42649a 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,305 +1,277 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, prefer-arrow-callback */
-/* global Dropzone */
+import Dropzone from 'dropzone';
import _ from 'underscore';
import './preview_markdown';
import csrf from './lib/utils/csrf';
-window.DropzoneInput = (function() {
- function DropzoneInput(form) {
- const divHover = '<div class="div-dropzone-hover"></div>';
- const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
- const $attachButton = form.find('.button-attach-file');
- const $attachingFileMessage = form.find('.attaching-file-message');
- const $cancelButton = form.find('.button-cancel-uploading-files');
- const $retryLink = form.find('.retry-uploading-link');
- const $uploadProgress = form.find('.uploading-progress');
- const $uploadingErrorContainer = form.find('.uploading-error-container');
- const $uploadingErrorMessage = form.find('.uploading-error-message');
- const $uploadingProgressContainer = form.find('.uploading-progress-container');
- const uploadsPath = window.uploads_path || null;
- const maxFileSize = gon.max_file_size || 10;
- const formTextarea = form.find('.js-gfm-input');
- let handlePaste;
- let pasteText;
- let addFileToForm;
- let updateAttachingMessage;
- let isImage;
- let getFilename;
- let uploadFile;
-
- formTextarea.wrap('<div class="div-dropzone"></div>');
- formTextarea.on('paste', (function(_this) {
- return function(event) {
- return handlePaste(event);
- };
- })(this));
-
- // Add dropzone area to the form.
- const $mdArea = formTextarea.closest('.md-area');
- form.setupMarkdownPreview();
- const $formDropzone = form.find('.div-dropzone');
- $formDropzone.parent().addClass('div-dropzone-wrapper');
- $formDropzone.append(divHover);
- $formDropzone.find('.div-dropzone-hover').append(iconPaperclip);
-
- if (!uploadsPath) return;
-
- const dropzone = $formDropzone.dropzone({
- url: uploadsPath,
- dictDefaultMessage: '',
- clickable: true,
- paramName: 'file',
- maxFilesize: maxFileSize,
- uploadMultiple: false,
- headers: csrf.headers,
- previewContainer: false,
- processing: function() {
- return $('.div-dropzone-alert').alert('close');
- },
- dragover: function() {
- $mdArea.addClass('is-dropzone-hover');
- form.find('.div-dropzone-hover').css('opacity', 0.7);
- },
- dragleave: function() {
- $mdArea.removeClass('is-dropzone-hover');
- form.find('.div-dropzone-hover').css('opacity', 0);
- },
- drop: function() {
- $mdArea.removeClass('is-dropzone-hover');
- form.find('.div-dropzone-hover').css('opacity', 0);
- formTextarea.focus();
- },
- success: function(header, response) {
- const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length;
- const shouldPad = processingFileCount >= 1;
-
- pasteText(response.link.markdown, shouldPad);
- // Show 'Attach a file' link only when all files have been uploaded.
- if (!processingFileCount) $attachButton.removeClass('hide');
- addFileToForm(response.link.url);
- },
- error: function(file, errorMessage = 'Attaching the file failed.', xhr) {
- // If 'error' event is fired by dropzone, the second parameter is error message.
- // If the 'errorMessage' parameter is empty, the default error message is set.
- // If the 'error' event is fired by backend (xhr) error response, the third parameter is
- // xhr object (xhr.responseText is error message).
- // On error we hide the 'Attach' and 'Cancel' buttons
- // and show an error.
-
- // If there's xhr error message, let's show it instead of dropzone's one.
- const message = xhr ? xhr.responseText : errorMessage;
-
- $uploadingErrorContainer.removeClass('hide');
- $uploadingErrorMessage.html(message);
- $attachButton.addClass('hide');
- $cancelButton.addClass('hide');
- },
- totaluploadprogress: function(totalUploadProgress) {
- updateAttachingMessage(this.files, $attachingFileMessage);
- $uploadProgress.text(Math.round(totalUploadProgress) + '%');
- },
- sending: function(file) {
- // DOM elements already exist.
- // Instead of dynamically generating them,
- // we just either hide or show them.
- $attachButton.addClass('hide');
- $uploadingErrorContainer.addClass('hide');
- $uploadingProgressContainer.removeClass('hide');
- $cancelButton.removeClass('hide');
- },
- removedfile: function() {
- $attachButton.removeClass('hide');
- $cancelButton.addClass('hide');
- $uploadingProgressContainer.addClass('hide');
- $uploadingErrorContainer.addClass('hide');
- },
- queuecomplete: function() {
- $('.dz-preview').remove();
- $('.markdown-area').trigger('input');
-
- $uploadingProgressContainer.addClass('hide');
- $cancelButton.addClass('hide');
- }
- });
-
- const child = $(dropzone[0]).children('textarea');
-
- // removeAllFiles(true) stops uploading files (if any)
- // and remove them from dropzone files queue.
- $cancelButton.on('click', (e) => {
- const target = e.target.closest('.js-main-target-form').querySelector('.div-dropzone');
-
- e.preventDefault();
- e.stopPropagation();
- Dropzone.forElement(target).removeAllFiles(true);
- });
-
- // If 'error' event is fired, we store a failed files,
- // clear dropzone files queue, change status of failed files to undefined,
- // 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('.js-main-target-form').querySelector('.div-dropzone'));
- const failedFiles = dropzoneInstance.files;
-
- e.preventDefault();
-
- // 'true' parameter of removeAllFiles() cancels uploading of files that are being uploaded at the moment.
- dropzoneInstance.removeAllFiles(true);
-
- failedFiles.map((failedFile, i) => {
- const file = failedFile;
-
- if (file.status === Dropzone.ERROR) {
- file.status = undefined;
- file.accepted = undefined;
- }
-
- return dropzoneInstance.addFile(file);
- });
- });
-
- handlePaste = function(event) {
- var filename, image, pasteEvent, text;
- pasteEvent = event.originalEvent;
- if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
- image = isImage(pasteEvent);
- if (image) {
- event.preventDefault();
- filename = getFilename(pasteEvent) || 'image.png';
- text = `{{${filename}}}`;
- pasteText(text);
- return uploadFile(image.getAsFile(), filename);
- }
- }
- };
-
- isImage = function(data) {
- var i, item;
- i = 0;
- while (i < data.clipboardData.items.length) {
- item = data.clipboardData.items[i];
- if (item.type.indexOf('image') !== -1) {
- return item;
- }
- i += 1;
- }
- return false;
- };
-
- pasteText = function(text, shouldPad) {
- var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
- var formattedText = text;
- if (shouldPad) formattedText += "\n\n";
- const textarea = child.get(0);
- caretStart = textarea.selectionStart;
- caretEnd = textarea.selectionEnd;
- textEnd = $(child).val().length;
- beforeSelection = $(child).val().substring(0, caretStart);
- afterSelection = $(child).val().substring(caretEnd, textEnd);
- $(child).val(beforeSelection + formattedText + afterSelection);
- textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
- textarea.style.height = `${textarea.scrollHeight}px`;
- formTextarea.get(0).dispatchEvent(new Event('input'));
- return formTextarea.trigger('input');
- };
-
- addFileToForm = function(path) {
- $(form).append('<input type="hidden" name="files[]" value="' + _.escape(path) + '">');
- };
-
- getFilename = function(e) {
- var value;
- if (window.clipboardData && window.clipboardData.getData) {
- value = window.clipboardData.getData('Text');
- } else if (e.clipboardData && e.clipboardData.getData) {
- value = e.clipboardData.getData('text/plain');
- }
- value = value.split("\r");
- return value[0];
- };
-
- const showSpinner = function(e) {
- return $uploadingProgressContainer.removeClass('hide');
- };
+export default function dropzoneInput(form) {
+ const divHover = '<div class="div-dropzone-hover"></div>';
+ const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
+ const $attachButton = form.find('.button-attach-file');
+ const $attachingFileMessage = form.find('.attaching-file-message');
+ const $cancelButton = form.find('.button-cancel-uploading-files');
+ const $retryLink = form.find('.retry-uploading-link');
+ const $uploadProgress = form.find('.uploading-progress');
+ const $uploadingErrorContainer = form.find('.uploading-error-container');
+ const $uploadingErrorMessage = form.find('.uploading-error-message');
+ const $uploadingProgressContainer = form.find('.uploading-progress-container');
+ const uploadsPath = window.uploads_path || null;
+ const maxFileSize = gon.max_file_size || 10;
+ const formTextarea = form.find('.js-gfm-input');
+ let handlePaste;
+ let pasteText;
+ let addFileToForm;
+ let updateAttachingMessage;
+ let isImage;
+ let getFilename;
+ let uploadFile;
+
+ formTextarea.wrap('<div class="div-dropzone"></div>');
+ formTextarea.on('paste', event => handlePaste(event));
+
+ // Add dropzone area to the form.
+ const $mdArea = formTextarea.closest('.md-area');
+ form.setupMarkdownPreview();
+ const $formDropzone = form.find('.div-dropzone');
+ $formDropzone.parent().addClass('div-dropzone-wrapper');
+ $formDropzone.append(divHover);
+ $formDropzone.find('.div-dropzone-hover').append(iconPaperclip);
+
+ if (!uploadsPath) {
+ $formDropzone.addClass('js-invalid-dropzone');
+ return;
+ }
- const closeSpinner = function() {
- return $uploadingProgressContainer.addClass('hide');
- };
+ const dropzone = $formDropzone.dropzone({
+ url: uploadsPath,
+ dictDefaultMessage: '',
+ clickable: true,
+ paramName: 'file',
+ maxFilesize: maxFileSize,
+ uploadMultiple: false,
+ headers: csrf.headers,
+ previewContainer: false,
+ processing: () => $('.div-dropzone-alert').alert('close'),
+ dragover: () => {
+ $mdArea.addClass('is-dropzone-hover');
+ form.find('.div-dropzone-hover').css('opacity', 0.7);
+ },
+ dragleave: () => {
+ $mdArea.removeClass('is-dropzone-hover');
+ form.find('.div-dropzone-hover').css('opacity', 0);
+ },
+ drop: () => {
+ $mdArea.removeClass('is-dropzone-hover');
+ form.find('.div-dropzone-hover').css('opacity', 0);
+ formTextarea.focus();
+ },
+ success(header, response) {
+ const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length;
+ const shouldPad = processingFileCount >= 1;
+
+ pasteText(response.link.markdown, shouldPad);
+ // Show 'Attach a file' link only when all files have been uploaded.
+ if (!processingFileCount) $attachButton.removeClass('hide');
+ addFileToForm(response.link.url);
+ },
+ error: (file, errorMessage = 'Attaching the file failed.', xhr) => {
+ // If 'error' event is fired by dropzone, the second parameter is error message.
+ // If the 'errorMessage' parameter is empty, the default error message is set.
+ // If the 'error' event is fired by backend (xhr) error response, the third parameter is
+ // xhr object (xhr.responseText is error message).
+ // On error we hide the 'Attach' and 'Cancel' buttons
+ // and show an error.
+
+ // If there's xhr error message, let's show it instead of dropzone's one.
+ const message = xhr ? xhr.responseText : errorMessage;
- const showError = function(message) {
$uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message);
- };
-
- const closeAlertMessage = function() {
- return form.find('.div-dropzone-alert').alert('close');
- };
-
- const insertToTextArea = function(filename, url) {
- return $(child).val(function(index, val) {
- return val.replace(`{{${filename}}}`, url);
- });
- };
-
- const appendToTextArea = function(url) {
- return $(child).val(function(index, val) {
- return val + url + "\n";
- });
- };
-
- uploadFile = function(item, filename) {
- var formData;
- formData = new FormData();
- formData.append('file', item, filename);
- return $.ajax({
- url: uploadsPath,
- type: 'POST',
- data: formData,
- dataType: 'json',
- processData: false,
- contentType: false,
- headers: csrf.headers,
- beforeSend: function() {
- showSpinner();
- return closeAlertMessage();
- },
- success: function(e, textStatus, response) {
- return insertToTextArea(filename, response.responseJSON.link.markdown);
- },
- error: function(response) {
- return showError(response.responseJSON.message);
- },
- complete: function() {
- return closeSpinner();
- }
- });
- };
-
- updateAttachingMessage = (files, messageContainer) => {
- let attachingMessage;
- const filesCount = files.filter(function(file) {
- return file.status === 'uploading' ||
- file.status === 'queued';
- }).length;
-
- // Dinamycally change uploading files text depending on files number in
- // dropzone files queue.
- if (filesCount > 1) {
- attachingMessage = 'Attaching ' + filesCount + ' files -';
- } else {
- attachingMessage = 'Attaching a file -';
+ $attachButton.addClass('hide');
+ $cancelButton.addClass('hide');
+ },
+ totaluploadprogress(totalUploadProgress) {
+ updateAttachingMessage(this.files, $attachingFileMessage);
+ $uploadProgress.text(`${Math.round(totalUploadProgress)}%`);
+ },
+ sending: () => {
+ // DOM elements already exist.
+ // Instead of dynamically generating them,
+ // we just either hide or show them.
+ $attachButton.addClass('hide');
+ $uploadingErrorContainer.addClass('hide');
+ $uploadingProgressContainer.removeClass('hide');
+ $cancelButton.removeClass('hide');
+ },
+ removedfile: () => {
+ $attachButton.removeClass('hide');
+ $cancelButton.addClass('hide');
+ $uploadingProgressContainer.addClass('hide');
+ $uploadingErrorContainer.addClass('hide');
+ },
+ queuecomplete: () => {
+ $('.dz-preview').remove();
+ $('.markdown-area').trigger('input');
+
+ $uploadingProgressContainer.addClass('hide');
+ $cancelButton.addClass('hide');
+ },
+ });
+
+ const child = $(dropzone[0]).children('textarea');
+
+ // removeAllFiles(true) stops uploading files (if any)
+ // and remove them from dropzone files queue.
+ $cancelButton.on('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true);
+ });
+
+ // If 'error' event is fired, we store a failed files,
+ // clear dropzone files queue, change status of failed files to undefined,
+ // 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('.js-main-target-form').querySelector('.div-dropzone'));
+ const failedFiles = dropzoneInstance.files;
+
+ e.preventDefault();
+
+ // 'true' parameter of removeAllFiles() cancels
+ // uploading of files that are being uploaded at the moment.
+ dropzoneInstance.removeAllFiles(true);
+
+ failedFiles.map((failedFile) => {
+ const file = failedFile;
+
+ if (file.status === Dropzone.ERROR) {
+ file.status = undefined;
+ file.accepted = undefined;
}
- messageContainer.text(attachingMessage);
- };
-
- form.find('.markdown-selector').click(function(e) {
- e.preventDefault();
- $(this).closest('.gfm-form').find('.div-dropzone').click();
- formTextarea.focus();
+ return dropzoneInstance.addFile(file);
});
- }
-
- return DropzoneInput;
-})();
+ });
+ // eslint-disable-next-line consistent-return
+ handlePaste = (event) => {
+ const pasteEvent = event.originalEvent;
+ if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
+ const image = isImage(pasteEvent);
+ if (image) {
+ event.preventDefault();
+ const filename = getFilename(pasteEvent) || 'image.png';
+ const text = `{{${filename}}}`;
+ pasteText(text);
+ return uploadFile(image.getAsFile(), filename);
+ }
+ }
+ };
+
+ isImage = (data) => {
+ let i = 0;
+ while (i < data.clipboardData.items.length) {
+ const item = data.clipboardData.items[i];
+ if (item.type.indexOf('image') !== -1) {
+ return item;
+ }
+ i += 1;
+ }
+ return false;
+ };
+
+ pasteText = (text, shouldPad) => {
+ let formattedText = text;
+ if (shouldPad) {
+ formattedText += '\n\n';
+ }
+ const textarea = child.get(0);
+ const caretStart = textarea.selectionStart;
+ const caretEnd = textarea.selectionEnd;
+ const textEnd = $(child).val().length;
+ const beforeSelection = $(child).val().substring(0, caretStart);
+ const afterSelection = $(child).val().substring(caretEnd, textEnd);
+ $(child).val(beforeSelection + formattedText + afterSelection);
+ textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
+ textarea.style.height = `${textarea.scrollHeight}px`;
+ formTextarea.get(0).dispatchEvent(new Event('input'));
+ return formTextarea.trigger('input');
+ };
+
+ addFileToForm = (path) => {
+ $(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`);
+ };
+
+ getFilename = (e) => {
+ let value;
+ if (window.clipboardData && window.clipboardData.getData) {
+ value = window.clipboardData.getData('Text');
+ } else if (e.clipboardData && e.clipboardData.getData) {
+ value = e.clipboardData.getData('text/plain');
+ }
+ value = value.split('\r');
+ return value[0];
+ };
+
+ const showSpinner = () => $uploadingProgressContainer.removeClass('hide');
+
+ const closeSpinner = () => $uploadingProgressContainer.addClass('hide');
+
+ const showError = (message) => {
+ $uploadingErrorContainer.removeClass('hide');
+ $uploadingErrorMessage.html(message);
+ };
+
+ const closeAlertMessage = () => form.find('.div-dropzone-alert').alert('close');
+
+ const insertToTextArea = (filename, url) => {
+ const $child = $(child);
+ $child.val((index, val) => val.replace(`{{${filename}}}`, url));
+
+ $child.trigger('change');
+ };
+
+ uploadFile = (item, filename) => {
+ const formData = new FormData();
+ formData.append('file', item, filename);
+ return $.ajax({
+ url: uploadsPath,
+ type: 'POST',
+ data: formData,
+ dataType: 'json',
+ processData: false,
+ contentType: false,
+ headers: csrf.headers,
+ beforeSend: () => {
+ showSpinner();
+ return closeAlertMessage();
+ },
+ success: (e, text, response) => {
+ const md = response.responseJSON.link.markdown;
+ insertToTextArea(filename, md);
+ },
+ error: response => showError(response.responseJSON.message),
+ complete: () => closeSpinner(),
+ });
+ };
+
+ updateAttachingMessage = (files, messageContainer) => {
+ let attachingMessage;
+ const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued').length;
+
+ // Dinamycally change uploading files text depending on files number in
+ // dropzone files queue.
+ if (filesCount > 1) {
+ attachingMessage = `Attaching ${filesCount} files -`;
+ } else {
+ attachingMessage = 'Attaching a file -';
+ }
+
+ messageContainer.text(attachingMessage);
+ };
+
+ form.find('.markdown-selector').click(function onMarkdownClick(e) {
+ e.preventDefault();
+ $(this).closest('.gfm-form').find('.div-dropzone').click();
+ formTextarea.focus();
+ });
+}
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index ee71728184f..ada985913bb 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -1,8 +1,7 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
/* global dateFormat */
import Pikaday from 'pikaday';
-import DateFix from './lib/utils/datefix';
+import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect {
constructor({ $dropdown, $loading } = {}) {
@@ -17,8 +16,8 @@ class DueDateSelect {
this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content');
this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
- this.fieldName = $dropdown.data('field-name'),
- this.abilityName = $dropdown.data('ability-name'),
+ this.fieldName = $dropdown.data('field-name');
+ this.abilityName = $dropdown.data('ability-name');
this.issueUpdateURL = $dropdown.data('issue-update');
this.rawSelectedDate = null;
@@ -39,20 +38,20 @@ class DueDateSelect {
hidden: () => {
this.$selectbox.hide();
this.$value.css('display', '');
- }
+ },
});
}
initDatePicker() {
const $dueDateInput = $(`input[name='${this.fieldName}']`);
- const dateFix = DateFix.dashedFix($dueDateInput.val());
const calendar = new Pikaday({
field: $dueDateInput.get(0),
theme: 'gitlab-theme',
format: 'yyyy-mm-dd',
+ parse: dateString => parsePikadayDate(dateString),
+ toString: date => pikadayToString(date),
onSelect: (dateText) => {
- const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
- $dueDateInput.val(formattedDate);
+ $dueDateInput.val(calendar.toString(dateText));
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
@@ -60,10 +59,10 @@ class DueDateSelect {
} else {
this.saveDueDate(true);
}
- }
+ },
});
- calendar.setDate(dateFix);
+ calendar.setDate(parsePikadayDate($dueDateInput.val()));
this.$datePicker.append(calendar.el);
this.$datePicker.data('pikaday', calendar);
}
@@ -79,8 +78,8 @@ class DueDateSelect {
gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
this.updateIssueBoardIssue();
} else {
- $("input[name='" + this.fieldName + "']").val('');
- return this.saveDueDate(false);
+ $(`input[name='${this.fieldName}']`).val('');
+ this.saveDueDate(false);
}
});
}
@@ -111,7 +110,7 @@ class DueDateSelect {
this.datePayload = datePayload;
}
- updateIssueBoardIssue () {
+ updateIssueBoardIssue() {
this.$loading.fadeIn();
this.$dropdown.trigger('loading.gl.dropdown');
this.$selectbox.hide();
@@ -149,8 +148,8 @@ class DueDateSelect {
return selectedDateValue.length ?
$('.js-remove-due-date-holder').removeClass('hidden') :
$('.js-remove-due-date-holder').addClass('hidden');
- }
- }).done((data) => {
+ },
+ }).done(() => {
if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle');
@@ -160,27 +159,28 @@ class DueDateSelect {
}
}
-class DueDateSelectors {
+export default class DueDateSelectors {
constructor() {
this.initMilestoneDatePicker();
this.initIssuableSelect();
}
-
+ // eslint-disable-next-line class-methods-use-this
initMilestoneDatePicker() {
- $('.datepicker').each(function() {
+ $('.datepicker').each(function initPikadayMilestone() {
const $datePicker = $(this);
- const dateFix = DateFix.dashedFix($datePicker.val());
const calendar = new Pikaday({
field: $datePicker.get(0),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: $datePicker.parent().get(0),
+ parse: dateString => parsePikadayDate(dateString),
+ toString: date => pikadayToString(date),
onSelect(dateText) {
- $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
- }
+ $datePicker.val(calendar.toString(dateText));
+ },
});
- calendar.setDate(dateFix);
+ calendar.setDate(parsePikadayDate($datePicker.val()));
$datePicker.data('pikaday', calendar);
});
@@ -191,19 +191,17 @@ class DueDateSelectors {
calendar.setDate(null);
});
}
-
+ // eslint-disable-next-line class-methods-use-this
initIssuableSelect() {
const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
$('.js-due-date-select').each((i, dropdown) => {
const $dropdown = $(dropdown);
+ // eslint-disable-next-line no-new
new DueDateSelect({
$dropdown,
- $loading
+ $loading,
});
});
}
}
-
-window.gl = window.gl || {};
-window.gl.DueDateSelectors = DueDateSelectors;
diff --git a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
index 3fd23efa9f8..e9defb62cf8 100644
--- a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
+++ b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
@@ -7,6 +7,17 @@ function isFlagEmoji(emojiUnicode) {
return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
}
+// Tested on mac OS 10.12.6 and Windows 10 FCU, it renders as two separate characters
+const baseFlagCodePoint = 127987; // parseInt('1F3F3', 16)
+const rainbowCodePoint = 127752; // parseInt('1F308', 16)
+function isRainbowFlagEmoji(emojiUnicode) {
+ const characters = Array.from(emojiUnicode);
+ // Length 4 because flags are made of 2 characters which are surrogate pairs
+ return emojiUnicode.length === 4 &&
+ characters[0].codePointAt(0) === baseFlagCodePoint &&
+ characters[1].codePointAt(0) === rainbowCodePoint;
+}
+
// Chrome <57 renders keycaps oddly
// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
@@ -57,9 +68,11 @@ function isPersonZwjEmoji(emojiUnicode) {
// in `isEmojiUnicodeSupported` logic
function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isFlagResult = isFlagEmoji(emojiUnicode);
+ const isRainbowFlagResult = isRainbowFlagEmoji(emojiUnicode);
return (
(unicodeSupportMap.flag && isFlagResult) ||
- !isFlagResult
+ (unicodeSupportMap.rainbowFlag && isRainbowFlagResult) ||
+ (!isFlagResult && !isRainbowFlagResult)
);
}
@@ -113,6 +126,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe
export {
isEmojiUnicodeSupported as default,
isFlagEmoji,
+ isRainbowFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji,
diff --git a/app/assets/javascripts/emoji/support/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js
index 755381c2f95..c18d07dad43 100644
--- a/app/assets/javascripts/emoji/support/unicode_support_map.js
+++ b/app/assets/javascripts/emoji/support/unicode_support_map.js
@@ -1,5 +1,7 @@
import AccessorUtilities from '../../lib/utils/accessor';
+const GL_EMOJI_VERSION = '0.2.0';
+
const unicodeSupportTestMap = {
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
@@ -13,6 +15,7 @@ const unicodeSupportTestMap = {
horseRacing: '\u{1F3C7}\u{1F3FF}',
// US flag, http://emojipedia.org/flags/
flag: '\u{1F1FA}\u{1F1F8}',
+ rainbowFlag: '\u{1F3F3}\u{1F308}',
// http://emojipedia.org/modifiers/
skinToneModifier: [
// spy_tone5
@@ -141,23 +144,31 @@ function generateUnicodeSupportMap(testMap) {
}
export default function getUnicodeSupportMap() {
- let unicodeSupportMap;
- let userAgentFromCache;
-
const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
- if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
+ let glEmojiVersionFromCache;
+ let userAgentFromCache;
+ if (isLocalStorageAvailable) {
+ glEmojiVersionFromCache = window.localStorage.getItem('gl-emoji-version');
+ userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
+ }
+ let unicodeSupportMap;
try {
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
} catch (err) {
// swallow
}
- if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
+ if (
+ !unicodeSupportMap ||
+ glEmojiVersionFromCache !== GL_EMOJI_VERSION ||
+ userAgentFromCache !== navigator.userAgent
+ ) {
unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
if (isLocalStorageAvailable) {
+ window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
}
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
new file mode 100644
index 00000000000..3236077c3cf
--- /dev/null
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -0,0 +1,71 @@
+<script>
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import tablePagination from '../../vue_shared/components/table_pagination.vue';
+ import environmentTable from '../components/environments_table.vue';
+
+ export default {
+ props: {
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ environments: {
+ type: Array,
+ required: true,
+ },
+ pagination: {
+ type: Object,
+ required: true,
+ },
+ canCreateDeployment: {
+ type: Boolean,
+ required: true,
+ },
+ canReadEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ components: {
+ environmentTable,
+ loadingIcon,
+ tablePagination,
+ },
+
+ methods: {
+ onChangePage(page) {
+ this.$emit('onChangePage', page);
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="environments-container">
+
+ <loading-icon
+ label="Loading environments"
+ v-if="isLoading"
+ size="3"
+ />
+
+ <slot name="emptyState"></slot>
+
+ <div
+ class="table-holder"
+ v-if="!isLoading && environments.length > 0">
+
+ <environment-table
+ :environments="environments"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ />
+
+ <table-pagination
+ v-if="pagination && pagination.totalPages > 1"
+ :change="onChangePage"
+ :pageInfo="pagination"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/empty_state.vue b/app/assets/javascripts/environments/components/empty_state.vue
new file mode 100644
index 00000000000..2646f08c8e6
--- /dev/null
+++ b/app/assets/javascripts/environments/components/empty_state.vue
@@ -0,0 +1,42 @@
+<script>
+ export default {
+ name: 'environmentsEmptyState',
+ props: {
+ newPath: {
+ type: String,
+ required: true,
+ },
+ canCreateEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ helpPath: {
+ type: String,
+ required: true,
+ },
+ },
+ };
+</script>
+<template>
+ <div class="blank-state-row">
+ <div class="blank-state-center">
+ <h2 class="blank-state-title js-blank-state-title">
+ {{s__("Environments|You don't have any environments right now.")}}
+ </h2>
+ <p class="blank-state-text">
+ {{s__("Environments|Environments are places where code gets deployed, such as staging or production.")}}
+ <br />
+ <a :href="helpPath">
+ {{s__("Environments|Read more about environments")}}
+ </a>
+ </p>
+
+ <a
+ v-if="canCreateEnvironment"
+ :href="newPath"
+ class="btn btn-create js-new-environment-button">
+ {{s__("Environments|New environment")}}
+ </a>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue
deleted file mode 100644
index ce5f6219a3e..00000000000
--- a/app/assets/javascripts/environments/components/environment.vue
+++ /dev/null
@@ -1,268 +0,0 @@
-<script>
-/* global Flash */
-import Visibility from 'visibilityjs';
-import EnvironmentsService from '../services/environments_service';
-import environmentTable from './environments_table.vue';
-import EnvironmentsStore from '../stores/environments_store';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-import tablePagination from '../../vue_shared/components/table_pagination.vue';
-import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
-import eventHub from '../event_hub';
-import Poll from '../../lib/utils/poll';
-import environmentsMixin from '../mixins/environments_mixin';
-
-export default {
-
- components: {
- environmentTable,
- tablePagination,
- loadingIcon,
- },
-
- mixins: [
- environmentsMixin,
- ],
-
- data() {
- const environmentsData = document.querySelector('#environments-list-view').dataset;
- const store = new EnvironmentsStore();
-
- return {
- store,
- state: store.state,
- visibility: 'available',
- isLoading: false,
- cssContainerClass: environmentsData.cssClass,
- endpoint: environmentsData.environmentsDataEndpoint,
- canCreateDeployment: environmentsData.canCreateDeployment,
- canReadEnvironment: environmentsData.canReadEnvironment,
- canCreateEnvironment: environmentsData.canCreateEnvironment,
- projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
- projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
- newEnvironmentPath: environmentsData.newEnvironmentPath,
- helpPagePath: environmentsData.helpPagePath,
- isMakingRequest: false,
-
- // Pagination Properties,
- paginationInformation: {},
- pageNumber: 1,
- };
- },
-
- computed: {
- scope() {
- return getParameterByName('scope');
- },
-
- canReadEnvironmentParsed() {
- return convertPermissionToBoolean(this.canReadEnvironment);
- },
-
- canCreateDeploymentParsed() {
- return convertPermissionToBoolean(this.canCreateDeployment);
- },
-
- canCreateEnvironmentParsed() {
- return convertPermissionToBoolean(this.canCreateEnvironment);
- },
- },
-
- /**
- * Fetches all the environments and stores them.
- * Toggles loading property.
- */
- created() {
- const scope = getParameterByName('scope') || this.visibility;
- const page = getParameterByName('page') || this.pageNumber;
-
- this.service = new EnvironmentsService(this.endpoint);
-
- const poll = new Poll({
- resource: this.service,
- method: 'get',
- data: { scope, page },
- successCallback: this.successCallback,
- errorCallback: this.errorCallback,
- notificationCallback: (isMakingRequest) => {
- this.isMakingRequest = isMakingRequest;
- },
- });
-
- if (!Visibility.hidden()) {
- this.isLoading = true;
- poll.makeRequest();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- poll.restart();
- } else {
- poll.stop();
- }
- });
-
- eventHub.$on('toggleFolder', this.toggleFolder);
- eventHub.$on('postAction', this.postAction);
- },
-
- beforeDestroy() {
- eventHub.$off('toggleFolder');
- eventHub.$off('postAction');
- },
-
- methods: {
- toggleFolder(folder) {
- this.store.toggleFolder(folder);
-
- if (!folder.isOpen) {
- this.fetchChildEnvironments(folder, true);
- }
- },
-
- /**
- * Will change the page number and update the URL.
- *
- * @param {Number} pageNumber desired page to go to.
- * @return {String}
- */
- changePage(pageNumber) {
- const param = setParamInURL('page', pageNumber);
-
- gl.utils.visitUrl(param);
- return param;
- },
-
- fetchEnvironments() {
- const scope = getParameterByName('scope') || this.visibility;
- const page = getParameterByName('page') || this.pageNumber;
-
- this.isLoading = true;
-
- return this.service.get({ scope, page })
- .then(this.successCallback)
- .catch(this.errorCallback);
- },
-
- fetchChildEnvironments(folder, showLoader = false) {
- this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
-
- this.service.getFolderContent(folder.folder_path)
- .then(resp => resp.json())
- .then(response => this.store.setfolderContent(folder, response.environments))
- .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
- .catch(() => {
- // eslint-disable-next-line no-new
- new Flash('An error occurred while fetching the environments.');
- this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
- });
- },
-
- postAction(endpoint) {
- if (!this.isMakingRequest) {
- this.isLoading = true;
-
- this.service.postAction(endpoint)
- .then(() => this.fetchEnvironments())
- .catch(() => new Flash('An error occurred while making the request.'));
- }
- },
-
- successCallback(resp) {
- this.saveData(resp);
-
- // We need to verify if any folder is open to also update it
- const openFolders = this.store.getOpenFolders();
- if (openFolders.length) {
- openFolders.forEach(folder => this.fetchChildEnvironments(folder));
- }
- },
-
- errorCallback() {
- this.isLoading = false;
- // eslint-disable-next-line no-new
- new Flash('An error occurred while fetching the environments.');
- },
- },
-};
-</script>
-<template>
- <div :class="cssContainerClass">
- <div class="top-area">
- <ul
- v-if="!isLoading"
- class="nav-links">
- <li :class="{ active: scope === null || scope === 'available' }">
- <a :href="projectEnvironmentsPath">
- Available
- <span class="badge js-available-environments-count">
- {{state.availableCounter}}
- </span>
- </a>
- </li>
- <li :class="{ active : scope === 'stopped' }">
- <a :href="projectStoppedEnvironmentsPath">
- Stopped
- <span class="badge js-stopped-environments-count">
- {{state.stoppedCounter}}
- </span>
- </a>
- </li>
- </ul>
- <div
- v-if="canCreateEnvironmentParsed && !isLoading"
- class="nav-controls">
- <a
- :href="newEnvironmentPath"
- class="btn btn-create">
- New environment
- </a>
- </div>
- </div>
-
- <div class="environments-container">
- <loading-icon
- label="Loading environments"
- size="3"
- v-if="isLoading"
- />
-
- <div
- class="blank-state blank-state-no-icon"
- v-if="!isLoading && state.environments.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- You don't have any environments right now.
- </h2>
- <p class="blank-state-text">
- Environments are places where code gets deployed, such as staging or production.
- <br />
- <a :href="helpPagePath">
- Read more about environments
- </a>
- </p>
-
- <a
- v-if="canCreateEnvironmentParsed"
- :href="newEnvironmentPath"
- class="btn btn-create js-new-environment-button">
- New environment
- </a>
- </div>
-
- <div
- class="table-holder"
- v-if="!isLoading && state.environments.length > 0">
-
- <environment-table
- :environments="state.environments"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- />
- </div>
-
- <table-pagination
- v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
- :change="changePage"
- :pageInfo="state.paginationInformation" />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index 6b749814ea4..520c3ac8ace 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,5 +1,6 @@
<script>
import tooltip from '../../vue_shared/directives/tooltip';
+import { s__ } from '../../locale';
/**
* Renders the external url link in environments table.
@@ -18,7 +19,7 @@ export default {
computed: {
title() {
- return 'Open';
+ return s__('Environments|Open');
},
},
};
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 6de01fa53d0..2f0e397aa45 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -2,7 +2,7 @@
import Timeago from 'timeago.js';
import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import '../../lib/utils/text_utility';
+import { humanize } from '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
@@ -139,7 +139,7 @@ export default {
if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map((action) => {
const parsedAction = {
- name: gl.text.humanize(action.name),
+ name: humanize(action.name),
play_path: action.play_path,
playable: action.playable,
};
@@ -421,14 +421,18 @@ export default {
</script>
<template>
<div
- :class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }"
+ class="gl-responsive-table-row"
+ :class="{
+ 'js-child-row environment-child-row': model.isChildren,
+ 'folder-row': model.isFolder,
+ }"
role="row">
<div class="table-section section-10" role="gridcell">
<div
v-if="!model.isFolder"
class="table-mobile-header"
role="rowheader">
- Environment
+ {{s__("Environments|Environment")}}
</div>
<a
v-if="!model.isFolder"
@@ -495,15 +499,16 @@ export default {
</a>
</div>
- <div class="table-section section-25" role="gridcell">
+ <div
+ v-if="!model.isFolder"
+ class="table-section section-25" role="gridcell">
<div
- v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header">
- Commit
+ {{s__("Environments|Commit")}}
</div>
<div
- v-if="!model.isFolder && hasLastDeploymentKey"
+ v-if="hasLastDeploymentKey"
class="js-commit-component table-mobile-content">
<commit-component
:tag="commitTag"
@@ -514,21 +519,22 @@ export default {
:author="commitAuthor"/>
</div>
<div
- v-if="!model.isFolder && !hasLastDeploymentKey"
+ v-if="!hasLastDeploymentKey"
class="commit-title table-mobile-content">
- No deployments yet
+ {{s__("Environments|No deployments yet")}}
</div>
</div>
- <div class="table-section section-10" role="gridcell">
+ <div
+ v-if="!model.isFolder"
+ class="table-section section-10" role="gridcell">
<div
- v-if="!model.isFolder"
role="rowheader"
class="table-mobile-header">
- Updated
+ {{s__("Environments|Updated")}}
</div>
<span
- v-if="!model.isFolder && canShowDate"
+ v-if="canShowDate"
class="environment-created-date-timeago table-mobile-content">
{{createdDate}}
</span>
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 1655561cdd3..b45af1a5ebc 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -34,6 +34,7 @@ export default {
:aria-label="title">
<i
class="fa fa-area-chart"
- aria-hidden="true" />
+ aria-hidden="true"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 49dba38edfb..92a596bfd33 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -48,10 +48,10 @@ export default {
:disabled="isLoading">
<span v-if="isLastDeployment">
- Re-deploy
+ {{s__("Environments|Re-deploy")}}
</span>
<span v-else>
- Rollback
+ {{s__("Environments|Rollback")}}
</span>
<loading-icon v-if="isLoading" />
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
new file mode 100644
index 00000000000..2592909734f
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -0,0 +1,128 @@
+<script>
+ import Flash from '../../flash';
+ import { s__ } from '../../locale';
+ import emptyState from './empty_state.vue';
+ import eventHub from '../event_hub';
+ import environmentsMixin from '../mixins/environments_mixin';
+ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+
+ export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ canCreateEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ canCreateDeployment: {
+ type: Boolean,
+ required: true,
+ },
+ canReadEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ cssContainerClass: {
+ type: String,
+ required: true,
+ },
+ newEnvironmentPath: {
+ type: String,
+ required: true,
+ },
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+ components: {
+ emptyState,
+ },
+
+ mixins: [
+ CIPaginationMixin,
+ environmentsMixin,
+ ],
+
+ created() {
+ eventHub.$on('toggleFolder', this.toggleFolder);
+ },
+
+ beforeDestroy() {
+ eventHub.$off('toggleFolder');
+ },
+
+ methods: {
+ toggleFolder(folder) {
+ this.store.toggleFolder(folder);
+
+ if (!folder.isOpen) {
+ this.fetchChildEnvironments(folder, true);
+ }
+ },
+
+ fetchChildEnvironments(folder, showLoader = false) {
+ this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
+
+ this.service.getFolderContent(folder.folder_path)
+ .then(resp => resp.json())
+ .then(response => this.store.setfolderContent(folder, response.environments))
+ .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
+ .catch(() => {
+ Flash(s__('Environments|An error occurred while fetching the environments.'));
+ this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
+ });
+ },
+
+ successCallback(resp) {
+ this.saveData(resp);
+
+ // We need to verify if any folder is open to also update it
+ const openFolders = this.store.getOpenFolders();
+ if (openFolders.length) {
+ openFolders.forEach(folder => this.fetchChildEnvironments(folder));
+ }
+ },
+ },
+ };
+</script>
+<template>
+ <div :class="cssContainerClass">
+ <div class="top-area">
+ <tabs
+ :tabs="tabs"
+ @onChangeTab="onChangeTab"
+ scope="environments"
+ />
+
+ <div
+ v-if="canCreateEnvironment && !isLoading"
+ class="nav-controls">
+ <a
+ :href="newEnvironmentPath"
+ class="btn btn-create">
+ {{s__("Environments|New environment")}}
+ </a>
+ </div>
+ </div>
+
+ <container
+ :is-loading="isLoading"
+ :environments="state.environments"
+ :pagination="state.paginationInformation"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ @onChangePage="onChangePage"
+ >
+ <empty-state
+ slot="emptyState"
+ v-if="!isLoading && state.environments.length === 0"
+ :new-path="newEnvironmentPath"
+ :help-path="helpPagePath"
+ :can-create-environment="canCreateEnvironment"
+ />
+ </container>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 175cc8f1f72..c04da4b81b7 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -2,12 +2,12 @@
/**
* Render environments table.
*/
-import EnvironmentTableRowComponent from './environment_item.vue';
+import environmentItem from './environment_item.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
- 'environment-item': EnvironmentTableRowComponent,
+ environmentItem,
loadingIcon,
},
@@ -42,19 +42,19 @@ export default {
<div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-10 environments-name" role="columnheader">
- Environment
+ {{s__("Environments|Environment")}}
</div>
<div class="table-section section-10 environments-deploy" role="columnheader">
- Deployment
+ {{s__("Environments|Deployment")}}
</div>
<div class="table-section section-15 environments-build" role="columnheader">
- Job
+ {{s__("Environments|Job")}}
</div>
<div class="table-section section-25 environments-commit" role="columnheader">
- Commit
+ {{s__("Environments|Commit")}}
</div>
<div class="table-section section-10 environments-date" role="columnheader">
- Updated
+ {{s__("Environments|Updated")}}
</div>
</div>
<template
@@ -86,7 +86,7 @@ export default {
<a
:href="folderUrl(model)"
class="btn btn-default">
- Show all
+ {{s__("Environments|Show all")}}
</a>
</div>
</div>
diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/environments_bundle.js
index c0662125f28..2e0a4001b7c 100644
--- a/app/assets/javascripts/environments/environments_bundle.js
+++ b/app/assets/javascripts/environments/environments_bundle.js
@@ -1,10 +1,39 @@
import Vue from 'vue';
-import EnvironmentsComponent from './components/environment.vue';
+import environmentsComponent from './components/environments_app.vue';
+import { convertPermissionToBoolean } from '../lib/utils/common_utils';
+import Translate from '../vue_shared/translate';
+
+Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#environments-list-view',
components: {
- 'environments-table-app': EnvironmentsComponent,
+ environmentsComponent,
+ },
+ data() {
+ const environmentsData = document.querySelector(this.$options.el).dataset;
+
+ return {
+ endpoint: environmentsData.environmentsDataEndpoint,
+ newEnvironmentPath: environmentsData.newEnvironmentPath,
+ helpPagePath: environmentsData.helpPagePath,
+ cssContainerClass: environmentsData.cssClass,
+ canCreateEnvironment: convertPermissionToBoolean(environmentsData.canCreateEnvironment),
+ canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment),
+ canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment),
+ };
+ },
+ render(createElement) {
+ return createElement('environments-component', {
+ props: {
+ endpoint: this.endpoint,
+ newEnvironmentPath: this.newEnvironmentPath,
+ helpPagePath: this.helpPagePath,
+ cssContainerClass: this.cssContainerClass,
+ canCreateEnvironment: this.canCreateEnvironment,
+ canCreateDeployment: this.canCreateDeployment,
+ canReadEnvironment: this.canReadEnvironment,
+ },
+ });
},
- render: createElement => createElement('environments-table-app'),
}));
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 9add8c3d721..5d2d14c7682 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,10 +1,35 @@
import Vue from 'vue';
-import EnvironmentsFolderComponent from './environments_folder_view.vue';
+import environmentsFolderApp from './environments_folder_view.vue';
+import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
+import Translate from '../../vue_shared/translate';
+
+Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#environments-folder-list-view',
components: {
- 'environments-folder-app': EnvironmentsFolderComponent,
+ environmentsFolderApp,
+ },
+ data() {
+ const environmentsData = document.querySelector(this.$options.el).dataset;
+
+ return {
+ endpoint: environmentsData.endpoint,
+ folderName: environmentsData.folderName,
+ cssContainerClass: environmentsData.cssClass,
+ canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment),
+ canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment),
+ };
+ },
+ render(createElement) {
+ return createElement('environments-folder-app', {
+ props: {
+ endpoint: this.endpoint,
+ folderName: this.folderName,
+ cssContainerClass: this.cssContainerClass,
+ canCreateDeployment: this.canCreateDeployment,
+ canReadEnvironment: this.canReadEnvironment,
+ },
+ });
},
- render: createElement => createElement('environments-folder-app'),
}));
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index 01e70c0bbb7..27418bad01a 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -1,168 +1,40 @@
<script>
-/* global Flash */
-import Visibility from 'visibilityjs';
-import EnvironmentsService from '../services/environments_service';
-import environmentTable from '../components/environments_table.vue';
-import EnvironmentsStore from '../stores/environments_store';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-import tablePagination from '../../vue_shared/components/table_pagination.vue';
-import Poll from '../../lib/utils/poll';
-import eventHub from '../event_hub';
-import environmentsMixin from '../mixins/environments_mixin';
-import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
-
-export default {
- components: {
- environmentTable,
- tablePagination,
- loadingIcon,
- },
-
- mixins: [
- environmentsMixin,
- ],
-
- data() {
- const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
- const store = new EnvironmentsStore();
- const pathname = window.location.pathname;
- const endpoint = `${pathname}.json`;
- const folderName = pathname.substr(pathname.lastIndexOf('/') + 1);
-
- return {
- store,
- folderName,
- endpoint,
- state: store.state,
- visibility: 'available',
- isLoading: false,
- cssContainerClass: environmentsData.cssClass,
- canCreateDeployment: environmentsData.canCreateDeployment,
- canReadEnvironment: environmentsData.canReadEnvironment,
- // Pagination Properties,
- paginationInformation: {},
- pageNumber: 1,
- };
- },
-
- computed: {
- scope() {
- return getParameterByName('scope');
- },
-
- canReadEnvironmentParsed() {
- return convertPermissionToBoolean(this.canReadEnvironment);
- },
-
- canCreateDeploymentParsed() {
- return convertPermissionToBoolean(this.canCreateDeployment);
- },
-
- /**
- * URL to link in the stopped tab.
- *
- * @return {String}
- */
- stoppedPath() {
- return `${window.location.pathname}?scope=stopped`;
- },
-
- /**
- * URL to link in the available tab.
- *
- * @return {String}
- */
- availablePath() {
- return window.location.pathname;
- },
- },
-
- /**
- * Fetches all the environments and stores them.
- * Toggles loading property.
- */
- created() {
- const scope = getParameterByName('scope') || this.visibility;
- const page = getParameterByName('page') || this.pageNumber;
-
- this.service = new EnvironmentsService(this.endpoint);
-
- const poll = new Poll({
- resource: this.service,
- method: 'get',
- data: { scope, page },
- successCallback: this.successCallback,
- errorCallback: this.errorCallback,
- notificationCallback: (isMakingRequest) => {
- this.isMakingRequest = isMakingRequest;
+ import environmentsMixin from '../mixins/environments_mixin';
+ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+
+ export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ folderName: {
+ type: String,
+ required: true,
+ },
+ cssContainerClass: {
+ type: String,
+ required: true,
+ },
+ canCreateDeployment: {
+ type: Boolean,
+ required: true,
+ },
+ canReadEnvironment: {
+ type: Boolean,
+ required: true,
},
- });
-
- if (!Visibility.hidden()) {
- this.isLoading = true;
- poll.makeRequest();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- poll.restart();
- } else {
- poll.stop();
- }
- });
-
- eventHub.$on('postAction', this.postAction);
- },
-
- beforeDestroyed() {
- eventHub.$off('postAction');
- },
-
- methods: {
- /**
- * Will change the page number and update the URL.
- *
- * @param {Number} pageNumber desired page to go to.
- */
- changePage(pageNumber) {
- const param = setParamInURL('page', pageNumber);
-
- gl.utils.visitUrl(param);
- return param;
- },
-
- fetchEnvironments() {
- const scope = getParameterByName('scope') || this.visibility;
- const page = getParameterByName('page') || this.pageNumber;
-
- this.isLoading = true;
-
- return this.service.get({ scope, page })
- .then(this.successCallback)
- .catch(this.errorCallback);
- },
-
- successCallback(resp) {
- this.saveData(resp);
- },
-
- errorCallback() {
- this.isLoading = false;
- // eslint-disable-next-line no-new
- new Flash('An error occurred while fetching the environments.');
},
-
- postAction(endpoint) {
- if (!this.isMakingRequest) {
- this.isLoading = true;
-
- this.service.postAction(endpoint)
- .then(() => this.fetchEnvironments())
- .catch(() => new Flash('An error occurred while making the request.'));
- }
+ mixins: [
+ environmentsMixin,
+ CIPaginationMixin,
+ ],
+ methods: {
+ successCallback(resp) {
+ this.saveData(resp);
+ },
},
- },
-};
+ };
</script>
<template>
<div :class="cssContainerClass">
@@ -171,56 +43,23 @@ export default {
v-if="!isLoading">
<h4 class="js-folder-name environments-folder-name">
- Environments / <b>{{folderName}}</b>
+ {{s__("Environments|Environments")}} / <b>{{folderName}}</b>
</h4>
- <ul class="nav-links">
- <li :class="{ active: scope === null || scope === 'available' }">
- <a
- :href="availablePath"
- class="js-available-environments-folder-tab">
- Available
- <span class="badge js-available-environments-count">
- {{state.availableCounter}}
- </span>
- </a>
- </li>
- <li :class="{ active : scope === 'stopped' }">
- <a
- :href="stoppedPath"
- class="js-stopped-environments-folder-tab">
- Stopped
- <span class="badge js-stopped-environments-count">
- {{state.stoppedCounter}}
- </span>
- </a>
- </li>
- </ul>
- </div>
-
- <div class="environments-container">
-
- <loading-icon
- label="Loading environments"
- v-if="isLoading"
- size="3"
+ <tabs
+ :tabs="tabs"
+ @onChangeTab="onChangeTab"
+ scope="environments"
/>
-
- <div
- class="table-holder"
- v-if="!isLoading && state.environments.length > 0">
-
- <environment-table
- :environments="state.environments"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- />
-
- <table-pagination
- v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
- :change="changePage"
- :pageInfo="state.paginationInformation"/>
- </div>
</div>
+
+ <container
+ :is-loading="isLoading"
+ :environments="state.environments"
+ :pagination="state.paginationInformation"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ @onChangePage="onChangePage"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 8f4066e3a6e..7219b076721 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -1,15 +1,174 @@
+/**
+ * Common code between environmets app and folder view
+ */
+
+import Visibility from 'visibilityjs';
+import Poll from '../../lib/utils/poll';
+import {
+ getParameterByName,
+ parseQueryStringIntoObject,
+} from '../../lib/utils/common_utils';
+import { s__ } from '../../locale';
+import Flash from '../../flash';
+import eventHub from '../event_hub';
+
+import EnvironmentsStore from '../stores/environments_store';
+import EnvironmentsService from '../services/environments_service';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import tablePagination from '../../vue_shared/components/table_pagination.vue';
+import environmentTable from '../components/environments_table.vue';
+import tabs from '../../vue_shared/components/navigation_tabs.vue';
+import container from '../components/container.vue';
+
export default {
+
+ components: {
+ environmentTable,
+ container,
+ loadingIcon,
+ tabs,
+ tablePagination,
+ },
+
+ data() {
+ const store = new EnvironmentsStore();
+
+ return {
+ store,
+ state: store.state,
+ isLoading: false,
+ isMakingRequest: false,
+ scope: getParameterByName('scope') || 'available',
+ page: getParameterByName('page') || '1',
+ requestData: {},
+ };
+ },
+
methods: {
saveData(resp) {
const headers = resp.headers;
return resp.json().then((response) => {
this.isLoading = false;
- this.store.storeAvailableCount(response.available_count);
- this.store.storeStoppedCount(response.stopped_count);
- this.store.storeEnvironments(response.environments);
- this.store.setPagination(headers);
+ if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
+ this.store.storeAvailableCount(response.available_count);
+ this.store.storeStoppedCount(response.stopped_count);
+ this.store.storeEnvironments(response.environments);
+ this.store.setPagination(headers);
+ }
});
},
+
+ /**
+ * Handles URL and query parameter changes.
+ * When the user uses the pagination or the tabs,
+ * - update URL
+ * - Make API request to the server with new parameters
+ * - Update the polling function
+ * - Update the internal state
+ */
+ updateContent(parameters) {
+ this.updateInternalState(parameters);
+ // fetch new data
+ return this.service.get(this.requestData)
+ .then(response => this.successCallback(response))
+ .then(() => {
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ })
+ .catch(() => {
+ this.errorCallback();
+
+ // restart polling
+ this.poll.restart();
+ });
+ },
+
+ errorCallback() {
+ this.isLoading = false;
+ Flash(s__('Environments|An error occurred while fetching the environments.'));
+ },
+
+ postAction(endpoint) {
+ if (!this.isMakingRequest) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => this.fetchEnvironments())
+ .catch(() => {
+ this.isLoading = false;
+ Flash(s__('Environments|An error occurred while making the request.'));
+ });
+ }
+ },
+
+ fetchEnvironments() {
+ this.isLoading = true;
+
+ return this.service.get(this.requestData)
+ .then(this.successCallback)
+ .catch(this.errorCallback);
+ },
+
+ },
+
+ computed: {
+ tabs() {
+ return [
+ {
+ name: s__('Available'),
+ scope: 'available',
+ count: this.state.availableCounter,
+ isActive: this.scope === 'available',
+ },
+ {
+ name: s__('Stopped'),
+ scope: 'stopped',
+ count: this.state.stoppedCounter,
+ isActive: this.scope === 'stopped',
+ },
+ ];
+ },
+ },
+
+ /**
+ * Fetches all the environments and stores them.
+ * Toggles loading property.
+ */
+ created() {
+ this.service = new EnvironmentsService(this.endpoint);
+ this.requestData = { page: this.page, scope: this.scope };
+
+ this.poll = new Poll({
+ resource: this.service,
+ method: 'get',
+ data: this.requestData,
+ successCallback: this.successCallback,
+ errorCallback: this.errorCallback,
+ notificationCallback: (isMakingRequest) => {
+ this.isMakingRequest = isMakingRequest;
+ },
+ });
+
+ if (!Visibility.hidden()) {
+ this.isLoading = true;
+ this.poll.makeRequest();
+ } else {
+ this.fetchEnvironments();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+
+ eventHub.$on('postAction', this.postAction);
+ },
+
+ beforeDestroyed() {
+ eventHub.$off('postAction');
},
};
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index aff8227c38c..5f2989ab854 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -36,7 +36,12 @@ export default class EnvironmentsStore {
storeEnvironments(environments = []) {
const filteredEnvironments = environments.map((env) => {
const oldEnvironmentState = this.state.environments
- .find(element => element.id === env.latest.id) || {};
+ .find((element) => {
+ if (env.latest) {
+ return element.id === env.latest.id;
+ }
+ return element.id === env.id;
+ }) || {};
let filtered = {};
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index a00d29a845a..90020344748 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,6 +1,3 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
-/* global notes */
-
/* Developer beware! Do not add logic to showButton or hideButton
* that will force a reflow. Doing so will create a signficant performance
* bottleneck for pages with large diffs. For a comprehensive list of what
@@ -20,8 +17,10 @@ const DIFF_EXPANDED_CLASS = 'diff-expanded';
export default {
init($diffFile) {
- /* Caching is used only when the following members are *true*. This is because there are likely to be
- * differently configured versions of diffs in the same session. However if these values are true, they
+ /* Caching is used only when the following members are *true*.
+ * This is because there are likely to be
+ * differently configured versions of diffs in the same session.
+ * However if these values are true, they
* will be true in all cases */
if (!this.userCanCreateNote) {
diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js
index 6d516a253bb..9e91f72b2ea 100644
--- a/app/assets/javascripts/filterable_list.js
+++ b/app/assets/javascripts/filterable_list.js
@@ -6,10 +6,11 @@ import _ from 'underscore';
*/
export default class FilterableList {
- constructor(form, filter, holder) {
+ constructor(form, filter, holder, filterInputField = 'filter_groups') {
this.filterForm = form;
this.listFilterElement = filter;
this.listHolderElement = holder;
+ this.filterInputField = filterInputField;
this.isBusy = false;
}
@@ -32,10 +33,10 @@ export default class FilterableList {
onFilterInput() {
const $form = $(this.filterForm);
const queryData = {};
- const filterGroupsParam = $form.find('[name="filter_groups"]').val();
+ const filterGroupsParam = $form.find(`[name="${this.filterInputField}"]`).val();
if (filterGroupsParam) {
- queryData.filter_groups = filterGroupsParam;
+ queryData[this.filterInputField] = filterGroupsParam;
}
this.filterResults(queryData);
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index ada14d2053c..a6cc079d720 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -1,7 +1,6 @@
-/* global Flash */
-
-import Ajax from '~/droplab/plugins/ajax';
-import Filter from '~/droplab/plugins/filter';
+import Flash from '../flash';
+import Ajax from '../droplab/plugins/ajax';
+import Filter from '../droplab/plugins/filter';
import './filtered_search_dropdown';
class DropdownEmoji extends gl.FilteredSearchDropdown {
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index b32d589481d..788fb1dc614 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -1,7 +1,6 @@
-/* global Flash */
-
-import Ajax from '~/droplab/plugins/ajax';
-import Filter from '~/droplab/plugins/filter';
+import Flash from '../flash';
+import Ajax from '../droplab/plugins/ajax';
+import Filter from '../droplab/plugins/filter';
import './filtered_search_dropdown';
class DropdownNonUser extends gl.FilteredSearchDropdown {
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index ce8817b1b2e..a9e2b65def0 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -1,6 +1,5 @@
-/* global Flash */
-
-import AjaxFilter from '~/droplab/plugins/ajax_filter';
+import Flash from '../flash';
+import AjaxFilter from '../droplab/plugins/ajax_filter';
import './filtered_search_dropdown';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 8d711e3213c..cf8a9b0402b 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -147,6 +147,16 @@ class DropdownUtils {
return dataValue !== null;
}
+ static getVisualTokenValues(visualToken) {
+ const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim();
+ let tokenValue = visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim();
+ if (tokenName === 'label' && tokenValue) {
+ // remove leading symbol and wrapping quotes
+ tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, '');
+ }
+ return { tokenName, tokenValue };
+ }
+
// Determines the full search query (visual tokens + input)
static getSearchQuery(untilInput = false) {
const container = FilteredSearchContainer.container;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index a44dc279a6f..69c57f923b6 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,3 +1,4 @@
+import Flash from '../flash';
import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store';
@@ -36,7 +37,7 @@ class FilteredSearchManager {
.catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
- new window.Flash('An error occurred while parsing recent searches');
+ new Flash('An error occurred while parsing recent searches');
// Gracefully fail to empty array
return [];
})
@@ -184,8 +185,8 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim();
- const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName);
+ const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
+ const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
@@ -335,8 +336,8 @@ class FilteredSearchManager {
let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) {
- const tokenKey = t.querySelector('.name').textContent.trim();
- canClearToken = this.canEdit && this.canEdit(tokenKey);
+ const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t);
+ canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue);
}
if (canClearToken) {
@@ -468,7 +469,7 @@ class FilteredSearchManager {
}
hasFilteredSearch = true;
- const canEdit = this.canEdit && this.canEdit(sanitizedKey);
+ const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue);
gl.FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
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 28e8240169d..6139e81fe6d 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,5 +1,5 @@
import AjaxCache from '../lib/utils/ajax_cache';
-import '../flash'; /* global Flash */
+import Flash from '../flash';
import FilteredSearchContainer from './container';
import UsersCache from '../lib/utils/users_cache';
@@ -38,21 +38,14 @@ class FilteredSearchVisualTokens {
}
static createVisualTokenElementHTML(canEdit = true) {
- let removeTokenMarkup = '';
- if (canEdit) {
- removeTokenMarkup = `
- <div class="remove-token" role="button">
- <i class="fa fa-close"></i>
- </div>
- `;
- }
-
return `
- <div class="selectable" role="button">
+ <div class="${canEdit ? 'selectable' : 'hidden'}" role="button">
<div class="name"></div>
<div class="value-container">
<div class="value"></div>
- ${removeTokenMarkup}
+ <div class="remove-token" role="button">
+ <i class="fa fa-close"></i>
+ </div>
</div>
</div>
`;
@@ -123,8 +116,8 @@ class FilteredSearchVisualTokens {
/* eslint-disable no-param-reassign */
tokenValueContainer.dataset.originalValue = tokenValue;
tokenValueElement.innerHTML = `
- <img class="avatar s20" src="${user.avatar_url}" alt="${user.name}'s avatar">
- ${user.name}
+ <img class="avatar s20" src="${user.avatar_url}" alt="">
+ ${_.escape(user.name)}
`;
/* eslint-enable no-param-reassign */
})
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index ccff8f0ace7..44deab9288e 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,71 +1,99 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */
-
-window.Flash = (function() {
- var hideFlash;
-
- hideFlash = function() {
- return $(this).fadeOut();
- };
-
- /**
- * Flash banner supports different types of Flash configurations
- * along with ability to provide actionConfig which can be used to show
- * additional action or link on banner next to message
- *
- * @param {String} message Flash message
- * @param {String} type Type of Flash, it can be `notice` or `alert` (default)
- * @param {Object} parent Reference to Parent element under which Flash needs to appear
- * @param {Object} actionConfig Map of config to show action on banner
- * @param {String} href URL to which action link should point (default '#')
- * @param {String} title Title of action
- * @param {Function} clickHandler Method to call when action is clicked on
- */
- function Flash(message, type, parent, actionConfig) {
- var flash, textDiv, actionLink;
- if (type == null) {
- type = 'alert';
- }
- if (parent == null) {
- parent = null;
- }
- if (parent) {
- this.flashContainer = parent.find('.flash-container');
- } else {
- this.flashContainer = $('.flash-container-page');
- }
- this.flashContainer.html('');
- flash = $('<div/>', {
- "class": "flash-" + type
- });
- flash.on('click', hideFlash);
- textDiv = $('<div/>', {
- "class": 'flash-text',
- text: message
+import _ from 'underscore';
+
+const hideFlash = (flashEl, fadeTransition = true) => {
+ if (fadeTransition) {
+ Object.assign(flashEl.style, {
+ transition: 'opacity .3s',
+ opacity: '0',
});
- textDiv.appendTo(flash);
+ }
- if (actionConfig) {
- const actionLinkConfig = {
- class: 'flash-action',
- href: actionConfig.href || '#',
- text: actionConfig.title
- };
+ flashEl.addEventListener('transitionend', () => {
+ flashEl.remove();
+ }, {
+ once: true,
+ passive: true,
+ });
- if (!actionConfig.href) {
- actionLinkConfig.role = 'button';
- }
+ if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend'));
+};
- actionLink = $('<a/>', actionLinkConfig);
+const createAction = config => `
+ <a
+ href="${config.href || '#'}"
+ class="flash-action"
+ ${config.href ? '' : 'role="button"'}
+ >
+ ${_.escape(config.title)}
+ </a>
+`;
- actionLink.appendTo(flash);
- this.flashContainer.on('click', '.flash-action', actionConfig.clickHandler);
- }
- if (this.flashContainer.parent().hasClass('content-wrapper')) {
- textDiv.addClass('container-fluid container-limited');
+const createFlashEl = (message, type, isInContentWrapper = false) => `
+ <div
+ class="flash-${type}"
+ >
+ <div
+ class="flash-text ${isInContentWrapper ? 'container-fluid container-limited' : ''}"
+ >
+ ${_.escape(message)}
+ </div>
+ </div>
+`;
+
+const removeFlashClickListener = (flashEl, fadeTransition) => {
+ flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
+};
+
+/*
+ * Flash banner supports different types of Flash configurations
+ * along with ability to provide actionConfig which can be used to show
+ * additional action or link on banner next to message
+ *
+ * @param {String} message Flash message text
+ * @param {String} type Type of Flash, it can be `notice` or `alert` (default)
+ * @param {Object} parent Reference to parent element under which Flash needs to appear
+ * @param {Object} actonConfig Map of config to show action on banner
+ * @param {String} href URL to which action config should point to (default: '#')
+ * @param {String} title Title of action
+ * @param {Function} clickHandler Method to call when action is clicked on
+ * @param {Boolean} fadeTransition Boolean to determine whether to fade the alert out
+ */
+const createFlash = function createFlash(
+ message,
+ type = 'alert',
+ parent = document,
+ actionConfig = null,
+ fadeTransition = true,
+) {
+ const flashContainer = parent.querySelector('.flash-container');
+
+ if (!flashContainer) return null;
+
+ const isInContentWrapper = flashContainer.parentNode.classList.contains('content-wrapper');
+
+ flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper);
+
+ const flashEl = flashContainer.querySelector(`.flash-${type}`);
+ removeFlashClickListener(flashEl, fadeTransition);
+
+ if (actionConfig) {
+ flashEl.innerHTML += createAction(actionConfig);
+
+ if (actionConfig.clickHandler) {
+ flashEl.querySelector('.flash-action').addEventListener('click', e => actionConfig.clickHandler(e));
}
- flash.appendTo(this.flashContainer);
- this.flashContainer.show();
}
- return Flash;
-})();
+ flashContainer.style.display = 'block';
+
+ return flashContainer;
+};
+
+export {
+ createFlash as default,
+ createFlashEl,
+ createAction,
+ hideFlash,
+ removeFlashClickListener,
+};
+window.Flash = createFlash;
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 5c624b79d45..a642464c920 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -338,7 +338,8 @@ class GfmAutoComplete {
let resultantValue = value;
if (value && !this.setting.skipSpecialCharacterTest) {
const withoutAt = value.substring(1);
- if (withoutAt && /[^\w\d]/.test(withoutAt)) {
+ const regex = value.charAt() === '~' ? /\W|^\d+$/ : /\W/;
+ if (withoutAt && regex.test(withoutAt)) {
resultantValue = `${value.charAt()}"${withoutAt}"`;
}
}
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index e8d8fef8579..4e7a6e54f90 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
import _ from 'underscore';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isObject } from './lib/utils/type_utility';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
@@ -330,7 +331,7 @@ GitLabDropdown = (function() {
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
- return $(selector);
+ return $(selector, this.instance.dropdown);
};
})(this),
data: (function(_this) {
diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js
index 0add7075254..bd63f6f16f0 100644
--- a/app/assets/javascripts/gl_field_error.js
+++ b/app/assets/javascripts/gl_field_error.js
@@ -54,7 +54,7 @@ const inputErrorClass = 'gl-field-error-outline';
const errorAnchorSelector = '.gl-field-error-anchor';
const ignoreInputSelector = '.gl-field-error-ignore';
-class GlFieldError {
+export default class GlFieldError {
constructor({ input, formErrors }) {
this.inputElement = $(input);
this.inputDomElement = this.inputElement.get(0);
@@ -159,6 +159,3 @@ class GlFieldError {
this.fieldErrorElement.hide();
}
}
-
-window.gl = window.gl || {};
-window.gl.GlFieldError = GlFieldError;
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
index 4bef60264bb..73bcbd93565 100644
--- a/app/assets/javascripts/gl_field_errors.js
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -1,42 +1,40 @@
-/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */
-
-import './gl_field_error';
+import GlFieldError from './gl_field_error';
const customValidationFlag = 'gl-field-error-ignore';
-class GlFieldErrors {
+export default class GlFieldErrors {
constructor(form) {
this.form = $(form);
this.state = {
inputs: [],
- valid: false
+ valid: false,
};
this.initValidators();
}
- initValidators () {
+ initValidators() {
// register selectors here as needed
const validateSelectors = [':text', ':password', '[type=email]']
- .map((selector) => `input${selector}`).join(',');
+ .map(selector => `input${selector}`).join(',');
this.state.inputs = this.form.find(validateSelectors).toArray()
- .filter((input) => !input.classList.contains(customValidationFlag))
- .map((input) => new window.gl.GlFieldError({ input, formErrors: this }));
+ .filter(input => !input.classList.contains(customValidationFlag))
+ .map(input => new GlFieldError({ input, formErrors: this }));
- this.form.on('submit', this.catchInvalidFormSubmit);
+ this.form.on('submit', GlFieldErrors.catchInvalidFormSubmit);
}
/* Neccessary to prevent intercept and override invalid form submit
* because Safari & iOS quietly allow form submission when form is invalid
* and prevents disabling of invalid submit button by application.js */
- catchInvalidFormSubmit (event) {
- const $form = $(event.currentTarget);
+ static catchInvalidFormSubmit(e) {
+ const $form = $(e.currentTarget);
if (!$form.attr('novalidate')) {
- if (!event.currentTarget.checkValidity()) {
- event.preventDefault();
- event.stopPropagation();
+ if (!e.currentTarget.checkValidity()) {
+ e.preventDefault();
+ e.stopPropagation();
}
}
}
@@ -50,11 +48,9 @@ class GlFieldErrors {
});
}
- focusOnFirstInvalid () {
- const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
+ focusOnFirstInvalid() {
+ const firstInvalid = this.state.inputs
+ .filter(input => !input.inputDomElement.validity.valid)[0];
firstInvalid.inputElement.focus();
}
}
-
-window.gl = window.gl || {};
-window.gl.GlFieldErrors = GlFieldErrors;
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 4e8141b2956..d0f9e6af0f8 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,104 +1,100 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */
-/* global GitLab */
-/* global DropzoneInput */
/* global autosize */
import GfmAutoComplete from './gfm_auto_complete';
-
-window.gl = window.gl || {};
-
-function GLForm(form, enableGFM = false) {
- this.form = form;
- this.textarea = this.form.find('textarea.js-gfm-input');
- this.enableGFM = enableGFM;
- // Before we start, we should clean up any previous data for this form
- this.destroy();
- // Setup the form
- this.setupForm();
- this.form.data('gl-form', this);
-}
-
-GLForm.prototype.destroy = function() {
- // Clean form listeners
- this.clearEventListeners();
- if (this.autoComplete) {
- this.autoComplete.destroy();
+import dropzoneInput from './dropzone_input';
+import textUtils from './lib/utils/text_markdown';
+
+export default class GLForm {
+ constructor(form, enableGFM = false) {
+ this.form = form;
+ this.textarea = this.form.find('textarea.js-gfm-input');
+ this.enableGFM = enableGFM;
+ // Before we start, we should clean up any previous data for this form
+ this.destroy();
+ // Setup the form
+ this.setupForm();
+ this.form.data('gl-form', this);
}
- return this.form.data('gl-form', null);
-};
-GLForm.prototype.setupForm = function() {
- var isNewForm;
- isNewForm = this.form.is(':not(.gfm-form)');
- this.form.removeClass('js-new-note-form');
- if (isNewForm) {
- this.form.find('.div-dropzone').remove();
- this.form.addClass('gfm-form');
- // remove notify commit author checkbox for non-commit notes
- gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
- this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
- this.autoComplete.setup(this.form.find('.js-gfm-input'), {
- emojis: true,
- members: this.enableGFM,
- issues: this.enableGFM,
- milestones: this.enableGFM,
- mergeRequests: this.enableGFM,
- labels: this.enableGFM,
- });
- new DropzoneInput(this.form);
- autosize(this.textarea);
+ destroy() {
+ // Clean form listeners
+ this.clearEventListeners();
+ if (this.autoComplete) {
+ this.autoComplete.destroy();
+ }
+ this.form.data('gl-form', null);
}
- // form and textarea event listeners
- this.addEventListeners();
- gl.text.init(this.form);
- // hide discard button
- this.form.find('.js-note-discard').hide();
- this.form.show();
- if (this.isAutosizeable) this.setupAutosize();
-};
-GLForm.prototype.setupAutosize = function () {
- this.textarea.off('autosize:resized')
- .on('autosize:resized', this.setHeightData.bind(this));
+ setupForm() {
+ const isNewForm = this.form.is(':not(.gfm-form)');
+ this.form.removeClass('js-new-note-form');
+ if (isNewForm) {
+ this.form.find('.div-dropzone').remove();
+ this.form.addClass('gfm-form');
+ // remove notify commit author checkbox for non-commit notes
+ gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
+ this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
+ this.autoComplete.setup(this.form.find('.js-gfm-input'), {
+ emojis: true,
+ members: this.enableGFM,
+ issues: this.enableGFM,
+ milestones: this.enableGFM,
+ mergeRequests: this.enableGFM,
+ labels: this.enableGFM,
+ });
+ dropzoneInput(this.form);
+ autosize(this.textarea);
+ }
+ // form and textarea event listeners
+ this.addEventListeners();
+ textUtils.init(this.form);
+ // hide discard button
+ this.form.find('.js-note-discard').hide();
+ this.form.show();
+ if (this.isAutosizeable) this.setupAutosize();
+ }
- this.textarea.off('mouseup.autosize')
- .on('mouseup.autosize', this.destroyAutosize.bind(this));
+ setupAutosize() {
+ this.textarea.off('autosize:resized')
+ .on('autosize:resized', this.setHeightData.bind(this));
- setTimeout(() => {
- autosize(this.textarea);
- this.textarea.css('resize', 'vertical');
- }, 0);
-};
+ this.textarea.off('mouseup.autosize')
+ .on('mouseup.autosize', this.destroyAutosize.bind(this));
-GLForm.prototype.setHeightData = function () {
- this.textarea.data('height', this.textarea.outerHeight());
-};
+ setTimeout(() => {
+ autosize(this.textarea);
+ this.textarea.css('resize', 'vertical');
+ }, 0);
+ }
-GLForm.prototype.destroyAutosize = function () {
- const outerHeight = this.textarea.outerHeight();
+ setHeightData() {
+ this.textarea.data('height', this.textarea.outerHeight());
+ }
- if (this.textarea.data('height') === outerHeight) return;
+ destroyAutosize() {
+ const outerHeight = this.textarea.outerHeight();
- autosize.destroy(this.textarea);
+ if (this.textarea.data('height') === outerHeight) return;
- this.textarea.data('height', outerHeight);
- this.textarea.outerHeight(outerHeight);
- this.textarea.css('max-height', window.outerHeight);
-};
+ autosize.destroy(this.textarea);
-GLForm.prototype.clearEventListeners = function() {
- this.textarea.off('focus');
- this.textarea.off('blur');
- return gl.text.removeListeners(this.form);
-};
+ this.textarea.data('height', outerHeight);
+ this.textarea.outerHeight(outerHeight);
+ this.textarea.css('max-height', window.outerHeight);
+ }
-GLForm.prototype.addEventListeners = function() {
- this.textarea.on('focus', function() {
- return $(this).closest('.md-area').addClass('is-focused');
- });
- return this.textarea.on('blur', function() {
- return $(this).closest('.md-area').removeClass('is-focused');
- });
-};
+ clearEventListeners() {
+ this.textarea.off('focus');
+ this.textarea.off('blur');
+ textUtils.removeListeners(this.form);
+ }
-window.gl.GLForm = GLForm;
+ addEventListeners() {
+ this.textarea.on('focus', function focusTextArea() {
+ $(this).closest('.md-area').addClass('is-focused');
+ });
+ this.textarea.on('blur', function blurTextArea() {
+ $(this).closest('.md-area').removeClass('is-focused');
+ });
+ }
+}
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index cdc4fcf6573..e7232ca3712 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -4,6 +4,7 @@ import _ from 'underscore';
import d3 from 'd3';
import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
import ContributorsStatGraphUtil from './stat_graph_contributors_util';
+import { n__ } from '../locale';
export default (function() {
function ContributorsStatGraph() {}
@@ -44,7 +45,7 @@ export default (function() {
commits = $('<span/>', {
"class": 'graph-author-commits-count'
});
- commits.text(author.commits + " commits");
+ commits.text(n__('%d commit', '%d commits', author.commits));
return $('<span/>').append(commits);
};
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index f03b47b1c1d..2168ff3a8ba 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -1,19 +1,12 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
-
-window.GroupAvatar = (function() {
- function GroupAvatar() {
- $('.js-choose-group-avatar-button').on("click", function() {
- var form;
- form = $(this).closest("form");
- return form.find(".js-group-avatar-input").click();
- });
- $('.js-group-avatar-input').on("change", function() {
- var filename, form;
- form = $(this).closest("form");
- filename = $(this).val().replace(/^.*[\\\/]/, '');
- return form.find(".js-avatar-filename").text(filename);
- });
- }
-
- return GroupAvatar;
-})();
+export default function groupAvatar() {
+ $('.js-choose-group-avatar-button').on('click', function onClickGroupAvatar() {
+ const form = $(this).closest('form');
+ return form.find('.js-group-avatar-input').click();
+ });
+ $('.js-group-avatar-input').on('change', function onChangeAvatarInput() {
+ const form = $(this).closest('form');
+ // eslint-disable-next-line no-useless-escape
+ const filename = $(this).val().replace(/^.*[\\\/]/, '');
+ return form.find('.js-avatar-filename').text(filename);
+ });
+}
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
index 7dc9ce898e8..befaebb635e 100644
--- a/app/assets/javascripts/group_label_subscription.js
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -1,6 +1,4 @@
-/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */
-
-class GroupLabelSubscription {
+export default class GroupLabelSubscription {
constructor(container) {
const $container = $(container);
this.$dropdown = $container.find('.dropdown');
@@ -18,7 +16,7 @@ class GroupLabelSubscription {
$.ajax({
type: 'POST',
- url: url
+ url,
}).done(() => {
this.toggleSubscriptionButtons();
this.$unsubscribeButtons.removeAttr('data-url');
@@ -35,7 +33,7 @@ class GroupLabelSubscription {
$.ajax({
type: 'POST',
- url: url
+ url,
}).done(() => {
this.toggleSubscriptionButtons();
});
@@ -47,6 +45,3 @@ class GroupLabelSubscription {
this.$unsubscribeButtons.toggleClass('hidden');
}
}
-
-window.gl = window.gl || {};
-window.gl.GroupLabelSubscription = GroupLabelSubscription;
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
new file mode 100644
index 00000000000..2c0b6ab4ea8
--- /dev/null
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -0,0 +1,194 @@
+<script>
+/* global Flash */
+
+import eventHub from '../event_hub';
+import { getParameterByName } from '../../lib/utils/common_utils';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import { COMMON_STR } from '../constants';
+
+import groupsComponent from './groups.vue';
+
+export default {
+ components: {
+ loadingIcon,
+ groupsComponent,
+ },
+ props: {
+ store: {
+ type: Object,
+ required: true,
+ },
+ service: {
+ type: Object,
+ required: true,
+ },
+ hideProjects: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isLoading: true,
+ isSearchEmpty: false,
+ searchEmptyMessage: '',
+ };
+ },
+ computed: {
+ groups() {
+ return this.store.getGroups();
+ },
+ pageInfo() {
+ return this.store.getPaginationInfo();
+ },
+ },
+ methods: {
+ fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) {
+ return this.service.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
+ .then((res) => {
+ if (updatePagination) {
+ this.updatePagination(res.headers);
+ }
+
+ return res;
+ })
+ .then(res => res.json())
+ .catch(() => {
+ this.isLoading = false;
+ $.scrollTo(0);
+
+ Flash(COMMON_STR.FAILURE);
+ });
+ },
+ fetchAllGroups() {
+ const page = getParameterByName('page') || null;
+ const sortBy = getParameterByName('sort') || null;
+ const archived = getParameterByName('archived') || null;
+ const filterGroupsBy = getParameterByName('filter') || null;
+
+ this.isLoading = true;
+ // eslint-disable-next-line promise/catch-or-return
+ this.fetchGroups({
+ page,
+ filterGroupsBy,
+ sortBy,
+ archived,
+ updatePagination: true,
+ }).then((res) => {
+ this.isLoading = false;
+ this.updateGroups(res, Boolean(filterGroupsBy));
+ });
+ },
+ fetchPage(page, filterGroupsBy, sortBy, archived) {
+ this.isLoading = true;
+
+ // eslint-disable-next-line promise/catch-or-return
+ this.fetchGroups({
+ page,
+ filterGroupsBy,
+ sortBy,
+ archived,
+ updatePagination: true,
+ }).then((res) => {
+ this.isLoading = false;
+ $.scrollTo(0);
+
+ const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href);
+ window.history.replaceState({
+ page: currentPath,
+ }, document.title, currentPath);
+
+ this.updateGroups(res);
+ });
+ },
+ toggleChildren(group) {
+ const parentGroup = group;
+ if (!parentGroup.isOpen) {
+ if (parentGroup.children.length === 0) {
+ parentGroup.isChildrenLoading = true;
+ // eslint-disable-next-line promise/catch-or-return
+ this.fetchGroups({
+ parentId: parentGroup.id,
+ }).then((res) => {
+ this.store.setGroupChildren(parentGroup, res);
+ }).catch(() => {
+ parentGroup.isChildrenLoading = false;
+ });
+ } else {
+ parentGroup.isOpen = true;
+ }
+ } else {
+ parentGroup.isOpen = false;
+ }
+ },
+ leaveGroup(group, parentGroup) {
+ const targetGroup = group;
+ targetGroup.isBeingRemoved = true;
+ this.service.leaveGroup(targetGroup.leavePath)
+ .then(res => res.json())
+ .then((res) => {
+ $.scrollTo(0);
+ this.store.removeGroup(targetGroup, parentGroup);
+ Flash(res.notice, 'notice');
+ })
+ .catch((err) => {
+ let message = COMMON_STR.FAILURE;
+ if (err.status === 403) {
+ message = COMMON_STR.LEAVE_FORBIDDEN;
+ }
+ Flash(message);
+ targetGroup.isBeingRemoved = false;
+ });
+ },
+ updatePagination(headers) {
+ this.store.setPaginationInfo(headers);
+ },
+ updateGroups(groups, fromSearch) {
+ this.isSearchEmpty = groups ? groups.length === 0 : false;
+ if (fromSearch) {
+ this.store.setSearchedGroups(groups);
+ } else {
+ this.store.setGroups(groups);
+ }
+ },
+ },
+ created() {
+ this.searchEmptyMessage = this.hideProjects ?
+ COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
+
+ eventHub.$on('fetchPage', this.fetchPage);
+ eventHub.$on('toggleChildren', this.toggleChildren);
+ eventHub.$on('leaveGroup', this.leaveGroup);
+ eventHub.$on('updatePagination', this.updatePagination);
+ eventHub.$on('updateGroups', this.updateGroups);
+ },
+ mounted() {
+ this.fetchAllGroups();
+ },
+ beforeDestroy() {
+ eventHub.$off('fetchPage', this.fetchPage);
+ eventHub.$off('toggleChildren', this.toggleChildren);
+ eventHub.$off('leaveGroup', this.leaveGroup);
+ eventHub.$off('updatePagination', this.updatePagination);
+ eventHub.$off('updateGroups', this.updateGroups);
+ },
+};
+</script>
+
+<template>
+ <div>
+ <loading-icon
+ class="loading-animation prepend-top-20"
+ size="2"
+ v-if="isLoading"
+ :label="s__('GroupsTree|Loading groups')"
+ />
+ <groups-component
+ v-if="!isLoading"
+ :groups="groups"
+ :search-empty="isSearchEmpty"
+ :search-empty-message="searchEmptyMessage"
+ :page-info="pageInfo"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue
index 7cc6c4b0359..e60221fa08d 100644
--- a/app/assets/javascripts/groups/components/group_folder.vue
+++ b/app/assets/javascripts/groups/components/group_folder.vue
@@ -1,15 +1,27 @@
<script>
+import { n__ } from '../../locale';
+import { MAX_CHILDREN_COUNT } from '../constants';
+
export default {
props: {
- groups: {
- type: Object,
- required: true,
- },
- baseGroup: {
+ parentGroup: {
type: Object,
required: false,
default: () => ({}),
},
+ groups: {
+ type: Array,
+ required: false,
+ default: () => ([]),
+ },
+ },
+ computed: {
+ hasMoreChildren() {
+ return this.parentGroup.childrenCount > MAX_CHILDREN_COUNT;
+ },
+ moreChildrenStats() {
+ return n__('One more item', '%d more items', this.parentGroup.childrenCount - this.parentGroup.children.length);
+ },
},
};
</script>
@@ -20,8 +32,20 @@ export default {
v-for="(group, index) in groups"
:key="index"
:group="group"
- :base-group="baseGroup"
- :collection="groups"
+ :parent-group="parentGroup"
/>
+ <li
+ v-if="hasMoreChildren"
+ class="group-row">
+ <a
+ :href="parentGroup.relativePath"
+ class="group-row-contents has-more-items">
+ <i
+ class="fa fa-external-link"
+ aria-hidden="true"
+ />
+ {{moreChildrenStats}}
+ </a>
+ </li>
</ul>
</template>
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 2060410e991..356a95c05ca 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -2,49 +2,28 @@
import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub';
+import itemCaret from './item_caret.vue';
+import itemTypeIcon from './item_type_icon.vue';
+import itemStats from './item_stats.vue';
+import itemActions from './item_actions.vue';
+
export default {
components: {
identicon,
+ itemCaret,
+ itemTypeIcon,
+ itemStats,
+ itemActions,
},
props: {
- group: {
- type: Object,
- required: true,
- },
- baseGroup: {
+ parentGroup: {
type: Object,
required: false,
default: () => ({}),
},
- collection: {
+ group: {
type: Object,
- required: false,
- default: () => ({}),
- },
- },
- methods: {
- onClickRowGroup(e) {
- e.stopPropagation();
-
- // Skip for buttons
- if (!(e.target.tagName === 'A') && !(e.target.tagName === 'I' && e.target.parentElement.tagName === 'A')) {
- if (this.group.hasSubgroups) {
- eventHub.$emit('toggleSubGroups', this.group);
- } else {
- window.location.href = this.group.groupPath;
- }
- }
- },
- onLeaveGroup(e) {
- e.preventDefault();
-
- // eslint-disable-next-line no-alert
- if (confirm(`Are you sure you want to leave the "${this.group.fullName}" group?`)) {
- this.leaveGroup();
- }
- },
- leaveGroup() {
- eventHub.$emit('leaveGroup', this.group, this.collection);
+ required: true,
},
},
computed: {
@@ -53,51 +32,33 @@ export default {
},
rowClass() {
return {
- 'group-row': true,
'is-open': this.group.isOpen,
- 'has-subgroups': this.group.hasSubgroups,
- 'no-description': !this.group.description,
+ 'has-children': this.hasChildren,
+ 'has-description': this.group.description,
+ 'being-removed': this.group.isBeingRemoved,
};
},
- visibilityIcon() {
- return {
- fa: true,
- 'fa-globe': this.group.visibility === 'public',
- 'fa-shield': this.group.visibility === 'internal',
- 'fa-lock': this.group.visibility === 'private',
- };
+ hasChildren() {
+ return this.group.childrenCount > 0;
},
- fullPath() {
- let fullPath = '';
-
- if (this.group.isOrphan) {
- // check if current group is baseGroup
- if (Object.keys(this.baseGroup).length > 0 && this.baseGroup !== this.group) {
- // Remove baseGroup prefix from our current group.fullName. e.g:
- // baseGroup.fullName: `level1`
- // group.fullName: `level1 / level2 / level3`
- // Result: `level2 / level3`
- const gfn = this.group.fullName;
- const bfn = this.baseGroup.fullName;
- const length = bfn.length;
- const start = gfn.indexOf(bfn);
- const extraPrefixChars = 3;
-
- fullPath = gfn.substr(start + length + extraPrefixChars);
+ hasAvatar() {
+ return this.group.avatarUrl !== null;
+ },
+ isGroup() {
+ return this.group.type === 'group';
+ },
+ },
+ methods: {
+ onClickRowGroup(e) {
+ const NO_EXPAND_CLS = 'no-expand';
+ if (!(e.target.classList.contains(NO_EXPAND_CLS) ||
+ e.target.parentElement.classList.contains(NO_EXPAND_CLS))) {
+ if (this.hasChildren) {
+ eventHub.$emit('toggleChildren', this.group);
} else {
- fullPath = this.group.fullName;
+ gl.utils.visitUrl(this.group.relativePath);
}
- } else {
- fullPath = this.group.name;
}
-
- return fullPath;
- },
- hasGroups() {
- return Object.keys(this.group.subGroups).length > 0;
- },
- hasAvatar() {
- return this.group.avatarUrl && this.group.avatarUrl.indexOf('/assets/no_group_avatar') === -1;
},
},
};
@@ -108,98 +69,36 @@ export default {
@click.stop="onClickRowGroup"
:id="groupDomId"
:class="rowClass"
+ class="group-row"
>
<div
class="group-row-contents">
- <div
- class="controls">
- <a
- v-if="group.canEdit"
- class="edit-group btn"
- :href="group.editPath">
- <i
- class="fa fa-cogs"
- aria-hidden="true"
- >
- </i>
- </a>
- <a
- @click="onLeaveGroup"
- :href="group.leavePath"
- class="leave-group btn"
- title="Leave this group">
- <i
- class="fa fa-sign-out"
- aria-hidden="true"
- >
- </i>
- </a>
- </div>
- <div
- class="stats">
- <span
- class="number-projects">
- <i
- class="fa fa-bookmark"
- aria-hidden="true"
- >
- </i>
- {{group.numberProjects}}
- </span>
- <span
- class="number-users">
- <i
- class="fa fa-users"
- aria-hidden="true"
- >
- </i>
- {{group.numberUsers}}
- </span>
- <span
- class="group-visibility">
- <i
- :class="visibilityIcon"
- aria-hidden="true"
- >
- </i>
- </span>
- </div>
+ <item-actions
+ v-if="isGroup"
+ :group="group"
+ :parent-group="parentGroup"
+ />
+ <item-stats
+ :item="group"
+ />
<div
class="folder-toggle-wrap">
- <span
- class="folder-caret"
- v-if="group.hasSubgroups">
- <i
- v-if="group.isOpen"
- class="fa fa-caret-down"
- aria-hidden="true"
- >
- </i>
- <i
- v-if="!group.isOpen"
- class="fa fa-caret-right"
- aria-hidden="true"
- >
- </i>
- </span>
- <span class="folder-icon">
- <i
- v-if="group.isOpen"
- class="fa fa-folder-open"
- aria-hidden="true"
- >
- </i>
- <i
- v-if="!group.isOpen"
- class="fa fa-folder"
- aria-hidden="true">
- </i>
- </span>
+ <item-caret
+ :is-group-open="group.isOpen"
+ />
+ <item-type-icon
+ :item-type="group.type"
+ :is-group-open="group.isOpen"
+ />
</div>
<div
- class="avatar-container s40 hidden-xs">
+ class="avatar-container s40 hidden-xs"
+ :class="{ 'content-loading': group.isChildrenLoading }"
+ >
<a
- :href="group.groupPath">
+ :href="group.relativePath"
+ class="no-expand"
+ >
<img
v-if="hasAvatar"
class="avatar s40"
@@ -215,19 +114,22 @@ export default {
<div
class="title">
<a
- :href="group.groupPath">{{fullPath}}</a>
- <template v-if="group.permissions.humanGroupAccess">
- as
- <span class="access-type">{{group.permissions.humanGroupAccess}}</span>
- </template>
+ :href="group.relativePath"
+ class="no-expand">{{group.fullName}}</a>
+ <span
+ v-if="group.permission"
+ class="access-type"
+ >
+ {{s__('GroupsTreeRole|as')}} {{group.permission}}
+ </span>
</div>
<div
class="description">{{group.description}}</div>
</div>
<group-folder
- v-if="group.isOpen && hasGroups"
- :groups="group.subGroups"
- :baseGroup="group"
+ v-if="group.isOpen && hasChildren"
+ :parent-group="group"
+ :groups="group.children"
/>
</li>
</template>
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index d17a43b048a..75a2bf34887 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -4,24 +4,33 @@ import eventHub from '../event_hub';
import { getParameterByName } from '../../lib/utils/common_utils';
export default {
+ components: {
+ tablePagination,
+ },
props: {
groups: {
- type: Object,
+ type: Array,
required: true,
},
pageInfo: {
type: Object,
required: true,
},
- },
- components: {
- tablePagination,
+ searchEmpty: {
+ type: Boolean,
+ required: true,
+ },
+ searchEmptyMessage: {
+ type: String,
+ required: true,
+ },
},
methods: {
change(page) {
const filterGroupsParam = getParameterByName('filter_groups');
const sortParam = getParameterByName('sort');
- eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam);
+ const archivedParam = getParameterByName('archived');
+ eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam);
},
},
};
@@ -29,10 +38,17 @@ export default {
<template>
<div class="groups-list-tree-container">
+ <div
+ v-if="searchEmpty"
+ class="has-no-search-results">
+ {{searchEmptyMessage}}
+ </div>
<group-folder
+ v-if="!searchEmpty"
:groups="groups"
/>
<table-pagination
+ v-if="!searchEmpty"
:change="change"
:pageInfo="pageInfo"
/>
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
new file mode 100644
index 00000000000..09cb79c1afd
--- /dev/null
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -0,0 +1,95 @@
+<script>
+import { s__ } from '../../locale';
+import tooltip from '../../vue_shared/directives/tooltip';
+import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
+import eventHub from '../event_hub';
+import { COMMON_STR } from '../constants';
+import Icon from '../../vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ PopupDialog,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ parentGroup: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ group: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ dialogStatus: false,
+ };
+ },
+ computed: {
+ leaveBtnTitle() {
+ return COMMON_STR.LEAVE_BTN_TITLE;
+ },
+ editBtnTitle() {
+ return COMMON_STR.EDIT_BTN_TITLE;
+ },
+ leaveConfirmationMessage() {
+ return s__(`GroupsTree|Are you sure you want to leave the "${this.group.fullName}" group?`);
+ },
+ },
+ methods: {
+ onLeaveGroup() {
+ this.dialogStatus = true;
+ },
+ leaveGroup(leaveConfirmed) {
+ this.dialogStatus = false;
+ if (leaveConfirmed) {
+ eventHub.$emit('leaveGroup', this.group, this.parentGroup);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="controls">
+ <a
+ v-tooltip
+ v-if="group.canEdit"
+ :href="group.editPath"
+ :title="editBtnTitle"
+ :aria-label="editBtnTitle"
+ data-container="body"
+ class="edit-group btn no-expand">
+ <icon
+ name="settings">
+ </icon>
+ </a>
+ <a
+ v-tooltip
+ v-if="group.canLeave"
+ @click.prevent="onLeaveGroup"
+ :href="group.leavePath"
+ :title="leaveBtnTitle"
+ :aria-label="leaveBtnTitle"
+ data-container="body"
+ class="leave-group btn no-expand">
+ <i
+ class="fa fa-sign-out"
+ aria-hidden="true"/>
+ </a>
+ <popup-dialog
+ v-show="dialogStatus"
+ :primary-button-label="__('Leave')"
+ kind="warning"
+ :title="__('Are you sure?')"
+ :text="__('Are you sure you want to leave this group?')"
+ :body="leaveConfirmationMessage"
+ @submit="leaveGroup"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/groups/components/item_caret.vue b/app/assets/javascripts/groups/components/item_caret.vue
new file mode 100644
index 00000000000..959b984816f
--- /dev/null
+++ b/app/assets/javascripts/groups/components/item_caret.vue
@@ -0,0 +1,25 @@
+<script>
+export default {
+ props: {
+ isGroupOpen: {
+ type: Boolean,
+ required: true,
+ default: false,
+ },
+ },
+ computed: {
+ iconClass() {
+ return this.isGroupOpen ? 'fa-caret-down' : 'fa-caret-right';
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="folder-caret">
+ <i
+ :class="iconClass"
+ class="fa"
+ aria-hidden="true"/>
+ </span>
+</template>
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
new file mode 100644
index 00000000000..9f8ac138fc3
--- /dev/null
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -0,0 +1,98 @@
+<script>
+import tooltip from '../../vue_shared/directives/tooltip';
+import { ITEM_TYPE, VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE } from '../constants';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ visibilityIcon() {
+ return VISIBILITY_TYPE_ICON[this.item.visibility];
+ },
+ visibilityTooltip() {
+ if (this.item.type === ITEM_TYPE.GROUP) {
+ return GROUP_VISIBILITY_TYPE[this.item.visibility];
+ }
+ return PROJECT_VISIBILITY_TYPE[this.item.visibility];
+ },
+ isProject() {
+ return this.item.type === ITEM_TYPE.PROJECT;
+ },
+ isGroup() {
+ return this.item.type === ITEM_TYPE.GROUP;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="stats">
+ <span
+ v-tooltip
+ v-if="isGroup"
+ :title="s__('Subgroups')"
+ class="number-subgroups"
+ data-placement="top"
+ data-container="body">
+ <i
+ class="fa fa-folder"
+ aria-hidden="true"
+ />
+ {{item.subgroupCount}}
+ </span>
+ <span
+ v-tooltip
+ v-if="isGroup"
+ :title="s__('Projects')"
+ class="number-projects"
+ data-placement="top"
+ data-container="body">
+ <i
+ class="fa fa-bookmark"
+ aria-hidden="true"
+ />
+ {{item.projectCount}}
+ </span>
+ <span
+ v-tooltip
+ v-if="isGroup"
+ :title="s__('Members')"
+ class="number-users"
+ data-placement="top"
+ data-container="body">
+ <i
+ class="fa fa-users"
+ aria-hidden="true"
+ />
+ {{item.memberCount}}
+ </span>
+ <span
+ v-if="isProject"
+ class="project-stars">
+ <i
+ class="fa fa-star"
+ aria-hidden="true"
+ />
+ {{item.starCount}}
+ </span>
+ <span
+ v-tooltip
+ :title="visibilityTooltip"
+ data-placement="left"
+ data-container="body"
+ class="item-visibility">
+ <i
+ :class="visibilityIcon"
+ class="fa"
+ aria-hidden="true"
+ />
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/groups/components/item_type_icon.vue b/app/assets/javascripts/groups/components/item_type_icon.vue
new file mode 100644
index 00000000000..c02a8ad6d8c
--- /dev/null
+++ b/app/assets/javascripts/groups/components/item_type_icon.vue
@@ -0,0 +1,34 @@
+<script>
+import { ITEM_TYPE } from '../constants';
+
+export default {
+ props: {
+ itemType: {
+ type: String,
+ required: true,
+ },
+ isGroupOpen: {
+ type: Boolean,
+ required: true,
+ default: false,
+ },
+ },
+ computed: {
+ iconClass() {
+ if (this.itemType === ITEM_TYPE.GROUP) {
+ return this.isGroupOpen ? 'fa-folder-open' : 'fa-folder';
+ }
+ return 'fa-bookmark';
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="item-type-icon">
+ <i
+ :class="iconClass"
+ class="fa"
+ aria-hidden="true"/>
+ </span>
+</template>
diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js
new file mode 100644
index 00000000000..6fde41414b3
--- /dev/null
+++ b/app/assets/javascripts/groups/constants.js
@@ -0,0 +1,35 @@
+import { __, s__ } from '../locale';
+
+export const MAX_CHILDREN_COUNT = 20;
+
+export const COMMON_STR = {
+ FAILURE: __('An error occurred. Please try again.'),
+ LEAVE_FORBIDDEN: s__('GroupsTree|Failed to leave the group. Please make sure you are not the only owner.'),
+ LEAVE_BTN_TITLE: s__('GroupsTree|Leave this group'),
+ EDIT_BTN_TITLE: s__('GroupsTree|Edit group'),
+ GROUP_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups matched your search'),
+ GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups or projects matched your search'),
+};
+
+export const ITEM_TYPE = {
+ PROJECT: 'project',
+ GROUP: 'group',
+};
+
+export const GROUP_VISIBILITY_TYPE = {
+ public: __('Public - The group and any public projects can be viewed without any authentication.'),
+ internal: __('Internal - The group and any internal projects can be viewed by any logged in user.'),
+ private: __('Private - The group and its projects can only be viewed by members.'),
+};
+
+export const PROJECT_VISIBILITY_TYPE = {
+ public: __('Public - The project can be accessed without any authentication.'),
+ internal: __('Internal - The project can be accessed by any logged in user.'),
+ private: __('Private - Project access must be granted explicitly to each user.'),
+};
+
+export const VISIBILITY_TYPE_ICON = {
+ public: 'fa-globe',
+ internal: 'fa-shield',
+ private: 'fa-lock',
+};
diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js
index 83b102764ba..2db233b09da 100644
--- a/app/assets/javascripts/groups/groups_filterable_list.js
+++ b/app/assets/javascripts/groups/groups_filterable_list.js
@@ -3,12 +3,13 @@ import eventHub from './event_hub';
import { getParameterByName } from '../lib/utils/common_utils';
export default class GroupFilterableList extends FilterableList {
- constructor({ form, filter, holder, filterEndpoint, pagePath }) {
- super(form, filter, holder);
+ constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) {
+ super(form, filter, holder, filterInputField);
this.form = form;
this.filterEndpoint = filterEndpoint;
this.pagePath = pagePath;
- this.$dropdown = $('.js-group-filter-dropdown-wrap');
+ this.filterInputField = filterInputField;
+ this.$dropdown = $(dropdownSel);
}
getFilterEndpoint() {
@@ -24,30 +25,34 @@ export default class GroupFilterableList extends FilterableList {
bindEvents() {
super.bindEvents();
- this.onFormSubmitWrapper = this.onFormSubmit.bind(this);
this.onFilterOptionClikWrapper = this.onOptionClick.bind(this);
- this.filterForm.addEventListener('submit', this.onFormSubmitWrapper);
this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);
}
- onFormSubmit(e) {
- e.preventDefault();
-
- const $form = $(this.form);
- const filterGroupsParam = $form.find('[name="filter_groups"]').val();
+ onFilterInput() {
const queryData = {};
+ const $form = $(this.form);
+ const archivedParam = getParameterByName('archived', window.location.href);
+ const filterGroupsParam = $form.find(`[name="${this.filterInputField}"]`).val();
if (filterGroupsParam) {
- queryData.filter_groups = filterGroupsParam;
+ queryData[this.filterInputField] = filterGroupsParam;
+ }
+
+ if (archivedParam) {
+ queryData.archived = archivedParam;
}
this.filterResults(queryData);
- this.setDefaultFilterOption();
+
+ if (this.setDefaultFilterOption) {
+ this.setDefaultFilterOption();
+ }
}
setDefaultFilterOption() {
- const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu a:first-child').text());
+ const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').first().text());
this.$dropdown.find('.dropdown-label').text(defaultOption);
}
@@ -55,23 +60,42 @@ export default class GroupFilterableList extends FilterableList {
e.preventDefault();
const queryData = {};
- const sortParam = getParameterByName('sort', e.currentTarget.href);
+
+ // Get type of option selected from dropdown
+ const currentTargetClassList = e.currentTarget.parentElement.classList;
+ const isOptionFilterBySort = currentTargetClassList.contains('js-filter-sort-order');
+ const isOptionFilterByArchivedProjects = currentTargetClassList.contains('js-filter-archived-projects');
+
+ // Get option query param, also preserve currently applied query param
+ const sortParam = getParameterByName('sort', isOptionFilterBySort ? e.currentTarget.href : window.location.href);
+ const archivedParam = getParameterByName('archived', isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href);
if (sortParam) {
queryData.sort = sortParam;
}
+ if (archivedParam) {
+ queryData.archived = archivedParam;
+ }
+
this.filterResults(queryData);
// Active selected option
- this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
+ if (isOptionFilterBySort) {
+ this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
+ this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').removeClass('is-active');
+ } else if (isOptionFilterByArchivedProjects) {
+ this.$dropdown.find('.dropdown-menu li.js-filter-archived-projects a').removeClass('is-active');
+ }
+
+ $(e.target).addClass('is-active');
// Clear current value on search form
- this.form.querySelector('[name="filter_groups"]').value = '';
+ this.form.querySelector(`[name="${this.filterInputField}"]`).value = '';
}
onFilterSuccess(data, xhr, queryData) {
- super.onFilterSuccess(data, xhr, queryData);
+ const currentPath = this.getPagePath(queryData);
const paginationData = {
'X-Per-Page': xhr.getResponseHeader('X-Per-Page'),
@@ -82,7 +106,11 @@ export default class GroupFilterableList extends FilterableList {
'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'),
};
- eventHub.$emit('updateGroups', data);
+ window.history.replaceState({
+ page: currentPath,
+ }, document.title, currentPath);
+
+ eventHub.$emit('updateGroups', data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField));
eventHub.$emit('updatePagination', paginationData);
}
}
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index 9ad8e5c6052..8b850765a1b 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -1,17 +1,17 @@
-/* global Flash */
-
import Vue from 'vue';
+import Translate from '../vue_shared/translate';
import GroupFilterableList from './groups_filterable_list';
-import GroupsComponent from './components/groups.vue';
-import GroupFolder from './components/group_folder.vue';
-import GroupItem from './components/group_item.vue';
-import GroupsStore from './stores/groups_store';
-import GroupsService from './services/groups_service';
-import eventHub from './event_hub';
-import { getParameterByName } from '../lib/utils/common_utils';
+import GroupsStore from './store/groups_store';
+import GroupsService from './service/groups_service';
+
+import groupsApp from './components/app.vue';
+import groupFolderComponent from './components/group_folder.vue';
+import groupItemComponent from './components/group_item.vue';
+
+Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
- const el = document.getElementById('dashboard-group-app');
+ const el = document.getElementById('js-groups-tree');
// Don't do anything if element doesn't exist (No groups)
// This is for when the user enters directly to the page via URL
@@ -19,176 +19,56 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
- Vue.component('groups-component', GroupsComponent);
- Vue.component('group-folder', GroupFolder);
- Vue.component('group-item', GroupItem);
+ Vue.component('group-folder', groupFolderComponent);
+ Vue.component('group-item', groupItemComponent);
// eslint-disable-next-line no-new
new Vue({
el,
+ components: {
+ groupsApp,
+ },
data() {
- this.store = new GroupsStore();
- this.service = new GroupsService(el.dataset.endpoint);
+ const dataset = this.$options.el.dataset;
+ const hideProjects = dataset.hideProjects === 'true';
+ const store = new GroupsStore(hideProjects);
+ const service = new GroupsService(dataset.endpoint);
return {
- store: this.store,
- isLoading: true,
- state: this.store.state,
+ store,
+ service,
+ hideProjects,
loading: true,
};
},
- computed: {
- isEmpty() {
- return Object.keys(this.state.groups).length === 0;
- },
- },
- methods: {
- fetchGroups(parentGroup) {
- let parentId = null;
- let getGroups = null;
- let page = null;
- let sort = null;
- let pageParam = null;
- let sortParam = null;
- let filterGroups = null;
- let filterGroupsParam = null;
-
- if (parentGroup) {
- parentId = parentGroup.id;
- } else {
- this.isLoading = true;
- }
-
- pageParam = getParameterByName('page');
- if (pageParam) {
- page = pageParam;
- }
-
- filterGroupsParam = getParameterByName('filter_groups');
- if (filterGroupsParam) {
- filterGroups = filterGroupsParam;
- }
-
- sortParam = getParameterByName('sort');
- if (sortParam) {
- sort = sortParam;
- }
-
- getGroups = this.service.getGroups(parentId, page, filterGroups, sort);
- getGroups
- .then(response => response.json())
- .then((response) => {
- this.isLoading = false;
-
- this.updateGroups(response, parentGroup);
- })
- .catch(this.handleErrorResponse);
-
- return getGroups;
- },
- fetchPage(page, filterGroups, sort) {
- this.isLoading = true;
-
- return this.service
- .getGroups(null, page, filterGroups, sort)
- .then((response) => {
- this.isLoading = false;
- $.scrollTo(0);
-
- const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href);
- window.history.replaceState({
- page: currentPath,
- }, document.title, currentPath);
-
- return response.json().then((data) => {
- this.updateGroups(data);
- this.updatePagination(response.headers);
- });
- })
- .catch(this.handleErrorResponse);
- },
- toggleSubGroups(parentGroup = null) {
- if (!parentGroup.isOpen) {
- this.store.resetGroups(parentGroup);
- this.fetchGroups(parentGroup);
- }
-
- this.store.toggleSubGroups(parentGroup);
- },
- leaveGroup(group, collection) {
- this.service.leaveGroup(group.leavePath)
- .then(resp => resp.json())
- .then((response) => {
- $.scrollTo(0);
-
- this.store.removeGroup(group, collection);
-
- // eslint-disable-next-line no-new
- new Flash(response.notice, 'notice');
- })
- .catch((error) => {
- let message = 'An error occurred. Please try again.';
-
- if (error.status === 403) {
- message = 'Failed to leave the group. Please make sure you are not the only owner';
- }
-
- // eslint-disable-next-line no-new
- new Flash(message);
- });
- },
- updateGroups(groups, parentGroup) {
- this.store.setGroups(groups, parentGroup);
- },
- updatePagination(headers) {
- this.store.storePagination(headers);
- },
- handleErrorResponse() {
- this.isLoading = false;
- $.scrollTo(0);
-
- // eslint-disable-next-line no-new
- new Flash('An error occurred. Please try again.');
- },
- },
- created() {
- eventHub.$on('fetchPage', this.fetchPage);
- eventHub.$on('toggleSubGroups', this.toggleSubGroups);
- eventHub.$on('leaveGroup', this.leaveGroup);
- eventHub.$on('updateGroups', this.updateGroups);
- eventHub.$on('updatePagination', this.updatePagination);
- },
beforeMount() {
+ const dataset = this.$options.el.dataset;
let groupFilterList = null;
- const form = document.querySelector('form#group-filter-form');
- const filter = document.querySelector('.js-groups-list-filter');
- const holder = document.querySelector('.js-groups-list-holder');
+ const form = document.querySelector(dataset.formSel);
+ const filter = document.querySelector(dataset.filterSel);
+ const holder = document.querySelector(dataset.holderSel);
const opts = {
form,
filter,
holder,
- filterEndpoint: el.dataset.endpoint,
- pagePath: el.dataset.path,
+ filterEndpoint: dataset.endpoint,
+ pagePath: dataset.path,
+ dropdownSel: dataset.dropdownSel,
+ filterInputField: 'filter',
};
groupFilterList = new GroupFilterableList(opts);
groupFilterList.initSearch();
},
- mounted() {
- this.fetchGroups()
- .then((response) => {
- this.updatePagination(response.headers);
- this.isLoading = false;
- })
- .catch(this.handleErrorResponse);
- },
- beforeDestroy() {
- eventHub.$off('fetchPage', this.fetchPage);
- eventHub.$off('toggleSubGroups', this.toggleSubGroups);
- eventHub.$off('leaveGroup', this.leaveGroup);
- eventHub.$off('updateGroups', this.updateGroups);
- eventHub.$off('updatePagination', this.updatePagination);
+ render(createElement) {
+ return createElement('groups-app', {
+ props: {
+ store: this.store,
+ service: this.service,
+ hideProjects: this.hideProjects,
+ },
+ });
},
});
});
diff --git a/app/assets/javascripts/groups/new_group_child.js b/app/assets/javascripts/groups/new_group_child.js
new file mode 100644
index 00000000000..8e273579aae
--- /dev/null
+++ b/app/assets/javascripts/groups/new_group_child.js
@@ -0,0 +1,62 @@
+import DropLab from '../droplab/drop_lab';
+import ISetter from '../droplab/plugins/input_setter';
+
+const InputSetter = Object.assign({}, ISetter);
+
+const NEW_PROJECT = 'new-project';
+const NEW_SUBGROUP = 'new-subgroup';
+
+export default class NewGroupChild {
+ constructor(buttonWrapper) {
+ this.buttonWrapper = buttonWrapper;
+ this.newGroupChildButton = this.buttonWrapper.querySelector('.js-new-group-child');
+ this.dropdownToggle = this.buttonWrapper.querySelector('.js-dropdown-toggle');
+ this.dropdownList = this.buttonWrapper.querySelector('.dropdown-menu');
+
+ this.newGroupPath = this.buttonWrapper.dataset.projectPath;
+ this.subgroupPath = this.buttonWrapper.dataset.subgroupPath;
+
+ this.init();
+ }
+
+ init() {
+ this.initDroplab();
+ this.bindEvents();
+ }
+
+ initDroplab() {
+ this.droplab = new DropLab();
+ this.droplab.init(
+ this.dropdownToggle,
+ this.dropdownList,
+ [InputSetter],
+ this.getDroplabConfig(),
+ );
+ }
+
+ getDroplabConfig() {
+ return {
+ InputSetter: [{
+ input: this.newGroupChildButton,
+ valueAttribute: 'data-value',
+ inputAttribute: 'data-action',
+ }, {
+ input: this.newGroupChildButton,
+ valueAttribute: 'data-text',
+ }],
+ };
+ }
+
+ bindEvents() {
+ this.newGroupChildButton
+ .addEventListener('click', this.onClickNewGroupChildButton.bind(this));
+ }
+
+ onClickNewGroupChildButton(e) {
+ if (e.target.dataset.action === NEW_PROJECT) {
+ gl.utils.visitUrl(this.newGroupPath);
+ } else if (e.target.dataset.action === NEW_SUBGROUP) {
+ gl.utils.visitUrl(this.subgroupPath);
+ }
+ }
+}
diff --git a/app/assets/javascripts/groups/service/groups_service.js b/app/assets/javascripts/groups/service/groups_service.js
new file mode 100644
index 00000000000..639410384c2
--- /dev/null
+++ b/app/assets/javascripts/groups/service/groups_service.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class GroupsService {
+ constructor(endpoint) {
+ this.groups = Vue.resource(endpoint);
+ }
+
+ getGroups(parentId, page, filterGroups, sort, archived) {
+ const data = {};
+
+ if (parentId) {
+ data.parent_id = parentId;
+ } else {
+ // Do not send the following param for sub groups
+ if (page) {
+ data.page = page;
+ }
+
+ if (filterGroups) {
+ data.filter = filterGroups;
+ }
+
+ if (sort) {
+ data.sort = sort;
+ }
+
+ if (archived) {
+ data.archived = archived;
+ }
+ }
+
+ return this.groups.get(data);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ leaveGroup(endpoint) {
+ return Vue.http.delete(endpoint);
+ }
+}
diff --git a/app/assets/javascripts/groups/services/groups_service.js b/app/assets/javascripts/groups/services/groups_service.js
deleted file mode 100644
index 97e02fcb76d..00000000000
--- a/app/assets/javascripts/groups/services/groups_service.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
-
-export default class GroupsService {
- constructor(endpoint) {
- this.groups = Vue.resource(endpoint);
- }
-
- getGroups(parentId, page, filterGroups, sort) {
- const data = {};
-
- if (parentId) {
- data.parent_id = parentId;
- } else {
- // Do not send the following param for sub groups
- if (page) {
- data.page = page;
- }
-
- if (filterGroups) {
- data.filter_groups = filterGroups;
- }
-
- if (sort) {
- data.sort = sort;
- }
- }
-
- return this.groups.get(data);
- }
-
- // eslint-disable-next-line class-methods-use-this
- leaveGroup(endpoint) {
- return Vue.http.delete(endpoint);
- }
-}
diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js
new file mode 100644
index 00000000000..a1689f4c5cc
--- /dev/null
+++ b/app/assets/javascripts/groups/store/groups_store.js
@@ -0,0 +1,105 @@
+import { normalizeHeaders, parseIntPagination } from '../../lib/utils/common_utils';
+
+export default class GroupsStore {
+ constructor(hideProjects) {
+ this.state = {};
+ this.state.groups = [];
+ this.state.pageInfo = {};
+ this.hideProjects = hideProjects;
+ }
+
+ setGroups(rawGroups) {
+ if (rawGroups && rawGroups.length) {
+ this.state.groups = rawGroups.map(rawGroup => this.formatGroupItem(rawGroup));
+ } else {
+ this.state.groups = [];
+ }
+ }
+
+ setSearchedGroups(rawGroups) {
+ const formatGroups = groups => groups.map((group) => {
+ const formattedGroup = this.formatGroupItem(group);
+ if (formattedGroup.children && formattedGroup.children.length) {
+ formattedGroup.children = formatGroups(formattedGroup.children);
+ }
+ return formattedGroup;
+ });
+
+ if (rawGroups && rawGroups.length) {
+ this.state.groups = formatGroups(rawGroups);
+ } else {
+ this.state.groups = [];
+ }
+ }
+
+ setGroupChildren(parentGroup, children) {
+ const updatedParentGroup = parentGroup;
+ updatedParentGroup.children = children.map(rawChild => this.formatGroupItem(rawChild));
+ updatedParentGroup.isOpen = true;
+ updatedParentGroup.isChildrenLoading = false;
+ }
+
+ getGroups() {
+ return this.state.groups;
+ }
+
+ setPaginationInfo(pagination = {}) {
+ let paginationInfo;
+
+ if (Object.keys(pagination).length) {
+ const normalizedHeaders = normalizeHeaders(pagination);
+ paginationInfo = parseIntPagination(normalizedHeaders);
+ } else {
+ paginationInfo = pagination;
+ }
+
+ this.state.pageInfo = paginationInfo;
+ }
+
+ getPaginationInfo() {
+ return this.state.pageInfo;
+ }
+
+ formatGroupItem(rawGroupItem) {
+ const groupChildren = rawGroupItem.children || [];
+ const groupIsOpen = (groupChildren.length > 0) || false;
+ const childrenCount = this.hideProjects ?
+ rawGroupItem.subgroup_count :
+ rawGroupItem.children_count;
+
+ return {
+ id: rawGroupItem.id,
+ name: rawGroupItem.name,
+ fullName: rawGroupItem.full_name,
+ description: rawGroupItem.description,
+ visibility: rawGroupItem.visibility,
+ avatarUrl: rawGroupItem.avatar_url,
+ relativePath: rawGroupItem.relative_path,
+ editPath: rawGroupItem.edit_path,
+ leavePath: rawGroupItem.leave_path,
+ canEdit: rawGroupItem.can_edit,
+ canLeave: rawGroupItem.can_leave,
+ type: rawGroupItem.type,
+ permission: rawGroupItem.permission,
+ children: groupChildren,
+ isOpen: groupIsOpen,
+ isChildrenLoading: false,
+ isBeingRemoved: false,
+ parentId: rawGroupItem.parent_id,
+ childrenCount,
+ projectCount: rawGroupItem.project_count,
+ subgroupCount: rawGroupItem.subgroup_count,
+ memberCount: rawGroupItem.number_users_with_delimiter,
+ starCount: rawGroupItem.star_count,
+ };
+ }
+
+ removeGroup(group, parentGroup) {
+ const updatedParentGroup = parentGroup;
+ if (updatedParentGroup.children && updatedParentGroup.children.length) {
+ updatedParentGroup.children = parentGroup.children.filter(child => group.id !== child.id);
+ } else {
+ this.state.groups = this.state.groups.filter(child => group.id !== child.id);
+ }
+ }
+}
diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js
deleted file mode 100644
index f59ec677603..00000000000
--- a/app/assets/javascripts/groups/stores/groups_store.js
+++ /dev/null
@@ -1,167 +0,0 @@
-import Vue from 'vue';
-import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
-
-export default class GroupsStore {
- constructor() {
- this.state = {};
- this.state.groups = {};
- this.state.pageInfo = {};
- }
-
- setGroups(rawGroups, parent) {
- const parentGroup = parent;
- const tree = this.buildTree(rawGroups, parentGroup);
-
- if (parentGroup) {
- parentGroup.subGroups = tree;
- } else {
- this.state.groups = tree;
- }
-
- return tree;
- }
-
- // eslint-disable-next-line class-methods-use-this
- resetGroups(parent) {
- const parentGroup = parent;
- parentGroup.subGroups = {};
- }
-
- storePagination(pagination = {}) {
- let paginationInfo;
-
- if (Object.keys(pagination).length) {
- const normalizedHeaders = normalizeHeaders(pagination);
- paginationInfo = parseIntPagination(normalizedHeaders);
- } else {
- paginationInfo = pagination;
- }
-
- this.state.pageInfo = paginationInfo;
- }
-
- buildTree(rawGroups, parentGroup) {
- const groups = this.decorateGroups(rawGroups);
- const tree = {};
- const mappedGroups = {};
- const orphans = [];
-
- // Map groups to an object
- groups.map((group) => {
- mappedGroups[`id${group.id}`] = group;
- mappedGroups[`id${group.id}`].subGroups = {};
- return group;
- });
-
- Object.keys(mappedGroups).map((key) => {
- const currentGroup = mappedGroups[key];
- if (currentGroup.parentId) {
- // If the group is not at the root level, add it to its parent array of subGroups.
- const findParentGroup = mappedGroups[`id${currentGroup.parentId}`];
- if (findParentGroup) {
- mappedGroups[`id${currentGroup.parentId}`].subGroups[`id${currentGroup.id}`] = currentGroup;
- mappedGroups[`id${currentGroup.parentId}`].isOpen = true; // Expand group if it has subgroups
- } else if (parentGroup && parentGroup.id === currentGroup.parentId) {
- tree[`id${currentGroup.id}`] = currentGroup;
- } else {
- // No parent found. We save it for later processing
- orphans.push(currentGroup);
-
- // Add to tree to preserve original order
- tree[`id${currentGroup.id}`] = currentGroup;
- }
- } else {
- // If the group is at the top level, add it to first level elements array.
- tree[`id${currentGroup.id}`] = currentGroup;
- }
-
- return key;
- });
-
- if (orphans.length) {
- orphans.map((orphan) => {
- let found = false;
- const currentOrphan = orphan;
-
- Object.keys(tree).map((key) => {
- const group = tree[key];
-
- if (
- group &&
- currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0 &&
- // Make sure the currently selected orphan is not the same as the group
- // we are checking here otherwise it will end up in an infinite loop
- currentOrphan.id !== group.id
- ) {
- group.subGroups[currentOrphan.id] = currentOrphan;
- group.isOpen = true;
- currentOrphan.isOrphan = true;
- found = true;
-
- // Delete if group was put at the top level. If not the group will be displayed twice.
- if (tree[`id${currentOrphan.id}`]) {
- delete tree[`id${currentOrphan.id}`];
- }
- }
-
- return key;
- });
-
- if (!found) {
- currentOrphan.isOrphan = true;
-
- tree[`id${currentOrphan.id}`] = currentOrphan;
- }
-
- return orphan;
- });
- }
-
- return tree;
- }
-
- decorateGroups(rawGroups) {
- this.groups = rawGroups.map(this.decorateGroup);
- return this.groups;
- }
-
- // eslint-disable-next-line class-methods-use-this
- decorateGroup(rawGroup) {
- return {
- id: rawGroup.id,
- fullName: rawGroup.full_name,
- fullPath: rawGroup.full_path,
- avatarUrl: rawGroup.avatar_url,
- name: rawGroup.name,
- hasSubgroups: rawGroup.has_subgroups,
- canEdit: rawGroup.can_edit,
- description: rawGroup.description,
- webUrl: rawGroup.web_url,
- groupPath: rawGroup.group_path,
- parentId: rawGroup.parent_id,
- visibility: rawGroup.visibility,
- leavePath: rawGroup.leave_path,
- editPath: rawGroup.edit_path,
- isOpen: false,
- isOrphan: false,
- numberProjects: rawGroup.number_projects_with_delimiter,
- numberUsers: rawGroup.number_users_with_delimiter,
- permissions: {
- humanGroupAccess: rawGroup.permissions.human_group_access,
- },
- subGroups: {},
- };
- }
-
- // eslint-disable-next-line class-methods-use-this
- removeGroup(group, collection) {
- Vue.delete(collection, `id${group.id}`);
- }
-
- // eslint-disable-next-line class-methods-use-this
- toggleSubGroups(toggleGroup) {
- const group = toggleGroup;
- group.isOpen = !group.isOpen;
- return group;
- }
-}
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 90ca70289ab..a69a0bde17b 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,121 +1,86 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var,
- camelcase, one-var-declaration-per-line, quotes, object-shorthand,
- prefer-arrow-callback, comma-dangle, consistent-return, yoda,
- prefer-rest-params, prefer-spread, no-unused-vars, prefer-template,
- promise/catch-or-return */
import Api from './api';
import { normalizeCRLFHeaders } from './lib/utils/common_utils';
-var slice = [].slice;
+export default function groupsSelect() {
+ // Needs to be accessible in rspec
+ window.GROUP_SELECT_PER_PAGE = 20;
+ $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
+ const $select = $(this);
+ const allAvailable = $select.data('all-available');
+ const skipGroups = $select.data('skip-groups') || [];
+ $select.select2({
+ placeholder: 'Search for a group',
+ multiple: $select.hasClass('multiselect'),
+ minimumInputLength: 0,
+ ajax: {
+ url: Api.buildUrl(Api.groupsPath),
+ dataType: 'json',
+ quietMillis: 250,
+ transport(params) {
+ return $.ajax(params)
+ .then((data, status, xhr) => {
+ const results = data || [];
-window.GroupsSelect = (function() {
- function GroupsSelect() {
- $('.ajax-groups-select').each((function(_this) {
- const self = _this;
-
- return function(i, select) {
- var all_available, skip_groups;
- const $select = $(select);
- all_available = $select.data('all-available');
- skip_groups = $select.data('skip-groups') || [];
-
- $select.select2({
- placeholder: "Search for a group",
- multiple: $select.hasClass('multiselect'),
- minimumInputLength: 0,
- ajax: {
- url: Api.buildUrl(Api.groupsPath),
- dataType: 'json',
- quietMillis: 250,
- transport: function (params) {
- $.ajax(params).then((data, status, xhr) => {
- const results = data || [];
-
- const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders());
- const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
- const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
- const more = currentPage < totalPages;
-
- return {
- results,
- pagination: {
- more,
- },
- };
- }).then(params.success).fail(params.error);
- },
- data: function (search, page) {
- return {
- search,
- page,
- per_page: GroupsSelect.PER_PAGE,
- all_available,
- };
- },
- results: function (data, page) {
- if (data.length) return { results: [] };
-
- const groups = data.length ? data : data.results || [];
- const more = data.pagination ? data.pagination.more : false;
- const results = groups.filter(group => skip_groups.indexOf(group.id) === -1);
+ const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders());
+ const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
+ const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
+ const more = currentPage < totalPages;
return {
results,
- page,
- more,
+ pagination: {
+ more,
+ },
};
- },
- },
- initSelection: function(element, callback) {
- var id;
- id = $(element).val();
- if (id !== "") {
- return Api.group(id, callback);
- }
- },
- formatResult: function() {
- var args;
- args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return self.formatResult.apply(self, args);
- },
- formatSelection: function() {
- var args;
- args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return self.formatSelection.apply(self, args);
- },
- dropdownCssClass: "ajax-groups-dropdown select2-infinite",
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup: function(m) {
- return m;
- }
- });
-
- self.dropdown = document.querySelector('.select2-infinite .select2-results');
-
- $select.on('select2-loaded', self.forceOverflow.bind(self));
- };
- })(this));
- }
-
- GroupsSelect.prototype.formatResult = function(group) {
- var avatar;
- if (group.avatar_url) {
- avatar = group.avatar_url;
- } else {
- avatar = gon.default_avatar_url;
- }
- return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>";
- };
-
- GroupsSelect.prototype.formatSelection = function(group) {
- return group.full_name;
- };
+ })
+ .then(params.success)
+ .fail(params.error);
+ },
+ data(search, page) {
+ return {
+ search,
+ page,
+ per_page: window.GROUP_SELECT_PER_PAGE,
+ all_available: allAvailable,
+ };
+ },
+ results(data, page) {
+ if (data.length) return { results: [] };
- GroupsSelect.prototype.forceOverflow = function (e) {
- this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`;
- };
+ const groups = data.length ? data : data.results || [];
+ const more = data.pagination ? data.pagination.more : false;
+ const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
- GroupsSelect.PER_PAGE = 20;
+ return {
+ results,
+ page,
+ more,
+ };
+ },
+ },
+ // eslint-disable-next-line consistent-return
+ initSelection(element, callback) {
+ const id = $(element).val();
+ if (id !== '') {
+ return Api.group(id, callback);
+ }
+ },
+ formatResult(object) {
+ return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`;
+ },
+ formatSelection(object) {
+ return object.full_name;
+ },
+ dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup(m) {
+ return m;
+ },
+ });
- return GroupsSelect;
-})();
+ $select.on('select2-loaded', () => {
+ const dropdown = document.querySelector('.select2-infinite .select2-results');
+ dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
+ });
+ });
+}
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index dc170c60456..33a352e158a 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,7 +1,18 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */
+import { highCountTrim } from '~/lib/utils/text_utility';
-$(document).on('todo:toggle', function(e, count) {
- var $todoPendingCount = $('.todos-count');
- $todoPendingCount.text(gl.text.highCountTrim(count));
- $todoPendingCount.toggleClass('hidden', count === 0);
-});
+/**
+ * Updates todo counter when todos are toggled.
+ * When count is 0, we hide the badge.
+ *
+ * @param {jQuery.Event} e
+ * @param {String} count
+ */
+export default function initTodoToggle() {
+ $(document).on('todo:toggle', (e, count) => {
+ const parsedCount = parseInt(count, 10);
+ const $todoPendingCount = $('.todos-count');
+
+ $todoPendingCount.text(highCountTrim(parsedCount));
+ $todoPendingCount.toggleClass('hidden', parsedCount === 0);
+ });
+}
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 5b4ca94ed30..1dc70872d92 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -1,83 +1,81 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, vars-on-top, no-new, max-len */
+class ImporterStatus {
+ constructor(jobsUrl, importUrl) {
+ this.jobsUrl = jobsUrl;
+ this.importUrl = importUrl;
+ this.initStatusPage();
+ this.setAutoUpdate();
+ }
-(function() {
- window.ImporterStatus = (function() {
- function ImporterStatus(jobs_url, import_url) {
- this.jobs_url = jobs_url;
- this.import_url = import_url;
- this.initStatusPage();
- this.setAutoUpdate();
- }
+ initStatusPage() {
+ $('.js-add-to-import')
+ .off('click')
+ .on('click', (event) => {
+ const $btn = $(event.currentTarget);
+ const $tr = $btn.closest('tr');
+ const $targetField = $tr.find('.import-target');
+ const $namespaceInput = $targetField.find('.js-select-namespace option:selected');
+ const id = $tr.attr('id').replace('repo_', '');
+ let targetNamespace;
+ let newName;
+ if ($namespaceInput.length > 0) {
+ targetNamespace = $namespaceInput[0].innerHTML;
+ newName = $targetField.find('#path').prop('value');
+ $targetField.empty().append(`${targetNamespace}/${newName}`);
+ }
+ $btn.disable().addClass('is-loading');
- ImporterStatus.prototype.initStatusPage = function() {
- $('.js-add-to-import').off('click').on('click', (function(_this) {
- return function(e) {
- var $btn, $namespace_input, $target_field, $tr, id, target_namespace, newName;
- $btn = $(e.currentTarget);
- $tr = $btn.closest('tr');
- $target_field = $tr.find('.import-target');
- $namespace_input = $target_field.find('.js-select-namespace option:selected');
- id = $tr.attr('id').replace('repo_', '');
- target_namespace = null;
- newName = null;
- if ($namespace_input.length > 0) {
- target_namespace = $namespace_input[0].innerHTML;
- newName = $target_field.find('#path').prop('value');
- $target_field.empty().append(target_namespace + "/" + newName);
- }
- $btn.disable().addClass('is-loading');
- return $.post(_this.import_url, {
- repo_id: id,
- target_namespace: target_namespace,
- new_name: newName
- }, {
- dataType: 'script'
- });
- };
- })(this));
- return $('.js-import-all').off('click').on('click', function(e) {
- var $btn;
- $btn = $(this);
+ return $.post(this.importUrl, {
+ repo_id: id,
+ target_namespace: targetNamespace,
+ new_name: newName,
+ }, {
+ dataType: 'script',
+ });
+ });
+
+ $('.js-import-all')
+ .off('click')
+ .on('click', function onClickImportAll() {
+ const $btn = $(this);
$btn.disable().addClass('is-loading');
- return $('.js-add-to-import').each(function() {
+ return $('.js-add-to-import').each(function triggerAddImport() {
return $(this).trigger('click');
});
});
- };
+ }
+
+ setAutoUpdate() {
+ return setInterval(() => $.get(this.jobsUrl, data => $.each(data, (i, job) => {
+ const jobItem = $(`#project_${job.id}`);
+ const statusField = jobItem.find('.job-status');
- ImporterStatus.prototype.setAutoUpdate = function() {
- return setInterval(((function(_this) {
- return function() {
- return $.get(_this.jobs_url, function(data) {
- return $.each(data, function(i, job) {
- var job_item, status_field;
- job_item = $("#project_" + job.id);
- status_field = job_item.find(".job-status");
- if (job.import_status === 'finished') {
- job_item.removeClass("active").addClass("success");
- return status_field.html('<span><i class="fa fa-check"></i> done</span>');
- } else if (job.import_status === 'scheduled') {
- return status_field.html("<i class='fa fa-spinner fa-spin'></i> scheduled");
- } else if (job.import_status === 'started') {
- return status_field.html("<i class='fa fa-spinner fa-spin'></i> started");
- } else {
- return status_field.html(job.import_status);
- }
- });
- });
- };
- })(this)), 4000);
- };
+ const spinner = '<i class="fa fa-spinner fa-spin"></i>';
- return ImporterStatus;
- })();
+ switch (job.import_status) {
+ case 'finished':
+ jobItem.removeClass('active').addClass('success');
+ statusField.html('<span><i class="fa fa-check"></i> done</span>');
+ break;
+ case 'scheduled':
+ statusField.html(`${spinner} scheduled`);
+ break;
+ case 'started':
+ statusField.html(`${spinner} started`);
+ break;
+ default:
+ statusField.html(job.import_status);
+ break;
+ }
+ })), 4000);
+ }
+}
- $(function() {
- if ($('.js-importer-status').length) {
- var jobsImportPath = $('.js-importer-status').data('jobs-import-path');
- var importPath = $('.js-importer-status').data('import-path');
+// eslint-disable-next-line consistent-return
+export default function initImporterStatus() {
+ const importerStatus = document.querySelector('.js-importer-status');
- new window.ImporterStatus(jobsImportPath, importPath);
- }
- });
-}).call(window);
+ if (importerStatus) {
+ const data = importerStatus.dataset;
+ return new ImporterStatus(data.jobsImportPath, data.importPath);
+ }
+}
diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js
index f785ed29e6c..1bab7965c19 100644
--- a/app/assets/javascripts/init_changes_dropdown.js
+++ b/app/assets/javascripts/init_changes_dropdown.js
@@ -1,7 +1,7 @@
import stickyMonitor from './lib/utils/sticky';
-export default () => {
- stickyMonitor(document.querySelector('.js-diff-files-changed'));
+export default (stickyTop) => {
+ stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
$('.js-diff-stats-dropdown').glDropdown({
filterable: true,
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index 29e3d2ea94e..ada693afc46 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -1,9 +1,11 @@
/* eslint-disable no-new */
/* global MilestoneSelect */
-/* global LabelsSelect */
-/* global IssuableContext */
+import LabelsSelect from './labels_select';
+import IssuableContext from './issuable_context';
/* global Sidebar */
+import DueDateSelectors from './due_date_select';
+
export default () => {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
@@ -12,7 +14,6 @@ export default () => {
});
new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser);
- gl.Subscription.bindAll('.subscription');
- new gl.DueDateSelectors();
+ new DueDateSelectors();
window.sidebar = new Sidebar();
};
diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js
index 1211c2c802c..2cbb70220d0 100644
--- a/app/assets/javascripts/init_legacy_filters.js
+++ b/app/assets/javascripts/init_legacy_filters.js
@@ -1,15 +1,14 @@
/* eslint-disable no-new */
-/* global LabelsSelect */
+import LabelsSelect from './labels_select';
/* global MilestoneSelect */
-/* global IssueStatusSelect */
-/* global SubscriptionSelect */
-
+import subscriptionSelect from './subscription_select';
import UsersSelect from './users_select';
+import issueStatusSelect from './issue_status_select';
export default () => {
new UsersSelect();
new LabelsSelect();
new MilestoneSelect();
- new IssueStatusSelect();
- new SubscriptionSelect();
+ issueStatusSelect();
+ subscriptionSelect();
};
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index cf1e6a14725..32415a8791f 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -1,4 +1,4 @@
-/* global Flash */
+import Flash from '../flash';
export default class IntegrationSettingsForm {
constructor(formSelector) {
@@ -102,7 +102,7 @@ export default class IntegrationSettingsForm {
})
.done((res) => {
if (res.error) {
- new Flash(`${res.message} ${res.service_response}`, null, null, {
+ new Flash(`${res.message} ${res.service_response}`, 'alert', document, {
title: 'Save anyway',
clickHandler: (e) => {
e.preventDefault();
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index c39ffdb2e0f..b124fafec70 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -1,7 +1,6 @@
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
-/* global IssuableIndex */
-/* global Flash */
import _ from 'underscore';
+import Flash from './flash';
export default {
init({ container, form, issues, prefixId } = {}) {
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index 0e8a0519928..ba2b6737988 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -1,10 +1,11 @@
/* eslint-disable class-methods-use-this, no-new */
-/* global LabelsSelect */
/* global MilestoneSelect */
-/* global IssueStatusSelect */
-/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
+import './milestone_select';
+import issueStatusSelect from './issue_status_select';
+import subscriptionSelect from './subscription_select';
+import LabelsSelect from './labels_select';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
@@ -45,8 +46,8 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() {
new LabelsSelect();
new MilestoneSelect();
- new IssueStatusSelect();
- new SubscriptionSelect();
+ issueStatusSelect();
+ subscriptionSelect();
}
setupBulkUpdateActions() {
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 1d305f1eb2f..da99394ff90 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,33 +1,32 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
import Cookies from 'js-cookie';
import bp from './breakpoints';
import UsersSelect from './users_select';
-const PARTICIPANTS_ROW_COUNT = 7;
+export default class IssuableContext {
+ constructor(currentUser) {
+ this.userSelect = new UsersSelect(currentUser);
-(function() {
- this.IssuableContext = (function() {
- function IssuableContext(currentUser) {
- this.initParticipants();
- new UsersSelect(currentUser);
- $('select.select2').select2({
- width: 'resolve',
- dropdownAutoWidth: true
- });
- $(".issuable-sidebar .inline-update").on("change", "select", function() {
- return $(this).submit();
- });
- $(".issuable-sidebar .inline-update").on("change", ".js-assignee", function() {
- return $(this).submit();
- });
- $(document).off('click', '.issuable-sidebar .dropdown-content a').on('click', '.issuable-sidebar .dropdown-content a', function(e) {
- return e.preventDefault();
- });
- $(document).off('click', '.edit-link').on('click', '.edit-link', function(e) {
- var $block, $selectbox;
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+
+ $('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
+ return $(this).submit();
+ });
+ $('.issuable-sidebar .inline-update').on('change', '.js-assignee', function onClickAssignee() {
+ return $(this).submit();
+ });
+ $(document)
+ .off('click', '.issuable-sidebar .dropdown-content a')
+ .on('click', '.issuable-sidebar .dropdown-content a', e => e.preventDefault());
+
+ $(document)
+ .off('click', '.edit-link')
+ .on('click', '.edit-link', function onClickEdit(e) {
e.preventDefault();
- $block = $(this).parents('.block');
- $selectbox = $block.find('.selectbox');
+ const $block = $(this).parents('.block');
+ const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) {
$selectbox.hide();
$block.find('.value').show();
@@ -35,47 +34,18 @@ const PARTICIPANTS_ROW_COUNT = 7;
$selectbox.show();
$block.find('.value').hide();
}
- if ($selectbox.is(':visible')) {
- return setTimeout(function() {
- return $block.find('.dropdown-menu-toggle').trigger('click');
- }, 0);
- }
- });
- window.addEventListener('beforeunload', function() {
- // collapsed_gutter cookie hides the sidebar
- var bpBreakpoint = bp.getBreakpointSize();
- if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
- Cookies.set('collapsed_gutter', true);
- }
- });
- }
- IssuableContext.prototype.initParticipants = function() {
- $(document).on("click", ".js-participants-more", this.toggleHiddenParticipants);
- return $(".js-participants-author").each(function(i) {
- if (i >= PARTICIPANTS_ROW_COUNT) {
- return $(this).addClass("js-participants-hidden").hide();
+ if ($selectbox.is(':visible')) {
+ setTimeout(() => $block.find('.dropdown-menu-toggle').trigger('click'), 0);
}
});
- };
- IssuableContext.prototype.toggleHiddenParticipants = function(e) {
- var currentText, lessText, originalText;
- e.preventDefault();
- currentText = $(this).text().trim();
- lessText = $(this).data("less-text");
- originalText = $(this).data("original-text");
- if (currentText === originalText) {
- $(this).text(lessText);
-
- if (gl.lazyLoader) gl.lazyLoader.loadCheck();
- } else {
- $(this).text(originalText);
+ window.addEventListener('beforeunload', () => {
+ // collapsed_gutter cookie hides the sidebar
+ const bpBreakpoint = bp.getBreakpointSize();
+ if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
+ Cookies.set('collapsed_gutter', true);
}
-
- $(".js-participants-hidden").toggle();
- };
-
- return IssuableContext;
- })();
-}).call(window);
+ });
+ }
+}
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 470c39c6f76..57dcaa0e1ac 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,108 +1,107 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
+/* eslint-disable func-names, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */
-/* global Autosave */
-/* global dateFormat */
import Pikaday from 'pikaday';
+import Autosave from './autosave';
import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
+import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
-(function() {
- this.IssuableForm = (function() {
- IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
-
- function IssuableForm(form) {
- var $issuableDueDate, calendar;
- this.form = form;
- this.toggleWip = this.toggleWip.bind(this);
- this.renderWipExplanation = this.renderWipExplanation.bind(this);
- this.resetAutosave = this.resetAutosave.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
- new UsersSelect();
- new ZenMode();
- this.titleField = this.form.find("input[name*='[title]']");
- this.descriptionField = this.form.find("textarea[name*='[description]']");
- if (!(this.titleField.length && this.descriptionField.length)) {
- return;
- }
- this.initAutosave();
- this.form.on("submit", this.handleSubmit);
- this.form.on("click", ".btn-cancel", this.resetAutosave);
- this.initWip();
- $issuableDueDate = $('#issuable-due-date');
- if ($issuableDueDate.length) {
- calendar = new Pikaday({
- field: $issuableDueDate.get(0),
- theme: 'gitlab-theme animate-picker',
- format: 'yyyy-mm-dd',
- container: $issuableDueDate.parent().get(0),
- onSelect: function(dateText) {
- $issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
- }
- });
- calendar.setDate(new Date($issuableDueDate.val()));
- }
+export default class IssuableForm {
+ constructor(form) {
+ this.form = form;
+ this.toggleWip = this.toggleWip.bind(this);
+ this.renderWipExplanation = this.renderWipExplanation.bind(this);
+ this.resetAutosave = this.resetAutosave.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
+
+ new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
+ new UsersSelect();
+ new ZenMode();
+
+ this.titleField = this.form.find('input[name*="[title]"]');
+ this.descriptionField = this.form.find('textarea[name*="[description]"]');
+ if (!(this.titleField.length && this.descriptionField.length)) {
+ return;
}
- IssuableForm.prototype.initAutosave = function() {
- new Autosave(this.titleField, [document.location.pathname, document.location.search, "title"]);
- return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, "description"]);
- };
-
- IssuableForm.prototype.handleSubmit = function() {
- return this.resetAutosave();
- };
-
- IssuableForm.prototype.resetAutosave = function() {
- this.titleField.data("autosave").reset();
- return this.descriptionField.data("autosave").reset();
- };
-
- IssuableForm.prototype.initWip = function() {
- this.$wipExplanation = this.form.find(".js-wip-explanation");
- this.$noWipExplanation = this.form.find(".js-no-wip-explanation");
- if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
- return;
- }
- this.form.on("click", ".js-toggle-wip", this.toggleWip);
- this.titleField.on("keyup blur", this.renderWipExplanation);
- return this.renderWipExplanation();
- };
-
- IssuableForm.prototype.workInProgress = function() {
- return this.wipRegex.test(this.titleField.val());
- };
-
- IssuableForm.prototype.renderWipExplanation = function() {
- if (this.workInProgress()) {
- this.$wipExplanation.show();
- return this.$noWipExplanation.hide();
- } else {
- this.$wipExplanation.hide();
- return this.$noWipExplanation.show();
- }
- };
-
- IssuableForm.prototype.toggleWip = function(event) {
- event.preventDefault();
- if (this.workInProgress()) {
- this.removeWip();
- } else {
- this.addWip();
- }
- return this.renderWipExplanation();
- };
-
- IssuableForm.prototype.removeWip = function() {
- return this.titleField.val(this.titleField.val().replace(this.wipRegex, ""));
- };
-
- IssuableForm.prototype.addWip = function() {
- return this.titleField.val("WIP: " + (this.titleField.val()));
- };
-
- return IssuableForm;
- })();
-}).call(window);
+ this.initAutosave();
+ this.form.on('submit', this.handleSubmit);
+ this.form.on('click', '.btn-cancel', this.resetAutosave);
+ this.initWip();
+
+ const $issuableDueDate = $('#issuable-due-date');
+
+ if ($issuableDueDate.length) {
+ const calendar = new Pikaday({
+ field: $issuableDueDate.get(0),
+ theme: 'gitlab-theme animate-picker',
+ format: 'yyyy-mm-dd',
+ container: $issuableDueDate.parent().get(0),
+ parse: dateString => parsePikadayDate(dateString),
+ toString: date => pikadayToString(date),
+ onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)),
+ });
+ calendar.setDate(parsePikadayDate($issuableDueDate.val()));
+ }
+ }
+
+ initAutosave() {
+ new Autosave(this.titleField, [document.location.pathname, document.location.search, 'title']);
+ return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, 'description']);
+ }
+
+ handleSubmit() {
+ return this.resetAutosave();
+ }
+
+ resetAutosave() {
+ this.titleField.data('autosave').reset();
+ return this.descriptionField.data('autosave').reset();
+ }
+
+ initWip() {
+ this.$wipExplanation = this.form.find('.js-wip-explanation');
+ this.$noWipExplanation = this.form.find('.js-no-wip-explanation');
+ if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
+ return;
+ }
+ this.form.on('click', '.js-toggle-wip', this.toggleWip);
+ this.titleField.on('keyup blur', this.renderWipExplanation);
+ return this.renderWipExplanation();
+ }
+
+ workInProgress() {
+ return this.wipRegex.test(this.titleField.val());
+ }
+
+ renderWipExplanation() {
+ if (this.workInProgress()) {
+ this.$wipExplanation.show();
+ return this.$noWipExplanation.hide();
+ } else {
+ this.$wipExplanation.hide();
+ return this.$noWipExplanation.show();
+ }
+ }
+
+ toggleWip(event) {
+ event.preventDefault();
+ if (this.workInProgress()) {
+ this.removeWip();
+ } else {
+ this.addWip();
+ }
+ return this.renderWipExplanation();
+ }
+
+ removeWip() {
+ return this.titleField.val(this.titleField.val().replace(this.wipRegex, ''));
+ }
+
+ addWip() {
+ this.titleField.val(`WIP: ${(this.titleField.val())}`);
+ }
+}
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index ece0220c927..0b123a11a3b 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -1,171 +1,42 @@
-/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
-/* global IssuableIndex */
-import _ from 'underscore';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
-((global) => {
- var issuable_created;
-
- issuable_created = false;
-
- global.IssuableIndex = {
- init: function(pagePrefix) {
- IssuableIndex.initTemplates();
- IssuableIndex.initSearch();
- IssuableIndex.initBulkUpdate(pagePrefix);
- IssuableIndex.initResetFilters();
- IssuableIndex.resetIncomingEmailToken();
- IssuableIndex.initLabelFilterRemove();
- },
- initTemplates: function() {
- return IssuableIndex.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
- },
- initSearch: function() {
- const $searchInput = $('#issuable_search');
-
- IssuableIndex.initSearchState($searchInput);
-
- // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
- const debouncedExecSearch = _.debounce(IssuableIndex.executeSearch, 1000, false);
-
- $searchInput.off('keyup').on('keyup', debouncedExecSearch);
-
- // ensures existing filters are preserved when manually submitted
- $('#issuable_search_form').on('submit', (e) => {
- e.preventDefault();
- debouncedExecSearch(e);
- });
- },
- initSearchState: function($searchInput) {
- const currentSearchVal = $searchInput.val();
-
- IssuableIndex.searchState = {
- elem: $searchInput,
- current: currentSearchVal
- };
-
- IssuableIndex.maybeFocusOnSearch();
- },
- accessSearchPristine: function(set) {
- // store reference to previous value to prevent search on non-mutating keyup
- const state = IssuableIndex.searchState;
- const currentSearchVal = state.elem.val();
-
- if (set) {
- state.current = currentSearchVal;
- } else {
- return state.current === currentSearchVal;
- }
- },
- maybeFocusOnSearch: function() {
- const currentSearchVal = IssuableIndex.searchState.current;
- if (currentSearchVal && currentSearchVal !== '') {
- const queryLength = currentSearchVal.length;
- const $searchInput = IssuableIndex.searchState.elem;
-
- /* The following ensures that the cursor is initially placed at
- * the end of search input when focus is applied. It accounts
- * for differences in browser implementations of `setSelectionRange`
- * and cursor placement for elements in focus.
- */
- $searchInput.focus();
- if ($searchInput.setSelectionRange) {
- $searchInput.setSelectionRange(queryLength, queryLength);
- } else {
- $searchInput.val(currentSearchVal);
- }
- }
- },
- executeSearch: function(e) {
- const $search = $('#issuable_search');
- const $searchName = $search.attr('name');
- const $searchValue = $search.val();
- const $filtersForm = $('.js-filter-form');
- const $input = $(`input[name='${$searchName}']`, $filtersForm);
- const isPristine = IssuableIndex.accessSearchPristine();
-
- if (isPristine) {
- return;
- }
-
- if (!$input.length) {
- $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
- } else {
- $input.val($searchValue);
- }
-
- IssuableIndex.filterResults($filtersForm);
- },
- initLabelFilterRemove: function() {
- return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
- var $button;
- $button = $(this);
- // Remove the label input box
- $('input[name="label_name[]"]').filter(function() {
- return this.value === $button.data('label');
- }).remove();
- // Submit the form to get new data
- IssuableIndex.filterResults($('.filter-form'));
- });
- },
- filterResults: (function(_this) {
- return function(form) {
- var formAction, formData, issuesUrl;
- formData = form.serializeArray();
- formData = formData.filter(function(data) {
- return data.value !== '';
- });
- formData = $.param(formData);
- formAction = form.attr('action');
- issuesUrl = formAction;
- issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&');
- issuesUrl += formData;
- return gl.utils.visitUrl(issuesUrl);
- };
- })(this),
- initResetFilters: function() {
- $('.reset-filters').on('click', function(e) {
- e.preventDefault();
- const target = e.target;
- const $form = $(target).parents('.js-filter-form');
- const baseIssuesUrl = target.href;
-
- $form.attr('action', baseIssuesUrl);
- gl.utils.visitUrl(baseIssuesUrl);
+export default class IssuableIndex {
+ constructor(pagePrefix) {
+ this.initBulkUpdate(pagePrefix);
+ IssuableIndex.resetIncomingEmailToken();
+ }
+ initBulkUpdate(pagePrefix) {
+ const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
+ const alreadyInitialized = !!this.bulkUpdateSidebar;
+
+ if (userCanBulkUpdate && !alreadyInitialized) {
+ IssuableBulkUpdateActions.init({
+ prefixId: pagePrefix,
});
- },
- initBulkUpdate: function(pagePrefix) {
- const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
- const alreadyInitialized = !!this.bulkUpdateSidebar;
-
- if (userCanBulkUpdate && !alreadyInitialized) {
- IssuableBulkUpdateActions.init({
- prefixId: pagePrefix,
- });
-
- this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
- }
- },
- resetIncomingEmailToken: function() {
- $('.incoming-email-token-reset').on('click', function(e) {
- e.preventDefault();
- $.ajax({
- type: 'PUT',
- url: $('.incoming-email-token-reset').attr('href'),
- dataType: 'json',
- success: function(response) {
- $('#issue_email').val(response.new_issue_address).focus();
- },
- beforeSend: function() {
- $('.incoming-email-token-reset').text('resetting...');
- },
- complete: function() {
- $('.incoming-email-token-reset').text('reset it');
- }
- });
- });
+ this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
}
- };
-})(window);
+ }
+
+ static resetIncomingEmailToken() {
+ $('.incoming-email-token-reset').on('click', (e) => {
+ e.preventDefault();
+
+ $.ajax({
+ type: 'PUT',
+ url: $('.incoming-email-token-reset').attr('href'),
+ dataType: 'json',
+ success(response) {
+ $('#issue_email').val(response.new_issue_address).focus();
+ },
+ beforeSend() {
+ $('.incoming-email-token-reset').text('resetting...');
+ },
+ complete() {
+ $('.incoming-email-token-reset').text('reset it');
+ },
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index c0bd64814ca..7de07e9403d 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,14 +1,12 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
-/* global Flash */
-
import 'vendor/jquery.waitforimages';
-import '~/lib/utils/text_utility';
-import './flash';
+import { addDelimiter } from './lib/utils/text_utility';
+import Flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
-class Issue {
+export default class Issue {
constructor() {
if ($('a.btn-close').length) {
this.taskList = new TaskList({
@@ -75,7 +73,7 @@ class Issue {
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
- projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
+ projectIssuesCounter.text(addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) {
if (isClosed) {
@@ -149,5 +147,3 @@ class Issue {
});
}
}
-
-export default Issue;
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index 06f6ec241f4..5bdc7c99503 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -1,5 +1,4 @@
<script>
-/* global Flash */
import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
@@ -17,6 +16,10 @@ export default {
required: true,
type: String,
},
+ updateEndpoint: {
+ required: true,
+ type: String,
+ },
canUpdate: {
required: true,
type: Boolean,
@@ -25,6 +28,21 @@ export default {
required: true,
type: Boolean,
},
+ showInlineEditButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showDeleteButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ enableAutocomplete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
issuableRef: {
type: String,
required: true,
@@ -88,6 +106,16 @@ export default {
type: String,
required: true,
},
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
const store = new Store({
@@ -153,21 +181,21 @@ export default {
})
.catch(() => {
eventHub.$emit('close.form');
- return new Flash('Error updating issue');
+ window.Flash(`Error updating ${this.issuableType}`);
});
},
deleteIssuable() {
this.service.deleteIssuable()
.then(res => res.json())
.then((data) => {
- // Stop the poll so we don't get 404's with the issue not existing
+ // Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop();
gl.utils.visitUrl(data.web_url);
})
.catch(() => {
eventHub.$emit('close.form');
- return new Flash('Error deleting issue');
+ window.Flash(`Error deleting ${this.issuableType}`);
});
},
},
@@ -219,24 +247,34 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
+ :show-delete-button="showDeleteButton"
+ :can-attach-file="canAttachFile"
+ :enable-autocomplete="enableAutocomplete"
/>
<div v-else>
<title-component
:issuable-ref="issuableRef"
+ :can-update="canUpdate"
:title-html="state.titleHtml"
- :title-text="state.titleText" />
+ :title-text="state.titleText"
+ :show-inline-edit-button="showInlineEditButton"
+ />
<description-component
v-if="state.descriptionHtml"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
:updated-at="state.updatedAt"
- :task-status="state.taskStatus" />
+ :task-status="state.taskStatus"
+ :issuable-type="issuableType"
+ :update-url="updateEndpoint"
+ />
<edited-component
v-if="hasUpdated"
:updated-at="state.updatedAt"
:updated-by-name="state.updatedByName"
- :updated-by-path="state.updatedByPath" />
+ :updated-by-path="state.updatedByPath"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index 48bad8f1e68..b7559ced946 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -22,6 +22,16 @@
required: false,
default: '',
},
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ updateUrl: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -48,7 +58,7 @@
if (this.canUpdate) {
// eslint-disable-next-line no-new
new TaskList({
- dataType: 'issue',
+ dataType: this.issuableType,
fieldName: 'description',
selector: '.detail-page-description',
});
@@ -95,7 +105,9 @@
<textarea
class="hidden js-task-list-field"
v-if="descriptionText"
- v-model="descriptionText">
+ v-model="descriptionText"
+ :data-update-url="updateUrl"
+ >
</textarea>
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index 8c81575fe6f..a539506bce2 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -13,6 +13,11 @@
type: Object,
required: true,
},
+ showDeleteButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -23,6 +28,9 @@
isSubmitEnabled() {
return this.formState.title.trim() !== '';
},
+ shouldShowDeleteButton() {
+ return this.canDestroy && this.showDeleteButton;
+ },
},
methods: {
closeForm() {
@@ -62,7 +70,7 @@
Cancel
</button>
<button
- v-if="canDestroy"
+ v-if="shouldShowDeleteButton"
class="btn btn-danger pull-right append-right-default"
:class="{ disabled: deleteLoading }"
type="button"
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index dc902eefc5f..52fe4ecd08b 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -1,5 +1,4 @@
<script>
- /* global Flash */
import updateMixin from '../../mixins/update';
import markdownField from '../../../vue_shared/components/markdown/field.vue';
@@ -18,6 +17,16 @@
type: String,
required: true,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ enableAutocomplete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
components: {
markdownField,
@@ -37,7 +46,10 @@
</label>
<markdown-field
:markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath">
+ :markdown-docs-path="markdownDocsPath"
+ :can-attach-file="canAttachFile"
+ :enable-autocomplete="enableAutocomplete"
+ >
<textarea
id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area"
diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue
index 83af8e1e245..c3abb9fd9d5 100644
--- a/app/assets/javascripts/issue_show/components/fields/title.vue
+++ b/app/assets/javascripts/issue_show/components/fields/title.vue
@@ -16,15 +16,15 @@
<fieldset>
<label
class="sr-only"
- for="issue-title">
+ for="issuable-title">
Title
</label>
<input
- id="issue-title"
+ id="issuable-title"
class="form-control"
type="text"
- placeholder="Issue title"
- aria-label="Issue title"
+ placeholder="Title"
+ aria-label="Title"
v-model="formState.title"
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable" />
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 28bf6c67ea5..0fa19022336 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -36,6 +36,21 @@
type: String,
required: true,
},
+ showDeleteButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ enableAutocomplete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
components: {
lockedWarning,
@@ -78,9 +93,13 @@
<description-field
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath" />
+ :markdown-docs-path="markdownDocsPath"
+ :can-attach-file="canAttachFile"
+ :enable-autocomplete="enableAutocomplete"
+ />
<edit-actions
:form-state="formState"
- :can-destroy="canDestroy" />
+ :can-destroy="canDestroy"
+ :show-delete-button="showDeleteButton" />
</form>
</template>
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index a9dabd4cff1..a363d06d950 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -1,5 +1,8 @@
<script>
import animateMixin from '../mixins/animate';
+ import eventHub from '../event_hub';
+ import tooltip from '../../vue_shared/directives/tooltip';
+ import { spriteIcon } from '../../lib/utils/common_utils';
export default {
mixins: [animateMixin],
@@ -15,6 +18,11 @@
type: String,
required: true,
},
+ canUpdate: {
+ required: false,
+ type: Boolean,
+ default: false,
+ },
titleHtml: {
type: String,
required: true,
@@ -23,6 +31,14 @@
type: String,
required: true,
},
+ showInlineEditButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ directives: {
+ tooltip,
},
watch: {
titleHtml() {
@@ -30,24 +46,46 @@
this.animateChange();
},
},
+ computed: {
+ pencilIcon() {
+ return spriteIcon('pencil', 'link-highlight');
+ },
+ },
methods: {
setPageTitle() {
const currentPageTitleScope = this.titleEl.innerText.split('·');
currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
this.titleEl.textContent = currentPageTitleScope.join('·');
},
+ edit() {
+ eventHub.$emit('open.form');
+ },
},
};
</script>
<template>
- <h2
- class="title"
- :class="{
- 'issue-realtime-pre-pulse': preAnimation,
- 'issue-realtime-trigger-pulse': pulseAnimation
- }"
- v-html="titleHtml"
- >
- </h2>
+ <div class="title-container">
+ <h2
+ class="title"
+ :class="{
+ 'issue-realtime-pre-pulse': preAnimation,
+ 'issue-realtime-trigger-pulse': pulseAnimation
+ }"
+ v-html="titleHtml"
+ >
+ </h2>
+ <button
+ v-tooltip
+ v-if="showInlineEditButton && canUpdate"
+ type="button"
+ class="btn btn-default btn-edit btn-svg"
+ v-html="pencilIcon"
+ title="Edit title and description"
+ data-placement="bottom"
+ data-container="body"
+ @click="edit"
+ >
+ </button>
+ </div>
</template>
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index 56cb536dcde..03546f61d1f 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,34 +1,23 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
-(function() {
- this.IssueStatusSelect = (function() {
- function IssueStatusSelect() {
- $('.js-issue-status').each(function(i, el) {
- var fieldName;
- fieldName = $(el).data("field-name");
- return $(el).glDropdown({
- selectable: true,
- fieldName: fieldName,
- toggleLabel: (function(_this) {
- return function(selected, el, instance) {
- var $item, label;
- label = 'Author';
- $item = instance.dropdown.find('.is-active');
- if ($item.length) {
- label = $item.text();
- }
- return label;
- };
- })(this),
- clicked: function(options) {
- return options.e.preventDefault();
- },
- id: function(obj, el) {
- return $(el).data("id");
- }
- });
- });
- }
-
- return IssueStatusSelect;
- })();
-}).call(window);
+export default function issueStatusSelect() {
+ $('.js-issue-status').each((i, el) => {
+ const fieldName = $(el).data('field-name');
+ return $(el).glDropdown({
+ selectable: true,
+ fieldName,
+ toggleLabel(selected, element, instance) {
+ let label = 'Author';
+ const $item = instance.dropdown.find('.is-active');
+ if ($item.length) {
+ label = $item.text();
+ }
+ return label;
+ },
+ clicked(options) {
+ return options.e.preventDefault();
+ },
+ id(obj, element) {
+ return $(element).data('id');
+ },
+ });
+ });
+}
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
new file mode 100644
index 00000000000..cf8fda9a4fa
--- /dev/null
+++ b/app/assets/javascripts/job.js
@@ -0,0 +1,283 @@
+import _ from 'underscore';
+import bp from './breakpoints';
+import { bytesToKiB } from './lib/utils/number_utils';
+import { setCiStatusFavicon } from './lib/utils/common_utils';
+
+export default class Job {
+ constructor(options) {
+ this.timeout = null;
+ this.state = null;
+ this.options = options || $('.js-build-options').data();
+
+ this.pageUrl = this.options.pageUrl;
+ this.buildStatus = this.options.buildStatus;
+ this.state = this.options.logState;
+ this.buildStage = this.options.buildStage;
+ this.$document = $(document);
+ this.$window = $(window);
+ this.logBytes = 0;
+ this.updateDropdown = this.updateDropdown.bind(this);
+
+ this.$buildTrace = $('#build-trace');
+ this.$buildRefreshAnimation = $('.js-build-refresh');
+ this.$truncatedInfo = $('.js-truncated-info');
+ this.$buildTraceOutput = $('.js-build-output');
+ this.$topBar = $('.js-top-bar');
+
+ // Scroll controllers
+ this.$scrollTopBtn = $('.js-scroll-up');
+ this.$scrollBottomBtn = $('.js-scroll-down');
+
+ clearTimeout(this.timeout);
+
+ this.initSidebar();
+ this.populateJobs(this.buildStage);
+ this.updateStageDropdownText(this.buildStage);
+ this.sidebarOnResize();
+
+ this.$document
+ .off('click', '.js-sidebar-build-toggle')
+ .on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
+
+ this.$document
+ .off('click', '.stage-item')
+ .on('click', '.stage-item', this.updateDropdown);
+
+ // add event listeners to the scroll buttons
+ this.$scrollTopBtn
+ .off('click')
+ .on('click', this.scrollToTop.bind(this));
+
+ this.$scrollBottomBtn
+ .off('click')
+ .on('click', this.scrollToBottom.bind(this));
+
+ this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
+
+ this.$window
+ .off('scroll')
+ .on('scroll', () => {
+ if (!this.isScrolledToBottom()) {
+ this.toggleScrollAnimation(false);
+ } else if (this.isScrolledToBottom() && !this.isLogComplete) {
+ this.toggleScrollAnimation(true);
+ }
+ this.scrollThrottled();
+ });
+
+ this.$window
+ .off('resize.build')
+ .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
+
+ this.updateArtifactRemoveDate();
+ this.initAffixTopArea();
+
+ this.getBuildTrace();
+ }
+
+ initAffixTopArea() {
+ /**
+ If the browser does not support position sticky, it returns the position as static.
+ If the browser does support sticky, then we allow the browser to handle it, if not
+ then we default back to Bootstraps affix
+ **/
+ if (this.$topBar.css('position') !== 'static') return;
+
+ const offsetTop = this.$buildTrace.offset().top;
+
+ this.$topBar.affix({
+ offset: {
+ top: offsetTop,
+ },
+ });
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ canScroll() {
+ return this.$document.height() > this.$window.height();
+ }
+
+ toggleScroll() {
+ const currentPosition = this.$document.scrollTop();
+ const scrollHeight = this.$document.height();
+
+ const windowHeight = this.$window.height();
+ if (this.canScroll()) {
+ if (currentPosition > 0 &&
+ (scrollHeight - currentPosition !== windowHeight)) {
+ // User is in the middle of the log
+
+ this.toggleDisableButton(this.$scrollTopBtn, false);
+ this.toggleDisableButton(this.$scrollBottomBtn, false);
+ } else if (currentPosition === 0) {
+ // User is at Top of Log
+
+ this.toggleDisableButton(this.$scrollTopBtn, true);
+ this.toggleDisableButton(this.$scrollBottomBtn, false);
+ } else if (this.isScrolledToBottom()) {
+ // User is at the bottom of the build log.
+
+ this.toggleDisableButton(this.$scrollTopBtn, false);
+ this.toggleDisableButton(this.$scrollBottomBtn, true);
+ }
+ } else {
+ this.toggleDisableButton(this.$scrollTopBtn, true);
+ this.toggleDisableButton(this.$scrollBottomBtn, true);
+ }
+ }
+
+ isScrolledToBottom() {
+ const currentPosition = this.$document.scrollTop();
+ const scrollHeight = this.$document.height();
+
+ const windowHeight = this.$window.height();
+ return scrollHeight - currentPosition === windowHeight;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ scrollDown() {
+ this.$document.scrollTop(this.$document.height());
+ }
+
+ scrollToBottom() {
+ this.scrollDown();
+ this.hasBeenScrolled = true;
+ this.toggleScroll();
+ }
+
+ scrollToTop() {
+ this.$document.scrollTop(0);
+ this.hasBeenScrolled = true;
+ this.toggleScroll();
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ toggleDisableButton($button, disable) {
+ if (disable && $button.prop('disabled')) return;
+ $button.prop('disabled', disable);
+ }
+
+ toggleScrollAnimation(toggle) {
+ this.$scrollBottomBtn.toggleClass('animate', toggle);
+ }
+
+ initSidebar() {
+ this.$sidebar = $('.js-build-sidebar');
+ }
+
+ getBuildTrace() {
+ return $.ajax({
+ url: `${this.pageUrl}/trace.json`,
+ data: { state: this.state },
+ })
+ .done((log) => {
+ setCiStatusFavicon(`${this.pageUrl}/status.json`);
+
+ if (log.state) {
+ this.state = log.state;
+ }
+
+ this.isScrollInBottom = this.isScrolledToBottom();
+
+ if (log.append) {
+ this.$buildTraceOutput.append(log.html);
+ this.logBytes += log.size;
+ } else {
+ this.$buildTraceOutput.html(log.html);
+ this.logBytes = log.size;
+ }
+
+ // if the incremental sum of logBytes we received is less than the total
+ // we need to show a message warning the user about that.
+ if (this.logBytes < log.total) {
+ // size is in bytes, we need to calculate KiB
+ const size = bytesToKiB(this.logBytes);
+ $('.js-truncated-info-size').html(`${size}`);
+ this.$truncatedInfo.removeClass('hidden');
+ } else {
+ this.$truncatedInfo.addClass('hidden');
+ }
+ this.isLogComplete = log.complete;
+
+ if (!log.complete) {
+ this.timeout = setTimeout(() => {
+ this.getBuildTrace();
+ }, 4000);
+ } else {
+ this.$buildRefreshAnimation.remove();
+ this.toggleScrollAnimation(false);
+ }
+
+ if (log.status !== this.buildStatus) {
+ gl.utils.visitUrl(this.pageUrl);
+ }
+ })
+ .fail(() => {
+ this.$buildRefreshAnimation.remove();
+ })
+ .then(() => {
+ if (this.isScrollInBottom) {
+ this.scrollDown();
+ }
+ })
+ .then(() => this.toggleScroll());
+ }
+ // eslint-disable-next-line class-methods-use-this
+ shouldHideSidebarForViewport() {
+ const bootstrapBreakpoint = bp.getBreakpointSize();
+ return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
+ }
+
+ toggleSidebar(shouldHide) {
+ const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
+ const $toggleButton = $('.js-sidebar-build-toggle-header');
+
+ this.$sidebar
+ .toggleClass('right-sidebar-expanded', shouldShow)
+ .toggleClass('right-sidebar-collapsed', shouldHide);
+
+ this.$topBar
+ .toggleClass('sidebar-expanded', shouldShow)
+ .toggleClass('sidebar-collapsed', shouldHide);
+
+ if (this.$sidebar.hasClass('right-sidebar-expanded')) {
+ $toggleButton.addClass('hidden');
+ } else {
+ $toggleButton.removeClass('hidden');
+ }
+ }
+
+ sidebarOnResize() {
+ this.toggleSidebar(this.shouldHideSidebarForViewport());
+ }
+
+ sidebarOnClick() {
+ if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
+ }
+ // eslint-disable-next-line class-methods-use-this, consistent-return
+ updateArtifactRemoveDate() {
+ const $date = $('.js-artifacts-remove');
+ if ($date.length) {
+ const date = $date.text();
+ return $date.text(
+ gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '),
+ );
+ }
+ }
+ // eslint-disable-next-line class-methods-use-this
+ populateJobs(stage) {
+ $('.build-job').hide();
+ $(`.build-job[data-stage="${stage}"]`).show();
+ }
+ // eslint-disable-next-line class-methods-use-this
+ updateStageDropdownText(stage) {
+ $('.stage-selection').text(stage);
+ }
+
+ updateDropdown(e) {
+ e.preventDefault();
+ const stage = e.currentTarget.text;
+ this.updateStageDropdownText(stage);
+ this.populateJobs(stage);
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue
index 3f6f40d47ba..6d671845f8e 100644
--- a/app/assets/javascripts/jobs/components/header.vue
+++ b/app/assets/javascripts/jobs/components/header.vue
@@ -43,16 +43,6 @@
type: 'link',
});
}
-
- if (this.job.retry_path) {
- actions.push({
- label: 'Retry',
- path: this.job.retry_path,
- cssClass: 'js-retry-button btn btn-inverted-secondary visible-md-block visible-lg-block',
- type: 'ujs-link',
- });
- }
-
return actions;
},
},
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index f92e669414a..baaf5641200 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -1,5 +1,3 @@
-/* global Flash */
-
import Vue from 'vue';
import JobMediator from './job_details_mediator';
import jobHeader from './components/header.vue';
diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js
index cc014b815c4..5a216f8fae2 100644
--- a/app/assets/javascripts/jobs/job_details_mediator.js
+++ b/app/assets/javascripts/jobs/job_details_mediator.js
@@ -1,11 +1,12 @@
-/* global Flash */
/* global Build */
import Visibility from 'visibilityjs';
+import Flash from '../flash';
import Poll from '../lib/utils/poll';
import JobStore from './stores/job_store';
import JobService from './services/job_service';
-import '../build';
+import Job from '../job';
+import handleRevealVariables from '../build_variables';
export default class JobMediator {
constructor(options = {}) {
@@ -20,15 +21,16 @@ export default class JobMediator {
}
initBuildClass() {
- this.build = new Build();
+ this.build = new Job();
+ handleRevealVariables();
}
fetchJob() {
this.poll = new Poll({
resource: this.service,
method: 'getJob',
- successCallback: this.successCallback.bind(this),
- errorCallback: this.errorCallback.bind(this),
+ successCallback: response => this.successCallback(response),
+ errorCallback: () => this.errorCallback(),
});
if (!Visibility.hidden()) {
@@ -55,7 +57,7 @@ export default class JobMediator {
successCallback(response) {
this.state.isLoading = false;
- return response.json().then(data => this.store.storeJob(data));
+ return this.store.storeJob(response.data);
}
errorCallback() {
diff --git a/app/assets/javascripts/jobs/services/job_service.js b/app/assets/javascripts/jobs/services/job_service.js
index eaf1c6e500a..b746489c45c 100644
--- a/app/assets/javascripts/jobs/services/job_service.js
+++ b/app/assets/javascripts/jobs/services/job_service.js
@@ -1,14 +1,11 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '../../lib/utils/axios_utils';
export default class JobService {
constructor(endpoint) {
- this.job = Vue.resource(endpoint);
+ this.job = endpoint;
}
getJob() {
- return this.job.get();
+ return axios.get(this.job);
}
}
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index d8814802d9e..c929dc98c10 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -1,124 +1,121 @@
/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
-/* global Flash */
/* global Sortable */
-((global) => {
- class LabelManager {
- constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
- this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
- this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
- this.otherLabels = otherLabels || $('.js-other-labels');
- this.errorMessage = 'Unable to update label prioritization at this time';
- this.emptyState = document.querySelector('#js-priority-labels-empty-state');
- this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
- filter: '.empty-message',
- forceFallback: true,
- fallbackClass: 'is-dragging',
- dataIdAttr: 'data-id',
- onUpdate: this.onPrioritySortUpdate.bind(this),
- });
- this.bindEvents();
- }
+import Flash from './flash';
- bindEvents() {
- this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick);
- return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
- }
+export default class LabelManager {
+ constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
+ this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
+ this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
+ this.otherLabels = otherLabels || $('.js-other-labels');
+ this.errorMessage = 'Unable to update label prioritization at this time';
+ this.emptyState = document.querySelector('#js-priority-labels-empty-state');
+ this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
+ filter: '.empty-message',
+ forceFallback: true,
+ fallbackClass: 'is-dragging',
+ dataIdAttr: 'data-id',
+ onUpdate: this.onPrioritySortUpdate.bind(this),
+ });
+ this.bindEvents();
+ }
- onTogglePriorityClick(e) {
- e.preventDefault();
- const _this = e.data;
- const $btn = $(e.currentTarget);
- const $label = $(`#${$btn.data('domId')}`);
- const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
- const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
- $tooltip.tooltip('destroy');
- _this.toggleLabelPriority($label, action);
- _this.toggleEmptyState($label, $btn, action);
- }
+ bindEvents() {
+ this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick);
+ return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
+ }
- onButtonActionClick(e) {
- e.stopPropagation();
- $(e.currentTarget).tooltip('hide');
- }
+ onTogglePriorityClick(e) {
+ e.preventDefault();
+ const _this = e.data;
+ const $btn = $(e.currentTarget);
+ const $label = $(`#${$btn.data('domId')}`);
+ const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
+ const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
+ $tooltip.tooltip('destroy');
+ _this.toggleLabelPriority($label, action);
+ _this.toggleEmptyState($label, $btn, action);
+ }
- toggleEmptyState($label, $btn, action) {
- this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li'));
- }
+ onButtonActionClick(e) {
+ e.stopPropagation();
+ $(e.currentTarget).tooltip('hide');
+ }
- toggleLabelPriority($label, action, persistState) {
- if (persistState == null) {
- persistState = true;
- }
- let xhr;
- const _this = this;
- const url = $label.find('.js-toggle-priority').data('url');
- let $target = this.prioritizedLabels;
- let $from = this.otherLabels;
- if (action === 'remove') {
- $target = this.otherLabels;
- $from = this.prioritizedLabels;
- }
- $label.detach().appendTo($target);
- if ($from.find('li').length) {
- $from.find('.empty-message').removeClass('hidden');
- }
- if ($target.find('> li:not(.empty-message)').length) {
- $target.find('.empty-message').addClass('hidden');
- }
- // Return if we are not persisting state
- if (!persistState) {
- return;
- }
- if (action === 'remove') {
- xhr = $.ajax({
- url,
- type: 'DELETE'
- });
- // Restore empty message
- if (!$from.find('li').length) {
- $from.find('.empty-message').removeClass('hidden');
- }
- } else {
- xhr = this.savePrioritySort($label, action);
- }
- return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
- }
+ toggleEmptyState($label, $btn, action) {
+ this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li'));
+ }
- onPrioritySortUpdate() {
- const xhr = this.savePrioritySort();
- return xhr.fail(function() {
- return new Flash(this.errorMessage, 'alert');
- });
+ toggleLabelPriority($label, action, persistState) {
+ if (persistState == null) {
+ persistState = true;
}
-
- savePrioritySort() {
- return $.post({
- url: this.prioritizedLabels.data('url'),
- data: {
- label_ids: this.getSortedLabelsIds()
- }
+ let xhr;
+ const _this = this;
+ const url = $label.find('.js-toggle-priority').data('url');
+ let $target = this.prioritizedLabels;
+ let $from = this.otherLabels;
+ if (action === 'remove') {
+ $target = this.otherLabels;
+ $from = this.prioritizedLabels;
+ }
+ $label.detach().appendTo($target);
+ if ($from.find('li').length) {
+ $from.find('.empty-message').removeClass('hidden');
+ }
+ if ($target.find('> li:not(.empty-message)').length) {
+ $target.find('.empty-message').addClass('hidden');
+ }
+ // Return if we are not persisting state
+ if (!persistState) {
+ return;
+ }
+ if (action === 'remove') {
+ xhr = $.ajax({
+ url,
+ type: 'DELETE'
});
+ // Restore empty message
+ if (!$from.find('li').length) {
+ $from.find('.empty-message').removeClass('hidden');
+ }
+ } else {
+ xhr = this.savePrioritySort($label, action);
}
+ return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
+ }
- rollbackLabelPosition($label, originalAction) {
- const action = originalAction === 'remove' ? 'add' : 'remove';
- this.toggleLabelPriority($label, action, false);
+ onPrioritySortUpdate() {
+ const xhr = this.savePrioritySort();
+ return xhr.fail(function() {
return new Flash(this.errorMessage, 'alert');
- }
+ });
+ }
- getSortedLabelsIds() {
- const sortedIds = [];
- this.prioritizedLabels.find('> li').each(function() {
- const id = $(this).data('id');
+ savePrioritySort() {
+ return $.post({
+ url: this.prioritizedLabels.data('url'),
+ data: {
+ label_ids: this.getSortedLabelsIds()
+ }
+ });
+ }
- if (id) {
- sortedIds.push(id);
- }
- });
- return sortedIds;
- }
+ rollbackLabelPosition($label, originalAction) {
+ const action = originalAction === 'remove' ? 'add' : 'remove';
+ this.toggleLabelPriority($label, action, false);
+ return new Flash(this.errorMessage, 'alert');
}
- gl.LabelManager = LabelManager;
-})(window.gl || (window.gl = {}));
+ getSortedLabelsIds() {
+ const sortedIds = [];
+ this.prioritizedLabels.find('> li').each(function() {
+ const id = $(this).data('id');
+
+ if (id) {
+ sortedIds.push(id);
+ }
+ });
+ return sortedIds;
+ }
+}
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js
index 03dd61b4263..7aab13ed9c6 100644
--- a/app/assets/javascripts/labels.js
+++ b/app/assets/javascripts/labels.js
@@ -1,44 +1,35 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, max-len */
-(function() {
- this.Labels = (function() {
- function Labels() {
- this.setSuggestedColor = this.setSuggestedColor.bind(this);
- this.updateColorPreview = this.updateColorPreview.bind(this);
- var form;
- form = $('.label-form');
- this.cleanBinding();
- this.addBinding();
- this.updateColorPreview();
- }
+export default class Labels {
+ constructor() {
+ this.setSuggestedColor = this.setSuggestedColor.bind(this);
+ this.updateColorPreview = this.updateColorPreview.bind(this);
+ this.cleanBinding();
+ this.addBinding();
+ this.updateColorPreview();
+ }
- Labels.prototype.addBinding = function() {
- $(document).on('click', '.suggest-colors a', this.setSuggestedColor);
- return $(document).on('input', 'input#label_color', this.updateColorPreview);
- };
+ addBinding() {
+ $(document).on('click', '.suggest-colors a', this.setSuggestedColor);
+ return $(document).on('input', 'input#label_color', this.updateColorPreview);
+ }
+ // eslint-disable-next-line class-methods-use-this
+ cleanBinding() {
+ $(document).off('click', '.suggest-colors a');
+ return $(document).off('input', 'input#label_color');
+ }
+ // eslint-disable-next-line class-methods-use-this
+ updateColorPreview() {
+ const previewColor = $('input#label_color').val();
+ return $('div.label-color-preview').css('background-color', previewColor);
+ // Updates the the preview color with the hex-color input
+ }
- Labels.prototype.cleanBinding = function() {
- $(document).off('click', '.suggest-colors a');
- return $(document).off('input', 'input#label_color');
- };
-
- Labels.prototype.updateColorPreview = function() {
- var previewColor;
- previewColor = $('input#label_color').val();
- return $('div.label-color-preview').css('background-color', previewColor);
- // Updates the the preview color with the hex-color input
- };
-
- // Updates the preview color with a click on a suggested color
- Labels.prototype.setSuggestedColor = function(e) {
- var color;
- color = $(e.currentTarget).data('color');
- $('input#label_color').val(color);
- this.updateColorPreview();
- // Notify the form, that color has changed
- $('.label-form').trigger('keyup');
- return e.preventDefault();
- };
-
- return Labels;
- })();
-}).call(window);
+ // Updates the preview color with a click on a suggested color
+ setSuggestedColor(e) {
+ const color = $(e.currentTarget).data('color');
+ $('input#label_color').val(color);
+ this.updateColorPreview();
+ // Notify the form, that color has changed
+ $('.label-form').trigger('keyup');
+ return e.preventDefault();
+ }
+}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index d479f7ed682..f7a1c9f1e40 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -6,474 +6,475 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label';
-(function() {
- this.LabelsSelect = (function() {
- function LabelsSelect(els) {
- var _this, $els;
- _this = this;
+export default class LabelsSelect {
+ constructor(els, options = {}) {
+ var _this, $els;
+ _this = this;
- $els = $(els);
+ $els = $(els);
- if (!els) {
- $els = $('.js-label-select');
- }
+ if (!els) {
+ $els = $('.js-label-select');
+ }
- $els.each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
- $dropdown = $(dropdown);
- $dropdownContainer = $dropdown.closest('.labels-filter');
- $toggleText = $dropdown.find('.dropdown-toggle-text');
- namespacePath = $dropdown.data('namespace-path');
- projectPath = $dropdown.data('project-path');
- labelUrl = $dropdown.data('labels');
- issueUpdateURL = $dropdown.data('issueUpdate');
- selectedLabel = $dropdown.data('selected');
- if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
- selectedLabel = selectedLabel.split(',');
- }
- showNo = $dropdown.data('show-no');
- showAny = $dropdown.data('show-any');
- showMenuAbove = $dropdown.data('showMenuAbove');
- defaultLabel = $dropdown.data('default-label');
- abilityName = $dropdown.data('ability-name');
- $selectbox = $dropdown.closest('.selectbox');
- $block = $selectbox.closest('.block');
- $form = $dropdown.closest('form, .js-issuable-update');
- $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
- $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
- $value = $block.find('.value');
- $loading = $block.find('.block-loading').fadeOut();
- fieldName = $dropdown.data('field-name');
- useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
- propertyName = useId ? 'id' : 'title';
- initialSelected = $selectbox
- .find('input[name="' + $dropdown.data('field-name') + '"]')
- .map(function () {
- return this.value;
- }).get();
- if (issueUpdateURL != null) {
- issueURLSplit = issueUpdateURL.split('/');
- }
- if (issueUpdateURL) {
- labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
- labelNoneHTMLTemplate = '<span class="no-value">None</span>';
- }
+ $els.each(function(i, dropdown) {
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
+ $dropdown = $(dropdown);
+ $dropdownContainer = $dropdown.closest('.labels-filter');
+ $toggleText = $dropdown.find('.dropdown-toggle-text');
+ namespacePath = $dropdown.data('namespace-path');
+ projectPath = $dropdown.data('project-path');
+ labelUrl = $dropdown.data('labels');
+ issueUpdateURL = $dropdown.data('issueUpdate');
+ selectedLabel = $dropdown.data('selected');
+ if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
+ selectedLabel = selectedLabel.split(',');
+ }
+ showNo = $dropdown.data('show-no');
+ showAny = $dropdown.data('show-any');
+ showMenuAbove = $dropdown.data('showMenuAbove');
+ defaultLabel = $dropdown.data('default-label');
+ abilityName = $dropdown.data('ability-name');
+ $selectbox = $dropdown.closest('.selectbox');
+ $block = $selectbox.closest('.block');
+ $form = $dropdown.closest('form, .js-issuable-update');
+ $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
+ $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
+ $value = $block.find('.value');
+ $loading = $block.find('.block-loading').fadeOut();
+ fieldName = $dropdown.data('field-name');
+ useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
+ propertyName = useId ? 'id' : 'title';
+ initialSelected = $selectbox
+ .find('input[name="' + $dropdown.data('field-name') + '"]')
+ .map(function () {
+ return this.value;
+ }).get();
+ if (issueUpdateURL != null) {
+ issueURLSplit = issueUpdateURL.split('/');
+ }
+ if (issueUpdateURL) {
+ labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
+ labelNoneHTMLTemplate = '<span class="no-value">None</span>';
+ }
+ const handleClick = options.handleClick;
- $sidebarLabelTooltip.tooltip();
+ $sidebarLabelTooltip.tooltip();
- if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
- new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
- }
+ if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
+ new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
+ }
- saveLabelData = function() {
- var data, selected;
- selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
- return this.value;
- }).get();
+ saveLabelData = function() {
+ var data, selected;
+ selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
+ return this.value;
+ }).get();
- if (_.isEqual(initialSelected, selected)) return;
- initialSelected = selected;
+ if (_.isEqual(initialSelected, selected)) return;
+ initialSelected = selected;
- data = {};
- data[abilityName] = {};
- data[abilityName].label_ids = selected;
- if (!selected.length) {
- data[abilityName].label_ids = [''];
+ data = {};
+ data[abilityName] = {};
+ data[abilityName].label_ids = selected;
+ if (!selected.length) {
+ data[abilityName].label_ids = [''];
+ }
+ $loading.removeClass('hidden').fadeIn();
+ $dropdown.trigger('loading.gl.dropdown');
+ return $.ajax({
+ type: 'PUT',
+ url: issueUpdateURL,
+ dataType: 'JSON',
+ data: data
+ }).done(function(data) {
+ var labelCount, template, labelTooltipTitle, labelTitles;
+ $loading.fadeOut();
+ $dropdown.trigger('loaded.gl.dropdown');
+ $selectbox.hide();
+ data.issueURLSplit = issueURLSplit;
+ labelCount = 0;
+ if (data.labels.length) {
+ template = labelHTMLTemplate(data);
+ labelCount = data.labels.length;
}
- $loading.removeClass('hidden').fadeIn();
- $dropdown.trigger('loading.gl.dropdown');
- return $.ajax({
- type: 'PUT',
- url: issueUpdateURL,
- dataType: 'JSON',
- data: data
- }).done(function(data) {
- var labelCount, template, labelTooltipTitle, labelTitles;
- $loading.fadeOut();
- $dropdown.trigger('loaded.gl.dropdown');
- $selectbox.hide();
- data.issueURLSplit = issueURLSplit;
- labelCount = 0;
- if (data.labels.length) {
- template = labelHTMLTemplate(data);
- labelCount = data.labels.length;
- }
- else {
- template = labelNoneHTMLTemplate;
- }
- $value.removeAttr('style').html(template);
- $sidebarCollapsedValue.text(labelCount);
-
- if (data.labels.length) {
- labelTitles = data.labels.map(function(label) {
- return label.title;
- });
+ else {
+ template = labelNoneHTMLTemplate;
+ }
+ $value.removeAttr('style').html(template);
+ $sidebarCollapsedValue.text(labelCount);
- if (labelTitles.length > 5) {
- labelTitles = labelTitles.slice(0, 5);
- labelTitles.push('and ' + (data.labels.length - 5) + ' more');
- }
+ if (data.labels.length) {
+ labelTitles = data.labels.map(function(label) {
+ return label.title;
+ });
- labelTooltipTitle = labelTitles.join(', ');
- }
- else {
- labelTooltipTitle = '';
- $sidebarLabelTooltip.tooltip('destroy');
+ if (labelTitles.length > 5) {
+ labelTitles = labelTitles.slice(0, 5);
+ labelTitles.push('and ' + (data.labels.length - 5) + ' more');
}
- $sidebarLabelTooltip
- .attr('title', labelTooltipTitle)
- .tooltip('fixTitle');
+ labelTooltipTitle = labelTitles.join(', ');
+ }
+ else {
+ labelTooltipTitle = '';
+ $sidebarLabelTooltip.tooltip('destroy');
+ }
- $('.has-tooltip', $value).tooltip({
- container: 'body'
- });
+ $sidebarLabelTooltip
+ .attr('title', labelTooltipTitle)
+ .tooltip('fixTitle');
+
+ $('.has-tooltip', $value).tooltip({
+ container: 'body'
});
- };
- $dropdown.glDropdown({
- showMenuAbove: showMenuAbove,
- data: function(term, callback) {
- return $.ajax({
- url: labelUrl
- }).done(function(data) {
- data = _.chain(data).groupBy(function(label) {
- return label.title;
- }).map(function(label) {
- var color;
- color = _.map(label, function(dup) {
- return dup.color;
+ });
+ };
+ $dropdown.glDropdown({
+ showMenuAbove: showMenuAbove,
+ data: function(term, callback) {
+ return $.ajax({
+ url: labelUrl
+ }).done(function(data) {
+ data = _.chain(data).groupBy(function(label) {
+ return label.title;
+ }).map(function(label) {
+ var color;
+ color = _.map(label, function(dup) {
+ return dup.color;
+ });
+ return {
+ id: label[0].id,
+ title: label[0].title,
+ color: color,
+ duplicate: color.length > 1
+ };
+ }).value();
+ if ($dropdown.hasClass('js-extra-options')) {
+ var extraData = [];
+ if (showNo) {
+ extraData.unshift({
+ id: 0,
+ title: 'No Label'
});
- return {
- id: label[0].id,
- title: label[0].title,
- color: color,
- duplicate: color.length > 1
- };
- }).value();
- if ($dropdown.hasClass('js-extra-options')) {
- var extraData = [];
- if (showNo) {
- extraData.unshift({
- id: 0,
- title: 'No Label'
- });
- }
- if (showAny) {
- extraData.unshift({
- isAny: true,
- title: 'Any Label'
- });
- }
- if (extraData.length) {
- extraData.push('divider');
- data = extraData.concat(data);
- }
- }
-
- callback(data);
- if (showMenuAbove) {
- $dropdown.data('glDropdown').positionMenuAbove();
- }
- });
- },
- renderRow: function(label, instance) {
- var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
- $li = $('<li>');
- $a = $('<a href="#">');
- selectedClass = [];
- removesAll = label.id <= 0 || (label.id == null);
- if ($dropdown.hasClass('js-filter-bulk-update')) {
- indeterminate = $dropdown.data('indeterminate') || [];
- marked = $dropdown.data('marked') || [];
-
- if (indeterminate.indexOf(label.id) !== -1) {
- selectedClass.push('is-indeterminate');
}
-
- if (marked.indexOf(label.id) !== -1) {
- // Remove is-indeterminate class if the item will be marked as active
- i = selectedClass.indexOf('is-indeterminate');
- if (i !== -1) {
- selectedClass.splice(i, 1);
- }
- selectedClass.push('is-active');
+ if (showAny) {
+ extraData.unshift({
+ isAny: true,
+ title: 'Any Label'
+ });
}
- } else {
- if (this.id(label)) {
- dropdownName = $dropdown.data('fieldName');
- dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
-
- if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
- selectedClass.push('is-active');
- }
+ if (extraData.length) {
+ extraData.push('divider');
+ data = extraData.concat(data);
}
+ }
- if ($dropdown.hasClass('js-multiselect') && removesAll) {
- selectedClass.push('dropdown-clear-active');
- }
+ callback(data);
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
}
- if (label.duplicate) {
- color = gl.DropdownUtils.duplicateLabelColor(label.color);
+ });
+ },
+ renderRow: function(label, instance) {
+ var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
+ $li = $('<li>');
+ $a = $('<a href="#">');
+ selectedClass = [];
+ removesAll = label.id <= 0 || (label.id == null);
+ if ($dropdown.hasClass('js-filter-bulk-update')) {
+ indeterminate = $dropdown.data('indeterminate') || [];
+ marked = $dropdown.data('marked') || [];
+
+ if (indeterminate.indexOf(label.id) !== -1) {
+ selectedClass.push('is-indeterminate');
}
- else {
- if (label.color != null) {
- color = label.color[0];
+
+ if (marked.indexOf(label.id) !== -1) {
+ // Remove is-indeterminate class if the item will be marked as active
+ i = selectedClass.indexOf('is-indeterminate');
+ if (i !== -1) {
+ selectedClass.splice(i, 1);
}
+ selectedClass.push('is-active');
}
- if (color) {
- colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
- }
- else {
- colorEl = '';
- }
- // We need to identify which items are actually labels
- if (label.id) {
- selectedClass.push('label-item');
- $a.attr('data-label-id', label.id);
- }
- $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title);
- // Return generated html
- return $li.html($a).prop('outerHTML');
- },
- search: {
- fields: ['title']
- },
- selectable: true,
- filterable: true,
- selected: $dropdown.data('selected') || [],
- toggleLabel: function(selected, el) {
- var isSelected = el !== null ? el.hasClass('is-active') : false;
- var title = selected.title;
- var selectedLabels = this.selected;
-
- if (selected.id === 0) {
- this.selected = [];
- return 'No Label';
+ } else {
+ if (this.id(label)) {
+ dropdownName = $dropdown.data('fieldName');
+ dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
+
+ if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
+ selectedClass.push('is-active');
+ }
}
- else if (isSelected) {
- this.selected.push(title);
+
+ if ($dropdown.hasClass('js-multiselect') && removesAll) {
+ selectedClass.push('dropdown-clear-active');
}
- else {
- var index = this.selected.indexOf(title);
- this.selected.splice(index, 1);
+ }
+ if (label.duplicate) {
+ color = gl.DropdownUtils.duplicateLabelColor(label.color);
+ }
+ else {
+ if (label.color != null) {
+ color = label.color[0];
}
+ }
+ if (color) {
+ colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
+ }
+ else {
+ colorEl = '';
+ }
+ // We need to identify which items are actually labels
+ if (label.id) {
+ selectedClass.push('label-item');
+ $a.attr('data-label-id', label.id);
+ }
+ $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title);
+ // Return generated html
+ return $li.html($a).prop('outerHTML');
+ },
+ search: {
+ fields: ['title']
+ },
+ selectable: true,
+ filterable: true,
+ selected: $dropdown.data('selected') || [],
+ toggleLabel: function(selected, el) {
+ var isSelected = el !== null ? el.hasClass('is-active') : false;
+ var title = selected.title;
+ var selectedLabels = this.selected;
+
+ if (selected.id === 0) {
+ this.selected = [];
+ return 'No Label';
+ }
+ else if (isSelected) {
+ this.selected.push(title);
+ }
+ else {
+ var index = this.selected.indexOf(title);
+ this.selected.splice(index, 1);
+ }
+
+ if (selectedLabels.length === 1) {
+ return selectedLabels;
+ }
+ else if (selectedLabels.length) {
+ return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more";
+ }
+ else {
+ return defaultLabel;
+ }
+ },
+ fieldName: $dropdown.data('field-name'),
+ id: function(label) {
+ if (label.id <= 0) return label.title;
+
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return label.id;
+ }
- if (selectedLabels.length === 1) {
- return selectedLabels;
+ if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
+ return label.title;
+ }
+ else {
+ return label.id;
+ }
+ },
+ hidden: function() {
+ var isIssueIndex, isMRIndex, page, selectedLabels;
+ page = $('body').attr('data-page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = page === 'projects:merge_requests:index';
+ $selectbox.hide();
+ // display:block overrides the hide-collapse rule
+ $value.removeAttr('style');
+
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return;
+ }
+
+ if ($('html').hasClass('issue-boards-page')) {
+ return;
+ }
+ if ($dropdown.hasClass('js-multiselect')) {
+ if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
+ Issuable.filterResults($dropdown.closest('form'));
}
- else if (selectedLabels.length) {
- return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more";
+ else if ($dropdown.hasClass('js-filter-submit')) {
+ $dropdown.closest('form').submit();
}
else {
- return defaultLabel;
+ if (!$dropdown.hasClass('js-filter-bulk-update')) {
+ saveLabelData();
+ }
}
- },
- fieldName: $dropdown.data('field-name'),
- id: function(label) {
- if (label.id <= 0) return label.title;
+ }
+ },
+ multiSelect: $dropdown.hasClass('js-multiselect'),
+ vue: $dropdown.hasClass('js-issue-board-sidebar'),
+ clicked: function(options) {
+ const { $el, e, isMarking } = options;
+ const label = options.selectedObj;
+
+ var isIssueIndex, isMRIndex, page, boardsModel;
+ var fadeOutLoader = () => {
+ $loading.fadeOut();
+ };
- if ($dropdown.hasClass('js-issuable-form-dropdown')) {
- return label.id;
- }
+ page = $('body').attr('data-page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = page === 'projects:merge_requests:index';
- if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
- return label.title;
- }
- else {
- return label.id;
- }
- },
- hidden: function() {
- var isIssueIndex, isMRIndex, page, selectedLabels;
- page = $('body').data('page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
- $selectbox.hide();
- // display:block overrides the hide-collapse rule
- $value.removeAttr('style');
-
- if ($dropdown.hasClass('js-issuable-form-dropdown')) {
- return;
- }
+ if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
+ $dropdown.parent()
+ .find('.dropdown-clear-active')
+ .removeClass('is-active');
+ }
- if ($('html').hasClass('issue-boards-page')) {
- return;
- }
- if ($dropdown.hasClass('js-multiselect')) {
- if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
- Issuable.filterResults($dropdown.closest('form'));
- }
- else if ($dropdown.hasClass('js-filter-submit')) {
- $dropdown.closest('form').submit();
- }
- else {
- if (!$dropdown.hasClass('js-filter-bulk-update')) {
- saveLabelData();
- }
- }
- }
- },
- multiSelect: $dropdown.hasClass('js-multiselect'),
- vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function(options) {
- const { $el, e, isMarking } = options;
- const label = options.selectedObj;
-
- var isIssueIndex, isMRIndex, page, boardsModel;
- var fadeOutLoader = () => {
- $loading.fadeOut();
- };
-
- page = $('body').data('page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
-
- if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
- $dropdown.parent()
- .find('.dropdown-clear-active')
- .removeClass('is-active');
- }
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return;
+ }
- if ($dropdown.hasClass('js-issuable-form-dropdown')) {
- return;
- }
+ if ($dropdown.hasClass('js-filter-bulk-update')) {
+ _this.enableBulkLabelDropdown();
+ _this.setDropdownData($dropdown, isMarking, label.id);
+ return;
+ }
- if ($dropdown.hasClass('js-filter-bulk-update')) {
- _this.enableBulkLabelDropdown();
- _this.setDropdownData($dropdown, isMarking, label.id);
- return;
- }
+ if ($dropdown.closest('.add-issues-modal').length) {
+ boardsModel = gl.issueBoards.ModalStore.store.filter;
+ }
- if ($dropdown.closest('.add-issues-modal').length) {
- boardsModel = gl.issueBoards.ModalStore.store.filter;
+ if (boardsModel) {
+ if (label.isAny) {
+ boardsModel['label_name'] = [];
+ } else if ($el.hasClass('is-active')) {
+ boardsModel['label_name'].push(label.title);
}
- if (boardsModel) {
- if (label.isAny) {
- boardsModel['label_name'] = [];
- } else if ($el.hasClass('is-active')) {
- boardsModel['label_name'].push(label.title);
- }
-
- e.preventDefault();
- return;
+ e.preventDefault();
+ return;
+ }
+ else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ if (!$dropdown.hasClass('js-multiselect')) {
+ selectedLabel = label.title;
+ return Issuable.filterResults($dropdown.closest('form'));
}
- else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- if (!$dropdown.hasClass('js-multiselect')) {
- selectedLabel = label.title;
- return Issuable.filterResults($dropdown.closest('form'));
- }
+ }
+ else if ($dropdown.hasClass('js-filter-submit')) {
+ return $dropdown.closest('form').submit();
+ }
+ else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if ($el.hasClass('is-active')) {
+ gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({
+ id: label.id,
+ title: label.title,
+ color: label.color[0],
+ textColor: '#fff'
+ }));
}
- else if ($dropdown.hasClass('js-filter-submit')) {
- return $dropdown.closest('form').submit();
+ else {
+ var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
+ labels = labels.filter(function (selectedLabel) {
+ return selectedLabel.id !== label.id;
+ });
+ gl.issueBoards.BoardsStore.detail.issue.labels = labels;
}
- else if ($dropdown.hasClass('js-issue-board-sidebar')) {
- if ($el.hasClass('is-active')) {
- gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({
- id: label.id,
- title: label.title,
- color: label.color[0],
- textColor: '#fff'
- }));
- }
- else {
- var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
- labels = labels.filter(function (selectedLabel) {
- return selectedLabel.id !== label.id;
- });
- gl.issueBoards.BoardsStore.detail.issue.labels = labels;
- }
- $loading.fadeIn();
+ $loading.fadeIn();
+
+ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ .then(fadeOutLoader)
+ .catch(fadeOutLoader);
+ }
+ else if (handleClick) {
+ e.preventDefault();
+ handleClick(label);
+ }
+ else {
+ if ($dropdown.hasClass('js-multiselect')) {
- gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
- .then(fadeOutLoader)
- .catch(fadeOutLoader);
}
else {
- if ($dropdown.hasClass('js-multiselect')) {
-
- }
- else {
- return saveLabelData();
- }
+ return saveLabelData();
}
- },
- });
-
- // Set dropdown data
- _this.setOriginalDropdownData($dropdownContainer, $dropdown);
+ }
+ },
});
- this.bindEvents();
- }
- LabelsSelect.prototype.bindEvents = function() {
- return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue);
- };
-
- LabelsSelect.prototype.onSelectCheckboxIssue = function() {
- if ($('.selected_issue:checked').length) {
- return;
+ // Set dropdown data
+ _this.setOriginalDropdownData($dropdownContainer, $dropdown);
+ });
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue);
+ }
+ // eslint-disable-next-line class-methods-use-this
+ onSelectCheckboxIssue() {
+ if ($('.selected_issue:checked').length) {
+ return;
+ }
+ return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
+ }
+ // eslint-disable-next-line class-methods-use-this
+ enableBulkLabelDropdown() {
+ IssuableBulkUpdateActions.willUpdateLabels = true;
+ }
+ // eslint-disable-next-line class-methods-use-this
+ setDropdownData($dropdown, isMarking, value) {
+ var i, markedIds, unmarkedIds, indeterminateIds;
+
+ markedIds = $dropdown.data('marked') || [];
+ unmarkedIds = $dropdown.data('unmarked') || [];
+ indeterminateIds = $dropdown.data('indeterminate') || [];
+
+ if (isMarking) {
+ markedIds.push(value);
+
+ i = indeterminateIds.indexOf(value);
+ if (i > -1) {
+ indeterminateIds.splice(i, 1);
}
- return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
- };
- LabelsSelect.prototype.enableBulkLabelDropdown = function() {
- IssuableBulkUpdateActions.willUpdateLabels = true;
- };
-
- LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
- var i, markedIds, unmarkedIds, indeterminateIds;
-
- markedIds = $dropdown.data('marked') || [];
- unmarkedIds = $dropdown.data('unmarked') || [];
- indeterminateIds = $dropdown.data('indeterminate') || [];
-
- if (isMarking) {
- markedIds.push(value);
-
- i = indeterminateIds.indexOf(value);
- if (i > -1) {
- indeterminateIds.splice(i, 1);
- }
-
- i = unmarkedIds.indexOf(value);
- if (i > -1) {
- unmarkedIds.splice(i, 1);
- }
- } else {
- // If marked item (not common) is unmarked
- i = markedIds.indexOf(value);
- if (i > -1) {
- markedIds.splice(i, 1);
- }
-
- // If an indeterminate item is being unmarked
- if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
- unmarkedIds.push(value);
- }
-
- // If a marked item is being unmarked
- // (a marked item could also be a label that is present in all selection)
- if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) {
- unmarkedIds.push(value);
- }
+ i = unmarkedIds.indexOf(value);
+ if (i > -1) {
+ unmarkedIds.splice(i, 1);
+ }
+ } else {
+ // If marked item (not common) is unmarked
+ i = markedIds.indexOf(value);
+ if (i > -1) {
+ markedIds.splice(i, 1);
}
- $dropdown.data('marked', markedIds);
- $dropdown.data('unmarked', unmarkedIds);
- $dropdown.data('indeterminate', indeterminateIds);
- };
+ // If an indeterminate item is being unmarked
+ if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
- LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) {
- var labels = [];
- $container.find('[name="label_name[]"]').map(function() {
- return labels.push(this.value);
- });
- $dropdown.data('marked', labels);
- };
+ // If a marked item is being unmarked
+ // (a marked item could also be a label that is present in all selection)
+ if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) {
+ unmarkedIds.push(value);
+ }
+ }
- return LabelsSelect;
- })();
-}).call(window);
+ $dropdown.data('marked', markedIds);
+ $dropdown.data('unmarked', unmarkedIds);
+ $dropdown.data('indeterminate', indeterminateIds);
+ }
+ // eslint-disable-next-line class-methods-use-this
+ setOriginalDropdownData($container, $dropdown) {
+ const labels = [];
+ $container.find('[name="label_name[]"]').map(function() {
+ return labels.push(this.value);
+ });
+ $dropdown.data('marked', labels);
+ }
+}
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index d064a2c0024..a6f82b247e2 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */
import _ from 'underscore';
import Cookies from 'js-cookie';
-import NewNavSidebar from './new_sidebar';
+import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav';
(function() {
@@ -51,8 +51,8 @@ import initFlyOutNav from './fly_out_nav';
});
$(() => {
- const newNavSidebar = new NewNavSidebar();
- newNavSidebar.bindEvents();
+ const contextualSidebar = new ContextualSidebar();
+ contextualSidebar.bindEvents();
initFlyOutNav();
});
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index 3d64b121fa7..dbbf1637a47 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -1,5 +1,3 @@
-/* eslint-disable one-export, one-var, one-var-declaration-per-line */
-
import _ from 'underscore';
export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
@@ -21,7 +19,10 @@ export default class LazyLoader {
}
searchLazyImages() {
this.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
- this.checkElementsInView();
+
+ if (this.lazyImages.length) {
+ this.checkElementsInView();
+ }
}
startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
@@ -45,15 +46,13 @@ export default class LazyLoader {
checkElementsInView() {
const scrollTop = pageYOffset;
const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
- let imgBoundRect, imgTop, imgBound;
// Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => {
if (selectedImage.getAttribute('data-src')) {
- imgBoundRect = selectedImage.getBoundingClientRect();
-
- imgTop = scrollTop + imgBoundRect.top;
- imgBound = imgTop + imgBoundRect.height;
+ const imgBoundRect = selectedImage.getBoundingClientRect();
+ const imgTop = scrollTop + imgBoundRect.top;
+ const imgBound = imgTop + imgBoundRect.height;
if (scrollTop < imgBound && visHeight > imgTop) {
LazyLoader.loadImage(selectedImage);
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
new file mode 100644
index 00000000000..7aeeca3b283
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -0,0 +1,22 @@
+import axios from 'axios';
+import csrf from './csrf';
+
+axios.defaults.headers.common[csrf.headerKey] = csrf.token;
+
+// Maintain a global counter for active requests
+// see: spec/support/wait_for_requests.rb
+axios.interceptors.request.use((config) => {
+ window.activeVueResources = window.activeVueResources || 0;
+ window.activeVueResources += 1;
+
+ return config;
+});
+
+// Remove the global counter
+axios.interceptors.response.use((config) => {
+ window.activeVueResources -= 1;
+
+ return config;
+});
+
+export default axios;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 423a25fbdfa..33cc807912c 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,5 +1,5 @@
-export const getPagePath = (index = 0) => $('body').data('page').split(':')[index];
+export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
export const isInGroupsPage = () => getPagePath() === 'groups';
@@ -172,7 +172,6 @@ export const getSelectedFragment = () => {
return documentFragment;
};
-// TODO: Update this name, there is a gl.text.insertText function.
export const insertText = (target, text) => {
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
const selectionStart = target.selectionStart;
@@ -191,7 +190,7 @@ export const insertText = (target, text) => {
target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
// Trigger autosave
- $(target).trigger('input');
+ target.dispatchEvent(new Event('input'));
// Trigger autosize
const event = document.createEvent('Event');
@@ -271,43 +270,39 @@ export const parseIntPagination = paginationInformation => ({
});
/**
- * Updates the search parameter of a URL given the parameter and value provided.
+ * Given a string of query parameters creates an object.
*
- * If no search params are present we'll add it.
- * If param for page is already present, we'll update it
- * If there are params but not for the given one, we'll add it at the end.
- * Returns the new search parameters.
+ * @example
+ * `scope=all&page=2` -> { scope: 'all', page: '2'}
+ * `scope=all` -> { scope: 'all' }
+ * ``-> {}
+ * @param {String} query
+ * @returns {Object}
+ */
+export const parseQueryStringIntoObject = (query = '') => {
+ if (query === '') return {};
+
+ return query
+ .split('&')
+ .reduce((acc, element) => {
+ const val = element.split('=');
+ Object.assign(acc, {
+ [val[0]]: decodeURIComponent(val[1]),
+ });
+ return acc;
+ }, {});
+};
+
+export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
+
+/**
+ * Based on the current location and the string parameters provided
+ * creates a new entry in the history without reloading the page.
*
* @param {String} param
- * @param {Number|String|Undefined|Null} value
- * @return {String}
*/
-export const setParamInURL = (param, value) => {
- let search;
- const locationSearch = window.location.search;
-
- if (locationSearch.length) {
- const parameters = locationSearch.substring(1, locationSearch.length)
- .split('&')
- .reduce((acc, element) => {
- const val = element.split('=');
- // eslint-disable-next-line no-param-reassign
- acc[val[0]] = decodeURIComponent(val[1]);
- return acc;
- }, {});
-
- parameters[param] = value;
-
- const toString = Object.keys(parameters)
- .map(val => `${val}=${encodeURIComponent(parameters[val])}`)
- .join('&');
-
- search = `?${toString}`;
- } else {
- search = `?${param}=${value}`;
- }
-
- return search;
+export const historyPushState = (newUrl) => {
+ window.history.pushState({}, document.title, newUrl);
};
/**
@@ -403,7 +398,11 @@ export const setCiStatusFavicon = (pageUrl) => {
});
};
-export const spriteIcon = icon => `<svg><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
+export const spriteIcon = (icon, className = '') => {
+ const classAttribute = className.length > 0 ? `class="${className}"` : '';
+
+ return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
+};
export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
diff --git a/app/assets/javascripts/lib/utils/datefix.js b/app/assets/javascripts/lib/utils/datefix.js
index 990dc3f6d1a..e98c9068367 100644
--- a/app/assets/javascripts/lib/utils/datefix.js
+++ b/app/assets/javascripts/lib/utils/datefix.js
@@ -1,8 +1,29 @@
-const DateFix = {
- dashedFix(val) {
- const [y, m, d] = val.split('-');
- return new Date(y, m - 1, d);
- },
+
+export const pad = (val, len = 2) => (`0${val}`).slice(-len);
+
+/**
+ * Formats dates in Pickaday
+ * @param {String} dateString Date in yyyy-mm-dd format
+ * @return {Date} UTC format
+ */
+export const parsePikadayDate = (dateString) => {
+ const parts = dateString.split('-');
+ const year = parseInt(parts[0], 10);
+ const month = parseInt(parts[1] - 1, 10);
+ const day = parseInt(parts[2], 10);
+
+ return new Date(year, month, day);
};
-export default DateFix;
+/**
+ * Used `onSelect` method in pickaday
+ * @param {Date} date UTC format
+ * @return {String} Date formated in yyyy-mm-dd
+ */
+export const pikadayToString = (date) => {
+ const day = pad(date.getDate());
+ const month = pad(date.getMonth() + 1);
+ const year = date.getFullYear();
+
+ return `${year}-${month}-${day}`;
+};
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 29fc91733b3..426a81a976d 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -2,6 +2,7 @@
import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format';
+import { pluralize } from './text_utility';
import {
lang,
@@ -143,9 +144,23 @@ export function timeIntervalInWords(intervalInSeconds) {
let text = '';
if (minutes >= 1) {
- text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`;
+ text = `${minutes} ${pluralize('minute', minutes)} ${seconds} ${pluralize('second', seconds)}`;
} else {
- text = `${seconds} ${gl.text.pluralize('second', seconds)}`;
+ text = `${seconds} ${pluralize('second', seconds)}`;
}
return text;
}
+
+export function dateInWords(date, abbreviated = false) {
+ if (!date) return date;
+
+ const month = date.getMonth();
+ const year = date.getFullYear();
+
+ const monthNames = [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
+ const monthNamesAbbr = [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
+
+ const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month];
+
+ return `${monthName} ${date.getDate()}, ${year}`;
+}
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index 917a45eb06b..a02c79b787e 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -52,3 +52,31 @@ export function bytesToKiB(number) {
export function bytesToMiB(number) {
return number / (BYTES_IN_KIB * BYTES_IN_KIB);
}
+
+/**
+ * Utility function that calculates GiB of the given bytes.
+ * @param {Number} number
+ * @returns {Number}
+ */
+export function bytesToGiB(number) {
+ return number / (BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB);
+}
+
+/**
+ * Port of rails number_to_human_size
+ * Formats the bytes in number into a more understandable
+ * representation (e.g., giving it 1500 yields 1.5 KB).
+ *
+ * @param {Number} size
+ * @returns {String}
+ */
+export function numberToHumanSize(size) {
+ if (size < BYTES_IN_KIB) {
+ return `${size} bytes`;
+ } else if (size < BYTES_IN_KIB * BYTES_IN_KIB) {
+ return `${bytesToKiB(size).toFixed(2)} KiB`;
+ } else if (size < BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB) {
+ return `${bytesToMiB(size).toFixed(2)} MiB`;
+ }
+ return `${bytesToGiB(size).toFixed(2)} GiB`;
+}
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index 1485e900945..7fca80c2fdb 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -3,7 +3,9 @@ import { normalizeHeaders } from './common_utils';
/**
* Polling utility for handling realtime updates.
- * Service for vue resouce and method need to be provided as props
+ * Requirements: Promise based HTTP client
+ *
+ * Service for promise based http client and method need to be provided as props
*
* @example
* new Poll({
@@ -60,7 +62,6 @@ export default class Poll {
checkConditions(response) {
const headers = normalizeHeaders(response.headers);
const pollInterval = parseInt(headers[this.intervalHeader], 10);
-
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
this.timeoutID = setTimeout(() => {
this.makeRequest();
@@ -102,7 +103,12 @@ export default class Poll {
/**
* Restarts polling after it has been stoped
*/
- restart() {
+ restart(options) {
+ // update data
+ if (options && options.data) {
+ this.options.data = options.data;
+ }
+
this.canPoll = true;
this.makeRequest();
}
diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js
index 64db42701ce..098afcfa1b4 100644
--- a/app/assets/javascripts/lib/utils/sticky.js
+++ b/app/assets/javascripts/lib/utils/sticky.js
@@ -28,14 +28,10 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
}
};
-export default (el, insertPlaceholder = true) => {
+export default (el, stickyTop, insertPlaceholder = true) => {
if (!el) return;
- const computedStyle = window.getComputedStyle(el);
-
- if (!/sticky/.test(computedStyle.position)) return;
-
- const stickyTop = parseInt(computedStyle.top, 10);
+ if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return;
document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), {
passive: true,
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
new file mode 100644
index 00000000000..2dc9cf0cc29
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -0,0 +1,153 @@
+/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
+
+const textUtils = {};
+
+textUtils.selectedText = function(text, textarea) {
+ return text.substring(textarea.selectionStart, textarea.selectionEnd);
+};
+
+textUtils.lineBefore = function(text, textarea) {
+ var split;
+ split = text.substring(0, textarea.selectionStart).trim().split('\n');
+ return split[split.length - 1];
+};
+
+textUtils.lineAfter = function(text, textarea) {
+ return text.substring(textarea.selectionEnd).trim().split('\n')[0];
+};
+
+textUtils.blockTagText = function(text, textArea, blockTag, selected) {
+ var lineAfter, lineBefore;
+ lineBefore = this.lineBefore(text, textArea);
+ lineAfter = this.lineAfter(text, textArea);
+ if (lineBefore === blockTag && lineAfter === blockTag) {
+ // To remove the block tag we have to select the line before & after
+ if (blockTag != null) {
+ textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
+ textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
+ }
+ return selected;
+ } else {
+ return blockTag + "\n" + selected + "\n" + blockTag;
+ }
+};
+
+textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
+ var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
+ removedLastNewLine = false;
+ removedFirstNewLine = false;
+ currentLineEmpty = false;
+
+ // Remove the first newline
+ if (selected.indexOf('\n') === 0) {
+ removedFirstNewLine = true;
+ selected = selected.replace(/\n+/, '');
+ }
+
+ // Remove the last newline
+ if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
+ removedLastNewLine = true;
+ selected = selected.replace(/\n$/, '');
+ }
+
+ selectedSplit = selected.split('\n');
+
+ if (!wrap) {
+ lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
+
+ // Check whether the current line is empty or consists only of spaces(=handle as empty)
+ if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
+ currentLineEmpty = true;
+ }
+ }
+
+ startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
+
+ if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
+ if (blockTag != null && blockTag !== '') {
+ insertText = this.blockTagText(text, textArea, blockTag, selected);
+ } else {
+ insertText = selectedSplit.map(function(val) {
+ if (val.indexOf(tag) === 0) {
+ return "" + (val.replace(tag, ''));
+ } else {
+ return "" + tag + val;
+ }
+ }).join('\n');
+ }
+ } else {
+ insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
+ }
+
+ if (removedFirstNewLine) {
+ insertText = '\n' + insertText;
+ }
+
+ if (removedLastNewLine) {
+ insertText += '\n';
+ }
+
+ if (document.queryCommandSupported('insertText')) {
+ inserted = document.execCommand('insertText', false, insertText);
+ }
+ if (!inserted) {
+ try {
+ document.execCommand("ms-beginUndoUnit");
+ } catch (error) {}
+ textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
+ try {
+ document.execCommand("ms-endUndoUnit");
+ } catch (error) {}
+ }
+ return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
+};
+
+textUtils.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
+ var pos;
+ if (!textArea.setSelectionRange) {
+ return;
+ }
+ if (textArea.selectionStart === textArea.selectionEnd) {
+ if (wrapped) {
+ pos = textArea.selectionStart - tag.length;
+ } else {
+ pos = textArea.selectionStart;
+ }
+
+ if (removedLastNewLine) {
+ pos -= 1;
+ }
+
+ return textArea.setSelectionRange(pos, pos);
+ }
+};
+
+textUtils.updateText = function(textArea, tag, blockTag, wrap) {
+ var $textArea, selected, text;
+ $textArea = $(textArea);
+ textArea = $textArea.get(0);
+ text = $textArea.val();
+ selected = this.selectedText(text, textArea);
+ $textArea.focus();
+ return this.insertText(textArea, text, tag, blockTag, selected, wrap);
+};
+
+textUtils.init = function(form) {
+ var self;
+ self = this;
+ return $('.js-md', form).off('click').on('click', function() {
+ var $this;
+ $this = $(this);
+ return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
+ });
+};
+
+textUtils.removeListeners = function(form) {
+ return $('.js-md', form).off('click');
+};
+
+textUtils.replaceRange = function(s, start, end, substitute) {
+ return s.substring(0, start) + substitute + s.substring(end);
+};
+
+export default textUtils;
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 021f936a4fa..9280b7f150c 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,189 +1,66 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
+/**
+ * Adds a , to a string composed by numbers, at every 3 chars.
+ *
+ * 2333 -> 2,333
+ * 232324 -> 232,324
+ *
+ * @param {String} text
+ * @returns {String}
+ */
+export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
-import 'vendor/latinise';
+/**
+ * Returns '99+' for numbers bigger than 99.
+ *
+ * @param {Number} count
+ * @return {Number|String}
+ */
+export const highCountTrim = count => (count > 99 ? '99+' : count);
-var base;
-var w = window;
-if (w.gl == null) {
- w.gl = {};
-}
-if ((base = w.gl).text == null) {
- base.text = {};
-}
-gl.text.addDelimiter = function(text) {
- return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
-};
-gl.text.highCountTrim = function(count) {
- return count > 99 ? '99+' : count;
-};
-gl.text.randomString = function() {
- return Math.random().toString(36).substring(7);
-};
-gl.text.replaceRange = function(s, start, end, substitute) {
- return s.substring(0, start) + substitute + s.substring(end);
-};
-gl.text.getTextWidth = function(text, font) {
- /**
- * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
- *
- * @param {String} text The text to be rendered.
- * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
- *
- * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
- */
- // re-use canvas object for better performance
- var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
- var context = canvas.getContext('2d');
- context.font = font;
- return context.measureText(text).width;
-};
-gl.text.selectedText = function(text, textarea) {
- return text.substring(textarea.selectionStart, textarea.selectionEnd);
-};
-gl.text.lineBefore = function(text, textarea) {
- var split;
- split = text.substring(0, textarea.selectionStart).trim().split('\n');
- return split[split.length - 1];
-};
-gl.text.lineAfter = function(text, textarea) {
- return text.substring(textarea.selectionEnd).trim().split('\n')[0];
-};
-gl.text.blockTagText = function(text, textArea, blockTag, selected) {
- var lineAfter, lineBefore;
- lineBefore = this.lineBefore(text, textArea);
- lineAfter = this.lineAfter(text, textArea);
- if (lineBefore === blockTag && lineAfter === blockTag) {
- // To remove the block tag we have to select the line before & after
- if (blockTag != null) {
- textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
- textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
- }
- return selected;
- } else {
- return blockTag + "\n" + selected + "\n" + blockTag;
- }
-};
-gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
- var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
- removedLastNewLine = false;
- removedFirstNewLine = false;
- currentLineEmpty = false;
-
- // Remove the first newline
- if (selected.indexOf('\n') === 0) {
- removedFirstNewLine = true;
- selected = selected.replace(/\n+/, '');
- }
-
- // Remove the last newline
- if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
- removedLastNewLine = true;
- selected = selected.replace(/\n$/, '');
- }
-
- selectedSplit = selected.split('\n');
+/**
+ * Converts first char to uppercase and replaces undercores with spaces
+ * @param {String} string
+ * @requires {String}
+ */
+export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
- if (!wrap) {
- lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
+/**
+ * Adds an 's' to the end of the string when count is bigger than 0
+ * @param {String} str
+ * @param {Number} count
+ * @returns {String}
+ */
+export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' : '');
- // Check whether the current line is empty or consists only of spaces(=handle as empty)
- if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
- currentLineEmpty = true;
- }
- }
+/**
+ * Replaces underscores with dashes
+ * @param {*} str
+ * @returns {String}
+ */
+export const dasherize = str => str.replace(/[_\s]+/g, '-');
- startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
+/**
+ * Removes accents and converts to lower case
+ * @param {String} str
+ * @returns {String}
+ */
+export const slugify = str => str.trim().toLowerCase();
- if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
- if (blockTag != null && blockTag !== '') {
- insertText = this.blockTagText(text, textArea, blockTag, selected);
- } else {
- insertText = selectedSplit.map(function(val) {
- if (val.indexOf(tag) === 0) {
- return "" + (val.replace(tag, ''));
- } else {
- return "" + tag + val;
- }
- }).join('\n');
- }
- } else {
- insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
- }
+/**
+ * Truncates given text
+ *
+ * @param {String} string
+ * @param {Number} maxLength
+ * @returns {String}
+ */
+export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
- if (removedFirstNewLine) {
- insertText = '\n' + insertText;
- }
-
- if (removedLastNewLine) {
- insertText += '\n';
- }
-
- if (document.queryCommandSupported('insertText')) {
- inserted = document.execCommand('insertText', false, insertText);
- }
- if (!inserted) {
- try {
- document.execCommand("ms-beginUndoUnit");
- } catch (error) {}
- textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
- try {
- document.execCommand("ms-endUndoUnit");
- } catch (error) {}
- }
- return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
-};
-gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
- var pos;
- if (!textArea.setSelectionRange) {
- return;
- }
- if (textArea.selectionStart === textArea.selectionEnd) {
- if (wrapped) {
- pos = textArea.selectionStart - tag.length;
- } else {
- pos = textArea.selectionStart;
- }
-
- if (removedLastNewLine) {
- pos -= 1;
- }
-
- return textArea.setSelectionRange(pos, pos);
- }
-};
-gl.text.updateText = function(textArea, tag, blockTag, wrap) {
- var $textArea, selected, text;
- $textArea = $(textArea);
- textArea = $textArea.get(0);
- text = $textArea.val();
- selected = this.selectedText(text, textArea);
- $textArea.focus();
- return this.insertText(textArea, text, tag, blockTag, selected, wrap);
-};
-gl.text.init = function(form) {
- var self;
- self = this;
- return $('.js-md', form).off('click').on('click', function() {
- var $this;
- $this = $(this);
- return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
- });
-};
-gl.text.removeListeners = function(form) {
- return $('.js-md', form).off('click');
-};
-gl.text.humanize = function(string) {
- return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
-};
-gl.text.pluralize = function(str, count) {
- return str + (count > 1 || count === 0 ? 's' : '');
-};
-gl.text.truncate = function(string, maxLength) {
- return string.substr(0, (maxLength - 3)) + '...';
-};
-gl.text.dasherize = function(str) {
- return str.replace(/[_\s]+/g, '-');
-};
-gl.text.slugify = function(str) {
- return str.trim().toLowerCase().latinise();
-};
+/**
+ * Capitalizes first character
+ *
+ * @param {String} text
+ * @return {String}
+ */
+export function capitalizeFirstCharacter(text) {
+ return `${text[0].toUpperCase()}${text.slice(1)}`;
+}
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 78c7a094127..17236c91490 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -85,7 +85,7 @@ w.gl.utils.getLocationHash = function(url) {
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
};
-w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
+w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(window.location.href);
// eslint-disable-next-line import/prefer-default-export
export function visitUrl(url, external = false) {
@@ -96,10 +96,14 @@ export function visitUrl(url, external = false) {
otherWindow.opener = null;
otherWindow.location = url;
} else {
- document.location.href = url;
+ window.location.href = url;
}
}
+export function redirectTo(url) {
+ return window.location.assign(url);
+}
+
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index 729baa2e1a7..3688a57937e 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,7 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */
-
-(function() {
- window.addEventListener('beforeunload', function() {
+export default function initLogoAnimation() {
+ window.addEventListener('beforeunload', () => {
$('.tanuki-logo').addClass('animate');
});
-}).call(window);
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 853546b617b..dcc0fa63b63 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,5 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */
-/* global Flash */
/* global ConfirmDangerModal */
/* global Aside */
@@ -13,7 +12,6 @@ import svg4everybody from 'svg4everybody';
// libraries with import side-effects
import 'mousetrap';
import 'mousetrap/plugins/pause/mousetrap-pause';
-import 'vendor/fuzzaldrin-plus';
// expose common libraries as globals (TODO: remove these)
window.jQuery = jQuery;
@@ -22,15 +20,6 @@ window._ = _;
window.Dropzone = Dropzone;
window.Sortable = Sortable;
-// shortcuts
-import './shortcuts';
-import './shortcuts_blob';
-import './shortcuts_dashboard_navigation';
-import './shortcuts_navigation';
-import './shortcuts_find_file';
-import './shortcuts_issuable';
-import './shortcuts_network';
-
// templates
import './templates/issuable_template_selector';
import './templates/issuable_template_selectors';
@@ -40,99 +29,49 @@ import './commit/image_file';
// lib/utils
import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility';
-import './lib/utils/pretty_time';
-import './lib/utils/text_utility';
import './lib/utils/url_utility';
// behaviors
import './behaviors/';
-// u2f
-import './u2f/authenticate';
-import './u2f/error';
-import './u2f/register';
-import './u2f/util';
-
// everything else
import './activities';
import './admin';
-import './api';
import './aside';
-import './autosave';
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
-import './broadcast_message';
-import './build';
-import './build_artifacts';
-import './build_variables';
-import './ci_lint_editor';
import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal';
-import './copy_as_gfm';
-import './copy_to_clipboard';
-import './diff';
-import './dropzone_input';
-import './due_date_select';
-import './files_comment_button';
-import './flash';
+import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
import './gl_form';
-import './group_avatar';
-import './group_label_subscription';
-import './groups_select';
-import './header';
-import './importer_status';
-import './issuable_index';
-import './issuable_context';
-import './issuable_form';
-import './issue';
-import './issue_status_select';
-import './label_manager';
-import './labels';
-import './labels_select';
+import initTodoToggle from './header';
+import initImporterStatus from './importer_status';
import './layout_nav';
import LazyLoader from './lazy_loader';
import './line_highlighter';
-import './logo';
-import './member_expiration_date';
-import './members';
+import initLogoAnimation from './logo';
import './merge_request';
import './merge_request_tabs';
-import './milestone';
import './milestone_select';
-import './namespace_select';
-import './new_branch_form';
-import './new_commit_form';
import './notes';
import './notifications_dropdown';
import './notifications_form';
import './pager';
import './preview_markdown';
-import './project';
-import './project_avatar';
import './project_find_file';
import './project_import';
-import './project_label_subscription';
-import './project_new';
-import './project_select';
-import './project_show';
-import './project_variables';
import './projects_dropdown';
import './projects_list';
import './syntax_highlight';
-import './render_math';
import './render_gfm';
import './right_sidebar';
import './search';
import './search_autocomplete';
-import './smart_interval';
-import './star';
-import './subscription';
-import './subscription_select';
import initBreadcrumbs from './breadcrumb';
import './dispatcher';
@@ -169,11 +108,13 @@ $(function () {
var $document = $(document);
var $window = $(window);
var $sidebarGutterToggle = $('.js-sidebar-toggle');
- var $flash = $('.flash-container');
var bootstrapBreakpoint = bp.getBreakpointSize();
var fitSidebarForSize;
initBreadcrumbs();
+ initImporterStatus();
+ initTodoToggle();
+ initLogoAnimation();
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
@@ -254,13 +195,6 @@ $(function () {
// Form submitter
});
gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
- // Flash
- if ($flash.length > 0) {
- $flash.click(function () {
- return $(this).fadeOut();
- });
- $flash.show();
- }
// Disable form buttons while a form is submitting
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
var buttons;
@@ -362,4 +296,12 @@ $(function () {
event.preventDefault();
gl.utils.visitUrl(`${action}${$(this).serialize()}`);
});
+
+ const flashContainer = document.querySelector('.flash-container');
+
+ if (flashContainer && flashContainer.children.length) {
+ flashContainer.querySelectorAll('.flash-alert, .flash-notice, .flash-success').forEach((flashEl) => {
+ removeFlashClickListener(flashEl);
+ });
+ }
});
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index cc9016e74da..84e70e35bad 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -1,55 +1,53 @@
-/* global dateFormat */
-
import Pikaday from 'pikaday';
-
-(() => {
- // Add datepickers to all `js-access-expiration-date` elements. If those elements are
- // children of an element with the `clearable-input` class, and have a sibling
- // `js-clear-input` element, then show that element when there is a value in the
- // datepicker, and make clicking on that element clear the field.
- //
- window.gl = window.gl || {};
- gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => {
- function toggleClearInput() {
- $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
- }
- const inputs = $(selector);
-
- inputs.each((i, el) => {
- const $input = $(el);
-
- const calendar = new Pikaday({
- field: $input.get(0),
- theme: 'gitlab-theme animate-picker',
- format: 'yyyy-mm-dd',
- minDate: new Date(),
- container: $input.parent().get(0),
- onSelect(dateText) {
- $input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
-
- $input.trigger('change');
-
- toggleClearInput.call($input);
- },
- });
-
- calendar.setDate(new Date($input.val()));
- $input.data('pikaday', calendar);
+import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
+
+// Add datepickers to all `js-access-expiration-date` elements. If those elements are
+// children of an element with the `clearable-input` class, and have a sibling
+// `js-clear-input` element, then show that element when there is a value in the
+// datepicker, and make clicking on that element clear the field.
+//
+export default function memberExpirationDate(selector = '.js-access-expiration-date') {
+ function toggleClearInput() {
+ $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
+ }
+ const inputs = $(selector);
+
+ inputs.each((i, el) => {
+ const $input = $(el);
+
+ const calendar = new Pikaday({
+ field: $input.get(0),
+ theme: 'gitlab-theme animate-picker',
+ format: 'yyyy-mm-dd',
+ minDate: new Date(),
+ container: $input.parent().get(0),
+ parse: dateString => parsePikadayDate(dateString),
+ toString: date => pikadayToString(date),
+ onSelect(dateText) {
+ $input.val(calendar.toString(dateText));
+
+ $input.trigger('change');
+
+ toggleClearInput.call($input);
+ },
});
- inputs.next('.js-clear-input').on('click', function clicked(event) {
- event.preventDefault();
+ calendar.setDate(parsePikadayDate($input.val()));
+ $input.data('pikaday', calendar);
+ });
- const input = $(this).closest('.clearable-input').find(selector);
- const calendar = input.data('pikaday');
+ inputs.next('.js-clear-input').on('click', function clicked(event) {
+ event.preventDefault();
- calendar.setDate(null);
- input.trigger('change');
- toggleClearInput.call(input);
- });
+ const input = $(this).closest('.clearable-input').find(selector);
+ const calendar = input.data('pikaday');
+
+ calendar.setDate(null);
+ input.trigger('change');
+ toggleClearInput.call(input);
+ });
- inputs.on('blur', toggleClearInput);
+ inputs.on('blur', toggleClearInput);
- inputs.each(toggleClearInput);
- };
-}).call(window);
+ inputs.each(toggleClearInput);
+}
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index 8291b8c4a70..52315e969d1 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -1,81 +1,62 @@
-/* eslint-disable class-methods-use-this */
-(() => {
- window.gl = window.gl || {};
-
- class Members {
- constructor() {
- this.addListeners();
- this.initGLDropdown();
- }
-
- addListeners() {
- $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
- $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
- $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
- gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
- }
+export default class Members {
+ constructor() {
+ this.addListeners();
+ this.initGLDropdown();
+ }
- initGLDropdown() {
- $('.js-member-permissions-dropdown').each((i, btn) => {
- const $btn = $(btn);
+ addListeners() {
+ $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
+ $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
+ gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
+ }
- $btn.glDropdown({
- selectable: true,
- isSelectable(selected, $el) {
- return !$el.hasClass('is-active');
- },
- fieldName: $btn.data('field-name'),
- id(selected, $el) {
- return $el.data('id');
- },
- toggleLabel(selected, $el) {
- return $el.text();
- },
- clicked: (options) => {
- this.formSubmit(null, options.$el);
- },
- });
+ initGLDropdown() {
+ $('.js-member-permissions-dropdown').each((i, btn) => {
+ const $btn = $(btn);
+
+ $btn.glDropdown({
+ selectable: true,
+ isSelectable(selected, $el) {
+ return !$el.hasClass('is-active');
+ },
+ fieldName: $btn.data('field-name'),
+ id(selected, $el) {
+ return $el.data('id');
+ },
+ toggleLabel(selected, $el) {
+ return $el.text();
+ },
+ clicked: (options) => {
+ this.formSubmit(null, options.$el);
+ },
});
- }
-
- removeRow(e) {
- const $target = $(e.target);
-
- if ($target.hasClass('btn-remove')) {
- $target.closest('.member')
- .fadeOut(function fadeOutMemberRow() {
- $(this).remove();
- });
- }
- }
-
- formSubmit(e, $el = null) {
- const $this = e ? $(e.currentTarget) : $el;
- const { $toggle, $dateInput } = this.getMemberListItems($this);
-
- $this.closest('form').trigger('submit.rails');
+ });
+ }
- $toggle.disable();
- $dateInput.disable();
- }
+ formSubmit(e, $el = null) {
+ const $this = e ? $(e.currentTarget) : $el;
+ const { $toggle, $dateInput } = this.getMemberListItems($this);
- formSuccess(e) {
- const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
+ $this.closest('form').trigger('submit.rails');
- $toggle.enable();
- $dateInput.enable();
- }
+ $toggle.disable();
+ $dateInput.disable();
+ }
- getMemberListItems($el) {
- const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
+ formSuccess(e) {
+ const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
- return {
- $memberListItem,
- $toggle: $memberListItem.find('.dropdown-menu-toggle'),
- $dateInput: $memberListItem.find('.js-access-expiration-date'),
- };
- }
+ $toggle.enable();
+ $dateInput.enable();
}
-
- gl.Members = Members;
-})();
+ // eslint-disable-next-line class-methods-use-this
+ getMemberListItems($el) {
+ const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
+
+ return {
+ $memberListItem,
+ $toggle: $memberListItem.find('.dropdown-menu-toggle'),
+ $dateInput: $memberListItem.find('.js-access-expiration-date'),
+ };
+ }
+}
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
index 645045fea88..93f8f6ee926 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
@@ -1,8 +1,8 @@
/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */
/* global ace */
-/* global Flash */
import Vue from 'vue';
+import Flash from '../../flash';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index d74cf5328ad..17591829b76 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -1,7 +1,7 @@
/* eslint-disable new-cap, comma-dangle, no-new */
-/* global Flash */
import Vue from 'vue';
+import Flash from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
import './merge_conflict_service';
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index af0658eb668..d30ff12bb59 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -5,6 +5,7 @@ import 'vendor/jquery.waitforimages';
import TaskList from './task_list';
import './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
+import { addDelimiter } from './lib/utils/text_utility';
(function() {
this.MergeRequest = (function() {
@@ -124,7 +125,7 @@ import IssuablesHelper from './helpers/issuables_helper';
const $el = $('.nav-links .js-merge-counter');
const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
- $el.text(gl.text.addDelimiter(count));
+ $el.text(addDelimiter(count));
};
MergeRequest.prototype.hideCloseButton = function() {
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index c042b22d1fd..54c1b7a268e 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,9 +1,8 @@
/* eslint-disable no-new, class-methods-use-this */
-/* global Flash */
/* global notes */
import Cookies from 'js-cookie';
-import './flash';
+import Flash from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
@@ -12,8 +11,8 @@ import {
handleLocationHash,
isMetaClick,
} from './lib/utils/common_utils';
-
import initDiscussionTab from './image_diff/init_discussion_tab';
+import Diff from './diff';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -68,6 +67,10 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) {
+ const mergeRequestTabs = document.querySelector('.js-tabs-affix');
+ const navbar = document.querySelector('.navbar-gitlab');
+ const paddingTop = 16;
+
this.diffsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false;
@@ -77,6 +80,11 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this);
this.showTab = this.showTab.bind(this);
+ this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
+
+ if (mergeRequestTabs) {
+ this.stickyTop += mergeRequestTabs.offsetHeight;
+ }
if (stubLocation) {
location = stubLocation;
@@ -279,7 +287,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
const $container = $('#diffs');
$container.html(data.html);
- initChangesDropdown();
+ initChangesDropdown(this.stickyTop);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
@@ -293,7 +301,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
}
this.diffsLoaded = true;
- new gl.Diff();
+ new Diff();
this.scrollToElement('#diffs');
$('.diff-file').each((i, el) => {
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 3e07ec4d0aa..f76a998bf8c 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,53 +1,49 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
-/* global Flash */
/* global Sortable */
-(function() {
- this.Milestone = (function() {
- function Milestone() {
- this.bindTabsSwitching();
+import Flash from './flash';
- // Load merge request tab if it is active
- // merge request tab is active based on different conditions in the backend
- this.loadTab($('.js-milestone-tabs .active a'));
+export default class Milestone {
+ constructor() {
+ this.bindTabsSwitching();
- this.loadInitialTab();
- }
+ // Load merge request tab if it is active
+ // merge request tab is active based on different conditions in the backend
+ this.loadTab($('.js-milestone-tabs .active a'));
+
+ this.loadInitialTab();
+ }
+
+ bindTabsSwitching() {
+ return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
+ const $target = $(e.target);
- Milestone.prototype.bindTabsSwitching = function() {
- return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
- const $target = $(e.target);
+ location.hash = $target.attr('href');
+ this.loadTab($target);
+ });
+ }
+ // eslint-disable-next-line class-methods-use-this
+ loadInitialTab() {
+ const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
- location.hash = $target.attr('href');
- this.loadTab($target);
+ if ($target.length) {
+ $target.tab('show');
+ }
+ }
+ // eslint-disable-next-line class-methods-use-this
+ loadTab($target) {
+ const endpoint = $target.data('endpoint');
+ const tabElId = $target.attr('href');
+
+ if (endpoint && !$target.hasClass('is-loaded')) {
+ $.ajax({
+ url: endpoint,
+ dataType: 'JSON',
+ })
+ .fail(() => new Flash('Error loading milestone tab'))
+ .done((data) => {
+ $(tabElId).html(data.html);
+ $target.addClass('is-loaded');
});
- };
-
- Milestone.prototype.loadInitialTab = function() {
- const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
-
- if ($target.length) {
- $target.tab('show');
- }
- };
-
- Milestone.prototype.loadTab = function($target) {
- const endpoint = $target.data('endpoint');
- const tabElId = $target.attr('href');
-
- if (endpoint && !$target.hasClass('is-loaded')) {
- $.ajax({
- url: endpoint,
- dataType: 'JSON',
- })
- .fail(() => new Flash('Error loading milestone tab'))
- .done((data) => {
- $(tabElId).html(data.html);
- $target.addClass('is-loaded');
- });
- }
- };
-
- return Milestone;
- })();
-}).call(window);
+ }
+ }
+}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 4675b1fcb8f..74e5a4f1cea 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -5,7 +5,7 @@ import _ from 'underscore';
(function() {
this.MilestoneSelect = (function() {
- function MilestoneSelect(currentProject, els) {
+ function MilestoneSelect(currentProject, els, options = {}) {
var _this, $els;
if (currentProject != null) {
_this = this;
@@ -136,18 +136,27 @@ import _ from 'underscore';
},
opened: function(e) {
const $el = $(e.currentTarget);
- if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function(options) {
- const { $el, e } = options;
- let selected = options.selectedObj;
+ clicked: function(clickEvent) {
+ const { $el, e } = clickEvent;
+ let selected = clickEvent.selectedObj;
+
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
- page = $('body').data('page');
+ if (!selected) return;
+
+ if (options.handleClick) {
+ e.preventDefault();
+ options.handleClick(selected);
+ return;
+ }
+
+ page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
isSelecting = (selected.name !== selectedMilestone);
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
index 64c1447f427..ca3d271663b 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -1,5 +1,5 @@
/* eslint-disable no-new */
-/* global Flash */
+import Flash from './flash';
/**
* In each pipelines table we have a mini pipeline graph for each pipeline.
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 442ed86d50c..cbe24c0915b 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,6 +1,6 @@
<script>
- /* global Flash */
import _ from 'underscore';
+ import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue';
import Graph from './graph.vue';
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 5aa3865f96a..f8782fde927 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -138,7 +138,7 @@
renderAxesPaths() {
this.timeSeries = createTimeSeries(
- this.graphData.queries[0],
+ this.graphData.queries,
this.graphWidth,
this.graphHeight,
this.graphHeightOffset,
@@ -153,8 +153,9 @@
const axisYScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]);
- axisXScale.domain(d3.extent(this.timeSeries[0].values, d => d.time));
- axisYScale.domain([0, d3.max(this.timeSeries[0].values.map(d => d.value))]);
+ const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
+ axisXScale.domain(d3.extent(allValues, d => d.time));
+ axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
const xAxis = d3.svg.axis()
.scale(axisXScale)
@@ -246,6 +247,7 @@
:key="index"
:generated-line-path="path.linePath"
:generated-area-path="path.areaPath"
+ :line-style="path.lineStyle"
:line-color="path.lineColor"
:area-color="path.areaColor"
/>
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index 85b6d7f4cbe..440b1b12631 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -79,7 +79,8 @@
},
formatMetricUsage(series) {
- const value = series.values[this.currentDataIndex].value;
+ const value = series.values[this.currentDataIndex] &&
+ series.values[this.currentDataIndex].value;
if (isNaN(value)) {
return '-';
}
@@ -92,6 +93,12 @@
}
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
},
+
+ strokeDashArray(type) {
+ if (type === 'dashed') return '6, 3';
+ if (type === 'dotted') return '3, 3';
+ return null;
+ },
},
mounted() {
this.$nextTick(() => {
@@ -162,13 +169,15 @@
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>
+ <line
+ :stroke="series.lineColor"
+ :stroke-width="measurements.legends.height"
+ :stroke-dasharray="strokeDashArray(series.lineStyle)"
+ :x1="measurements.legends.offsetX"
+ :x2="measurements.legends.offsetX + measurements.legends.width"
+ :y1="graphHeight - measurements.legends.offsetY"
+ :y2="graphHeight - measurements.legends.offsetY">
+ </line>
<text
v-if="timeSeries.length > 1"
class="legend-metric-title"
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index 043f1bf66bb..5e6d409033a 100644
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
@@ -9,6 +9,10 @@
type: String,
required: true,
},
+ lineStyle: {
+ type: String,
+ required: false,
+ },
lineColor: {
type: String,
required: true,
@@ -18,6 +22,13 @@
required: true,
},
},
+ computed: {
+ strokeDashArray() {
+ if (this.lineStyle === 'dashed') return '3, 1';
+ if (this.lineStyle === 'dotted') return '1, 1';
+ return null;
+ },
+ },
};
</script>
<template>
@@ -34,6 +45,7 @@
:stroke="lineColor"
fill="none"
stroke-width="1"
+ :stroke-dasharray="strokeDashArray"
transform="translate(-5, 20)">
</path>
</g>
diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js
index fed884d5c94..e230a06cd8c 100644
--- a/app/assets/javascripts/monitoring/services/monitoring_service.js
+++ b/app/assets/javascripts/monitoring/services/monitoring_service.js
@@ -1,10 +1,7 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
+import axios from '../../lib/utils/axios_utils';
import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils';
-Vue.use(VueResource);
-
const MAX_REQUESTS = 3;
function backOffRequest(makeRequestCallback) {
@@ -32,8 +29,8 @@ export default class MonitoringService {
}
getGraphsData() {
- return backOffRequest(() => Vue.http.get(this.metricsEndpoint))
- .then(resp => resp.json())
+ return backOffRequest(() => axios.get(this.metricsEndpoint))
+ .then(resp => resp.data)
.then((response) => {
if (!response || !response.data) {
throw new Error('Unexpected metrics data response from prometheus endpoint');
@@ -43,8 +40,8 @@ export default class MonitoringService {
}
getDeploymentData() {
- return backOffRequest(() => Vue.http.get(this.deploymentEndpoint))
- .then(resp => resp.json())
+ return backOffRequest(() => axios.get(this.deploymentEndpoint))
+ .then(resp => resp.data)
.then((response) => {
if (!response || !response.deployments) {
throw new Error('Unexpected deployment data response from prometheus endpoint');
diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js
index ee3c45efacc..ee866850e13 100644
--- a/app/assets/javascripts/monitoring/utils/measurements.js
+++ b/app/assets/javascripts/monitoring/utils/measurements.js
@@ -7,15 +7,16 @@ export default {
left: 40,
},
legends: {
- width: 10,
+ width: 15,
height: 3,
+ offsetX: 20,
+ offsetY: 32,
},
backgroundLegend: {
width: 30,
height: 50,
},
axisLabelLineOffset: -20,
- legendOffset: 33,
},
large: { // This covers both md and lg screen sizes
margin: {
@@ -27,13 +28,14 @@ export default {
legends: {
width: 15,
height: 3,
+ offsetX: 20,
+ offsetY: 34,
},
backgroundLegend: {
width: 30,
height: 150,
},
axisLabelLineOffset: 20,
- 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
index 65eec0d8d02..d21a265bd43 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -11,7 +11,9 @@ const defaultColorPalette = {
const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
-export default function createTimeSeries(queryData, graphWidth, graphHeight, graphHeightOffset) {
+const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
+
+function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
let usedColors = [];
function pickColor(name) {
@@ -31,17 +33,7 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
return defaultColorPalette[pick];
}
- const maxValues = queryData.result.map((timeSeries, index) => {
- const maxValue = d3.max(timeSeries.values.map(d => d.value));
- return {
- maxValue,
- index,
- };
- });
-
- const maxValueFromSeries = _.max(maxValues, val => val.maxValue);
-
- return queryData.result.map((timeSeries, timeSeriesNumber) => {
+ return query.result.map((timeSeries, timeSeriesNumber) => {
let metricTag = '';
let lineColor = '';
let areaColor = '';
@@ -52,9 +44,9 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
const timeSeriesScaleY = d3.scale.linear()
.range([graphHeight - graphHeightOffset, 0]);
- timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time));
+ timeSeriesScaleX.domain(xDom);
timeSeriesScaleX.ticks(d3.time.minute, 60);
- timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
+ timeSeriesScaleY.domain(yDom);
const defined = d => !isNaN(d.value) && d.value != null;
@@ -72,10 +64,10 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
.y1(d => timeSeriesScaleY(d.value));
const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
- const seriesCustomizationData = queryData.series != null &&
- _.findWhere(queryData.series[0].when,
- { value: timeSeriesMetricLabel });
- if (seriesCustomizationData != null) {
+ const seriesCustomizationData = query.series != null &&
+ _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
+
+ if (seriesCustomizationData) {
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
[lineColor, areaColor] = pickColor(seriesCustomizationData.color);
} else {
@@ -83,14 +75,35 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
[lineColor, areaColor] = pickColor();
}
+ if (query.track) {
+ metricTag += ` - ${query.track}`;
+ }
+
return {
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
values: timeSeries.values,
+ lineStyle,
lineColor,
areaColor,
metricTag,
};
});
}
+
+export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
+ const allValues = queries.reduce((allQueryResults, query) => allQueryResults.concat(
+ query.result.reduce((allResults, result) => allResults.concat(result.values), []),
+ ), []);
+
+ const xDom = d3.extent(allValues, d => d.time);
+ const yDom = [0, d3.max(allValues.map(d => d.value))];
+
+ return queries.reduce((series, query, index) => {
+ const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length];
+ return series.concat(
+ queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle),
+ );
+ }, []);
+}
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index 5da2db063a4..1d496c64e53 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,85 +1,57 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */
import Api from './api';
+import './lib/utils/url_utility';
-(function() {
- window.NamespaceSelect = (function() {
- function NamespaceSelect(opts) {
- this.onSelectItem = this.onSelectItem.bind(this);
- var fieldName, showAny;
- this.dropdown = opts.dropdown;
- showAny = true;
- fieldName = 'namespace_id';
- if (this.dropdown.attr('data-field-name')) {
- fieldName = this.dropdown.data('fieldName');
- }
- if (this.dropdown.attr('data-show-any')) {
- showAny = this.dropdown.data('showAny');
- }
- this.dropdown.glDropdown({
- filterable: true,
- selectable: true,
- filterRemote: true,
- search: {
- fields: ['path']
- },
- fieldName: fieldName,
- toggleLabel: function(selected) {
- if (selected.id == null) {
- return selected.text;
- } else {
- return selected.kind + ": " + selected.full_path;
- }
- },
- data: function(term, dataCallback) {
- return Api.namespaces(term, function(namespaces) {
- var anyNamespace;
- if (showAny) {
- anyNamespace = {
- text: 'Any namespace',
- id: null
- };
- namespaces.unshift(anyNamespace);
- namespaces.splice(1, 0, 'divider');
- }
- return dataCallback(namespaces);
- });
- },
- text: function(namespace) {
- if (namespace.id == null) {
- return namespace.text;
- } else {
- return namespace.kind + ": " + namespace.full_path;
- }
- },
- renderRow: this.renderRow,
- clicked: this.onSelectItem
- });
- }
-
- NamespaceSelect.prototype.onSelectItem = function(options) {
- const { e } = options;
- return e.preventDefault();
- };
+export default class NamespaceSelect {
+ constructor(opts) {
+ const isFilter = opts.dropdown.dataset.isFilter === 'true';
+ const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id';
- return NamespaceSelect;
- })();
-
- window.NamespaceSelects = (function() {
- function NamespaceSelects(opts) {
- var ref;
- if (opts == null) {
- opts = {};
- }
- this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-namespace-select');
- this.$dropdowns.each(function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return new window.NamespaceSelect({
- dropdown: $dropdown
+ $(opts.dropdown).glDropdown({
+ filterable: true,
+ selectable: true,
+ filterRemote: true,
+ search: {
+ fields: ['path']
+ },
+ fieldName: fieldName,
+ toggleLabel: function(selected) {
+ if (selected.id == null) {
+ return selected.text;
+ } else {
+ return selected.kind + ": " + selected.full_path;
+ }
+ },
+ data: function(term, dataCallback) {
+ return Api.namespaces(term, function(namespaces) {
+ if (isFilter) {
+ const anyNamespace = {
+ text: 'Any namespace',
+ id: null
+ };
+ namespaces.unshift(anyNamespace);
+ namespaces.splice(1, 0, 'divider');
+ }
+ return dataCallback(namespaces);
});
- });
- }
-
- return NamespaceSelects;
- })();
-}).call(window);
+ },
+ text: function(namespace) {
+ if (namespace.id == null) {
+ return namespace.text;
+ } else {
+ return namespace.kind + ": " + namespace.full_path;
+ }
+ },
+ renderRow: this.renderRow,
+ clicked(options) {
+ if (!isFilter) {
+ const { e } = options;
+ e.preventDefault();
+ }
+ },
+ url(namespace) {
+ return gl.utils.mergeUrlParams({ [fieldName]: namespace.id }, window.location.href);
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index 8aae2ad201c..129f1724cb8 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,6 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
-/* global ShortcutsNetwork */
+import ShortcutsNetwork from '../shortcuts_network';
import Network from './network';
$(function() {
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 39fb302b644..77733b67c4d 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,97 +1,93 @@
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */
-import RefSelectDropdown from '~/ref_select_dropdown';
+import RefSelectDropdown from './ref_select_dropdown';
-(function() {
- this.NewBranchForm = (function() {
- function NewBranchForm(form, availableRefs) {
- this.validate = this.validate.bind(this);
- this.branchNameError = form.find('.js-branch-name-error');
- this.name = form.find('.js-branch-name');
- this.ref = form.find('#ref');
- new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new
- this.setupRestrictions();
- this.addBinding();
- this.init();
+export default class NewBranchForm {
+ constructor(form, availableRefs) {
+ this.validate = this.validate.bind(this);
+ this.branchNameError = form.find('.js-branch-name-error');
+ this.name = form.find('.js-branch-name');
+ this.ref = form.find('#ref');
+ new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new
+ this.setupRestrictions();
+ this.addBinding();
+ this.init();
+ }
+
+ addBinding() {
+ return this.name.on('blur', this.validate);
+ }
+
+ init() {
+ if (this.name.length && this.name.val().length > 0) {
+ return this.name.trigger('blur');
}
+ }
- NewBranchForm.prototype.addBinding = function() {
- return this.name.on('blur', this.validate);
+ setupRestrictions() {
+ var endsWith, invalid, single, startsWith;
+ startsWith = {
+ pattern: /^(\/|\.)/g,
+ prefix: "can't start with",
+ conjunction: "or"
};
-
- NewBranchForm.prototype.init = function() {
- if (this.name.length && this.name.val().length > 0) {
- return this.name.trigger('blur');
- }
+ endsWith = {
+ pattern: /(\/|\.|\.lock)$/g,
+ prefix: "can't end in",
+ conjunction: "or"
};
-
- NewBranchForm.prototype.setupRestrictions = function() {
- var endsWith, invalid, single, startsWith;
- startsWith = {
- pattern: /^(\/|\.)/g,
- prefix: "can't start with",
- conjunction: "or"
- };
- endsWith = {
- pattern: /(\/|\.|\.lock)$/g,
- prefix: "can't end in",
- conjunction: "or"
- };
- invalid = {
- pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g,
- prefix: "can't contain",
- conjunction: ", "
- };
- single = {
- pattern: /^@+$/g,
- prefix: "can't be",
- conjunction: "or"
- };
- return this.restrictions = [startsWith, invalid, endsWith, single];
+ invalid = {
+ pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g,
+ prefix: "can't contain",
+ conjunction: ", "
+ };
+ single = {
+ pattern: /^@+$/g,
+ prefix: "can't be",
+ conjunction: "or"
};
+ return this.restrictions = [startsWith, invalid, endsWith, single];
+ }
- NewBranchForm.prototype.validate = function() {
- var errorMessage, errors, formatter, unique, validator;
- const indexOf = [].indexOf;
+ validate() {
+ var errorMessage, errors, formatter, unique, validator;
+ const indexOf = [].indexOf;
- this.branchNameError.empty();
- unique = function(values, value) {
- if (indexOf.call(values, value) === -1) {
- values.push(value);
- }
- return values;
- };
- formatter = function(values, restriction) {
- var formatted;
- formatted = values.map(function(value) {
- switch (false) {
- case !/\s/.test(value):
- return 'spaces';
- case !/\/{2,}/g.test(value):
- return 'consecutive slashes';
- default:
- return "'" + value + "'";
- }
- });
- return restriction.prefix + " " + (formatted.join(restriction.conjunction));
- };
- validator = (function(_this) {
- return function(errors, restriction) {
- var matched;
- matched = _this.name.val().match(restriction.pattern);
- if (matched) {
- return errors.concat(formatter(matched.reduce(unique, []), restriction));
- } else {
- return errors;
- }
- };
- })(this);
- errors = this.restrictions.reduce(validator, []);
- if (errors.length > 0) {
- errorMessage = $("<span/>").text(errors.join(', '));
- return this.branchNameError.append(errorMessage);
+ this.branchNameError.empty();
+ unique = function(values, value) {
+ if (indexOf.call(values, value) === -1) {
+ values.push(value);
}
+ return values;
};
-
- return NewBranchForm;
- })();
-}).call(window);
+ formatter = function(values, restriction) {
+ var formatted;
+ formatted = values.map(function(value) {
+ switch (false) {
+ case !/\s/.test(value):
+ return 'spaces';
+ case !/\/{2,}/g.test(value):
+ return 'consecutive slashes';
+ default:
+ return "'" + value + "'";
+ }
+ });
+ return restriction.prefix + " " + (formatted.join(restriction.conjunction));
+ };
+ validator = (function(_this) {
+ return function(errors, restriction) {
+ var matched;
+ matched = _this.name.val().match(restriction.pattern);
+ if (matched) {
+ return errors.concat(formatter(matched.reduce(unique, []), restriction));
+ } else {
+ return errors;
+ }
+ };
+ })(this);
+ errors = this.restrictions.reduce(validator, []);
+ if (errors.length > 0) {
+ errorMessage = $("<span/>").text(errors.join(', '));
+ return this.branchNameError.append(errorMessage);
+ }
+ }
+}
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 04073ef7270..6e152497d20 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,32 +1,28 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
-(function() {
- this.NewCommitForm = (function() {
- function NewCommitForm(form) {
- this.form = form;
- this.renderDestination = this.renderDestination.bind(this);
- this.branchName = form.find('.js-branch-name');
- this.originalBranch = form.find('.js-original-branch');
- this.createMergeRequest = form.find('.js-create-merge-request');
- this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
- this.branchName.keyup(this.renderDestination);
- this.renderDestination();
- }
+export default class NewCommitForm {
+ constructor(form) {
+ this.form = form;
+ this.renderDestination = this.renderDestination.bind(this);
+ this.branchName = form.find('.js-branch-name');
+ this.originalBranch = form.find('.js-original-branch');
+ this.createMergeRequest = form.find('.js-create-merge-request');
+ this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
+ this.branchName.keyup(this.renderDestination);
+ this.renderDestination();
+ }
- NewCommitForm.prototype.renderDestination = function() {
- var different;
- different = this.branchName.val() !== this.originalBranch.val();
- if (different) {
- this.createMergeRequestContainer.show();
- if (!this.wasDifferent) {
- this.createMergeRequest.prop('checked', true);
- }
- } else {
- this.createMergeRequestContainer.hide();
- this.createMergeRequest.prop('checked', false);
+ renderDestination() {
+ var different;
+ different = this.branchName.val() !== this.originalBranch.val();
+ if (different) {
+ this.createMergeRequestContainer.show();
+ if (!this.wasDifferent) {
+ this.createMergeRequest.prop('checked', true);
}
- return this.wasDifferent = different;
- };
-
- return NewCommitForm;
- })();
-}).call(window);
+ } else {
+ this.createMergeRequestContainer.hide();
+ this.createMergeRequest.prop('checked', false);
+ }
+ return this.wasDifferent = different;
+ }
+}
diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js
deleted file mode 100644
index 997550b37fb..00000000000
--- a/app/assets/javascripts/new_sidebar.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import Cookies from 'js-cookie';
-import _ from 'underscore';
-import bp from './breakpoints';
-
-export default class NewNavSidebar {
- constructor() {
- this.initDomElements();
- this.render();
- }
-
- initDomElements() {
- this.$page = $('.page-with-sidebar');
- this.$sidebar = $('.nav-sidebar');
- this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar);
- this.$overlay = $('.mobile-overlay');
- this.$openSidebar = $('.toggle-mobile-nav');
- this.$closeSidebar = $('.close-nav-button');
- this.$sidebarToggle = $('.js-toggle-sidebar');
- }
-
- bindEvents() {
- document.addEventListener('click', (e) => {
- if (!e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')) {
- this.toggleCollapsedSidebar(true);
- }
- });
- this.$openSidebar.on('click', () => this.toggleSidebarNav(true));
- this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
- this.$overlay.on('click', () => this.toggleSidebarNav(false));
- this.$sidebarToggle.on('click', () => {
- const value = !this.$sidebar.hasClass('sidebar-icons-only');
- this.toggleCollapsedSidebar(value);
- });
-
- $(window).on('resize', () => _.debounce(this.render(), 100));
- }
-
- static setCollapsedCookie(value) {
- if (bp.getBreakpointSize() !== 'lg') {
- return;
- }
- Cookies.set('sidebar_collapsed', value, { expires: 365 * 10 });
- }
-
- toggleSidebarNav(show) {
- this.$sidebar.toggleClass('nav-sidebar-expanded', show);
- this.$overlay.toggleClass('mobile-nav-open', show);
- this.$sidebar.removeClass('sidebar-icons-only');
- }
-
- toggleCollapsedSidebar(collapsed) {
- const breakpoint = bp.getBreakpointSize();
-
- if (this.$sidebar.length) {
- this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
- this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
- }
- NewNavSidebar.setCollapsedCookie(collapsed);
-
- this.toggleSidebarOverflow();
- }
-
- toggleSidebarOverflow() {
- if (this.$innerScroll.prop('scrollHeight') > this.$innerScroll.prop('offsetHeight')) {
- this.$innerScroll.css('overflow-y', 'scroll');
- } else {
- this.$innerScroll.css('overflow-y', '');
- }
- }
-
- render() {
- const breakpoint = bp.getBreakpointSize();
-
- if (breakpoint === 'sm' || breakpoint === 'md') {
- this.toggleCollapsedSidebar(true);
- } else if (breakpoint === 'lg') {
- const collapse = Cookies.get('sidebar_collapsed') === 'true';
- this.toggleCollapsedSidebar(collapse);
- }
- }
-}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 24de21f2ce2..e1ab28978e8 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -5,29 +5,27 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign,
no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new,
brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow,
newline-per-chained-call, no-useless-escape, class-methods-use-this */
-/* global Flash */
-/* global Autosave */
+
/* global ResolveService */
/* global mrRefreshWidgetUrl */
import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
-import autosize from 'vendor/autosize';
-import Dropzone from 'dropzone';
+import Autosize from 'autosize';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
+import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
+import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
-import './autosave';
-import './dropzone_input';
+import Autosave from './autosave';
import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
-window.autosize = autosize;
-window.Dropzone = Dropzone;
+window.autosize = Autosize;
function normalizeNewlines(str) {
return str.replace(/\r\n/g, '\n');
@@ -354,7 +352,7 @@ export default class Notes {
Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
}
- this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
+ this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0));
this.refresh();
}
return;
@@ -415,8 +413,9 @@ export default class Notes {
return;
}
this.note_ids.push(noteEntity.id);
+
form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
- row = form.closest('tr');
+ row = (form.length || !noteEntity.discussion_line_code) ? form.closest('tr') : $(`#${noteEntity.discussion_line_code}`);
if (noteEntity.on_image) {
row = form;
@@ -557,7 +556,7 @@ export default class Notes {
*/
setupNoteForm(form) {
var textarea, key;
- new gl.GLForm(form, this.enableGFM);
+ this.glForm = new GLForm(form, this.enableGFM);
textarea = form.find('.js-note-text');
key = [
'Note',
@@ -593,7 +592,7 @@ export default class Notes {
} else if ($form.hasClass('js-discussion-note-form')) {
formParentTimeline = $form.closest('.discussion-notes').find('.notes');
}
- return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline);
+ return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline.get(0));
}
updateNoteError($parentTimeline) {
@@ -1152,7 +1151,7 @@ export default class Notes {
var targetId = $originalContentEl.data('target-id');
var targetType = $originalContentEl.data('target-type');
- new gl.GLForm($editForm.find('form'), this.enableGFM);
+ this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
$editForm.find('form')
.attr('action', postUrl)
@@ -1213,13 +1212,13 @@ export default class Notes {
}
addFlash(...flashParams) {
- this.flashInstance = new Flash(...flashParams);
+ this.flashContainer = new Flash(...flashParams);
}
clearFlash() {
- if (this.flashInstance && this.flashInstance.flashContainer) {
- this.flashInstance.flashContainer.hide();
- this.flashInstance = null;
+ if (this.flashContainer) {
+ this.flashContainer.style.display = 'none';
+ this.flashContainer = null;
}
}
@@ -1257,7 +1256,7 @@ export default class Notes {
}
static checkMergeRequestStatus() {
- if (getPagePath(1) === 'merge_requests') {
+ if (getPagePath(1) === 'merge_requests' && gl.mrWidget) {
gl.mrWidget.checkStatus();
}
}
@@ -1282,10 +1281,12 @@ export default class Notes {
* Get data from Form attributes to use for saving/submitting comment.
*/
getFormData($form) {
+ const content = $form.find('.js-note-text').val();
return {
formData: $form.serialize(),
- formContent: _.escape($form.find('.js-note-text').val()),
+ formContent: _.escape(content),
formAction: $form.attr('action'),
+ formContentOriginal: content,
};
}
@@ -1417,7 +1418,7 @@ export default class Notes {
const isMainForm = $form.hasClass('js-main-target-form');
const isDiscussionForm = $form.hasClass('js-discussion-note-form');
const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
- const { formData, formContent, formAction } = this.getFormData($form);
+ const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);
let noteUniqueId;
let systemNoteUniqueId;
let hasQuickActions = false;
@@ -1576,7 +1577,7 @@ export default class Notes {
$form = $notesContainer.parent().find('form');
}
- $form.find('.js-note-text').val(formContent);
+ $form.find('.js-note-text').val(formContentOriginal);
this.reenableTargetFormSubmitButton(e);
this.addNoteError($form);
});
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue
index ab8516296a8..30e02554b65 100644
--- a/app/assets/javascripts/notes/components/issue_comment_form.vue
+++ b/app/assets/javascripts/notes/components/issue_comment_form.vue
@@ -1,9 +1,9 @@
<script>
- /* global Flash, Autosave */
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
- import autosize from 'vendor/autosize';
- import '../../autosave';
+ import Autosize from 'autosize';
+ import Flash from '../../flash';
+ import Autosave from '../../autosave';
import TaskList from '../../task_list';
import * as constants from '../constants';
import eventHub from '../event_hub';
@@ -145,7 +145,7 @@
Flash(
'Something went wrong while adding your comment. Please try again.',
'alert',
- $(this.$refs.commentForm),
+ this.$refs.commentForm,
);
}
} else {
@@ -160,7 +160,7 @@
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));
+ Flash(msg, 'alert', this.$el);
this.note = noteData.data.note.note; // Restore textarea content.
this.removePlaceholderNotes();
});
@@ -219,7 +219,7 @@
},
resizeTextarea() {
this.$nextTick(() => {
- autosize.update(this.$refs.textarea);
+ Autosize.update(this.$refs.textarea);
});
},
},
@@ -357,7 +357,8 @@
@click="handleSave(true)"
v-if="canUpdateIssue"
:class="actionButtonClassNames"
- class="btn btn-comment btn-comment-and-close">
+ :disabled="isSubmitting"
+ class="btn btn-comment btn-comment-and-close js-action-button">
{{issueActionButtonTitle}}
</button>
<button
diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue
index b131ef4b182..0f13221b81e 100644
--- a/app/assets/javascripts/notes/components/issue_discussion.vue
+++ b/app/assets/javascripts/notes/components/issue_discussion.vue
@@ -1,6 +1,6 @@
<script>
- /* global Flash */
import { mapActions, mapGetters } from 'vuex';
+ import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import issueNote from './issue_note.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -9,8 +9,8 @@
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 placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
+ import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
export default {
@@ -133,7 +133,7 @@
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));
+ Flash(msg, 'alert', this.$el);
this.$refs.noteForm.note = noteText;
callback(err);
});
diff --git a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
index e73ec2aaf71..64466b04b40 100644
--- a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
@@ -1,18 +1,21 @@
<script>
+ import Icon from '../../vue_shared/components/icon.vue';
+
export default {
- computed: {
- lockIcon() {
- return gl.utils.spriteIcon('lock');
- },
+ component: {
+ Icon,
},
};
-
</script>
<template>
<div class="disabled-comment text-center">
- <span class="issuable-note-warning">
- <span class="icon" v-html="lockIcon"></span>
+ <span class="issuable-note-warning inline">
+ <icon
+ name="lock"
+ :size="16"
+ class="icon">
+ </icon>
<span>This issue is locked. Only <b>project members</b> can comment.</span>
</span>
</div>
diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue
index 1f43b8a16ad..40318f9a600 100644
--- a/app/assets/javascripts/notes/components/issue_note.vue
+++ b/app/assets/javascripts/notes/components/issue_note.vue
@@ -1,7 +1,6 @@
<script>
- /* global Flash */
-
import { mapGetters, mapActions } from 'vuex';
+ import Flash from '../../flash';
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';
@@ -101,7 +100,7 @@
this.isEditing = true;
this.$nextTick(() => {
const msg = 'Something went wrong while editing your comment. Please try again.';
- Flash(msg, 'alert', $(this.$el));
+ Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
});
@@ -123,7 +122,9 @@
// 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
+ if (this.$refs.noteBody) {
+ this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better
+ }
},
},
created() {
diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue
index d42e61e3899..c3a340139e7 100644
--- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue
@@ -1,10 +1,9 @@
<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 Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip';
diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue
index b6fc5e5036f..5c9119644e3 100644
--- a/app/assets/javascripts/notes/components/issue_notes_app.vue
+++ b/app/assets/javascripts/notes/components/issue_notes_app.vue
@@ -1,14 +1,14 @@
<script>
- /* global Flash */
import { mapGetters, mapActions } from 'vuex';
+ import Flash from '../../flash';
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 systemNote from '../../vue_shared/components/notes/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 placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
+ import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
@@ -37,7 +37,7 @@
components: {
issueNote,
issueDiscussion,
- issueSystemNote,
+ systemNote,
issueCommentForm,
loadingIcon,
placeholderNote,
@@ -68,7 +68,7 @@
}
return placeholderNote;
} else if (note.individual_note) {
- return note.notes[0].system ? issueSystemNote : issueNote;
+ return note.notes[0].system ? systemNote : issueNote;
}
return issueDiscussion;
diff --git a/app/assets/javascripts/notes/components/issue_placeholder_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_note.vue
deleted file mode 100644
index 6921d91372f..00000000000
--- a/app/assets/javascripts/notes/components/issue_placeholder_note.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<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
deleted file mode 100644
index 80a8ef56a83..00000000000
--- a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue
+++ /dev/null
@@ -1,21 +0,0 @@
-<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
deleted file mode 100644
index 0cfb6522e77..00000000000
--- a/app/assets/javascripts/notes/components/issue_system_note.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<script>
- import { mapGetters } from 'vuex';
- 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;
- },
- iconHtml() {
- return gl.utils.spriteIcon(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="iconHtml">
- </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/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index 5843b97f225..a008171beda 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -1,5 +1,4 @@
-/* globals Autosave */
-import '../../autosave';
+import Autosave from '../../autosave';
export default {
methods: {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 1a791039909..6f04aecc9b7 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,5 +1,5 @@
-/* global Flash */
import Visibility from 'visibilityjs';
+import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
import * as utils from './utils';
@@ -99,7 +99,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
eTagPoll.makeRequest();
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
- Flash('Commands applied', 'notice', $(noteData.flashContainer));
+ Flash('Commands applied', 'notice', noteData.flashContainer);
}
if (commandsChanges) {
@@ -114,8 +114,8 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
.catch(() => {
Flash(
'Something went wrong while adding your award. Please try again.',
- null,
- $(noteData.flashContainer),
+ 'alert',
+ noteData.flashContainer,
);
});
}
@@ -126,7 +126,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
}
if (errors && errors.commands_only) {
- Flash(errors.commands_only, 'notice', $(noteData.flashContainer));
+ Flash(errors.commands_only, 'notice', noteData.flashContainer);
}
commit(types.REMOVE_PLACEHOLDER_NOTES);
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index 838356133cd..f90ac2d9f71 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,5 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, max-len */
-/* global Flash */
+import Flash from './flash';
(function() {
this.NotificationsDropdown = (function() {
diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js
index 50c725aa3d5..f1cf6e92ef5 100644
--- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js
+++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import Translate from '../vue_shared/translate';
+import GlFieldErrors from '../gl_field_errors';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
@@ -39,7 +40,7 @@ document.addEventListener('DOMContentLoaded', () => {
gl.timezoneDropdown = new TimezoneDropdown();
gl.targetBranchDropdown = new TargetBranchDropdown();
- gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement);
+ gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);
setupPipelineVariableList($('.js-pipeline-variable-list'));
});
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 54227425d2a..19d8e1f49cf 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,7 +1,7 @@
<script>
- import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltip from '../../../vue_shared/directives/tooltip';
-
+ import icon from '../../../vue_shared/components/icon.vue';
+ import { dasherize } from '../../../lib/utils/text_utility';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
@@ -29,17 +29,18 @@
},
},
+ components: {
+ icon,
+ },
+
directives: {
tooltip,
},
computed: {
- actionIconSvg() {
- return getActionIcon(this.actionIcon);
- },
-
cssClass() {
- return `js-${gl.text.dasherize(this.actionIcon)}`;
+ const actionIconDash = dasherize(this.actionIcon);
+ return `${actionIconDash} js-icon-${actionIconDash}`;
},
},
};
@@ -50,14 +51,9 @@
:data-method="actionMethod"
:title="tooltipText"
:href="link"
- class="ci-action-icon-container"
+ class="ci-action-icon-container ci-action-icon-wrapper"
+ :class="cssClass"
data-container="body">
-
- <i
- class="ci-action-icon-wrapper"
- :class="cssClass"
- v-html="actionIconSvg"
- aria-hidden="true"
- />
+ <icon :name="actionIcon"/>
</a>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
index 18fe1847eef..1c0944d45fc 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
@@ -1,5 +1,5 @@
<script>
- import getActionIcon from '../../../vue_shared/ci_action_icons';
+ import icon from '../../../vue_shared/components/icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
@@ -29,14 +29,12 @@
},
},
- directives: {
- tooltip,
+ components: {
+ icon,
},
- computed: {
- actionIconSvg() {
- return getActionIcon(this.actionIcon);
- },
+ directives: {
+ tooltip,
},
};
</script>
@@ -49,7 +47,7 @@
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-container="body"
- v-html="actionIconSvg"
aria-label="Job's action">
+ <icon :name="actionIcon"/>
</a>
</template>
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 3e5d6d15909..7006d05e7b2 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -18,7 +18,7 @@
* "group": "success",
* "details_path": "/root/ci-mock/builds/4256",
* "action": {
- * "icon": "icon_action_retry",
+ * "icon": "retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 3933509a6f4..5dea4555515 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -19,7 +19,7 @@
* "group": "success",
* "details_path": "/root/ci-mock/builds/4256",
* "action": {
- * "icon": "icon_action_retry",
+ * "icon": "retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
diff --git a/app/assets/javascripts/pipelines/components/navigation_tabs.vue b/app/assets/javascripts/pipelines/components/navigation_tabs.vue
deleted file mode 100644
index 73f7e3a0cad..00000000000
--- a/app/assets/javascripts/pipelines/components/navigation_tabs.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-<script>
- export default {
- name: 'PipelineNavigationTabs',
- props: {
- scope: {
- type: String,
- required: true,
- },
- count: {
- type: Object,
- required: true,
- },
- paths: {
- type: Object,
- required: true,
- },
- },
- mounted() {
- $(document).trigger('init.scrolling-tabs');
- },
- methods: {
- shouldRenderBadge(count) {
- // 0 is valid in a badge, but evaluates to false, we need to check for undefined
- return count !== undefined;
- },
- },
-};
-</script>
-<template>
- <ul class="nav-links scrolling-tabs">
- <li
- class="js-pipelines-tab-all"
- :class="{ active: scope === 'all'}">
- <a :href="paths.allPath">
- All
- <span
- v-if="shouldRenderBadge(count.all)"
- class="badge js-totalbuilds-count">
- {{count.all}}
- </span>
- </a>
- </li>
- <li
- class="js-pipelines-tab-pending"
- :class="{ active: scope === 'pending'}">
- <a :href="paths.pendingPath">
- Pending
- <span
- v-if="shouldRenderBadge(count.pending)"
- class="badge">
- {{count.pending}}
- </span>
- </a>
- </li>
- <li
- class="js-pipelines-tab-running"
- :class="{ active: scope === 'running'}">
- <a :href="paths.runningPath">
- Running
- <span
- v-if="shouldRenderBadge(count.running)"
- class="badge">
- {{count.running}}
- </span>
- </a>
- </li>
- <li
- class="js-pipelines-tab-finished"
- :class="{ active: scope === 'finished'}">
- <a :href="paths.finishedPath">
- Finished
- <span
- v-if="shouldRenderBadge(count.finished)"
- class="badge">
- {{count.finished}}
- </span>
- </a>
- </li>
- <li
- class="js-pipelines-tab-branches"
- :class="{ active: scope === 'branches'}">
- <a :href="paths.branchesPath">Branches</a>
- </li>
- <li
- class="js-pipelines-tab-tags"
- :class="{ active: scope === 'tags'}">
- <a :href="paths.tagsPath">Tags</a>
- </li>
- </ul>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 085bd20cefe..fe1f3b4246a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -1,10 +1,16 @@
<script>
+ import _ from 'underscore';
import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
- import navigationTabs from './navigation_tabs.vue';
+ import navigationTabs from '../../vue_shared/components/navigation_tabs.vue';
import navigationControls from './nav_controls.vue';
- import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
+ import {
+ convertPermissionToBoolean,
+ getParameterByName,
+ parseQueryStringIntoObject,
+ } from '../../lib/utils/common_utils';
+ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default {
props: {
@@ -12,6 +18,15 @@
type: Object,
required: true,
},
+ // Can be rendered in 3 different places, with some visual differences
+ // Accepts root | child
+ // `root` -> main view
+ // `child` -> rendered inside MR or Commit View
+ viewType: {
+ type: String,
+ required: false,
+ default: 'root',
+ },
},
components: {
tablePagination,
@@ -20,6 +35,7 @@
},
mixins: [
pipelinesMixin,
+ CIPaginationMixin,
],
data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
@@ -32,27 +48,18 @@
autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
- allPath: pipelinesData.allPath,
- pendingPath: pipelinesData.pendingPath,
- runningPath: pipelinesData.runningPath,
- finishedPath: pipelinesData.finishedPath,
- branchesPath: pipelinesData.branchesPath,
- tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
state: this.store.state,
- apiScope: 'all',
- pagenum: 1,
+ scope: getParameterByName('scope') || 'all',
+ page: getParameterByName('page') || '1',
+ requestData: {},
};
},
computed: {
canCreatePipelineParsed() {
return convertPermissionToBoolean(this.canCreatePipeline);
},
- scope() {
- const scope = getParameterByName('scope');
- return scope === null ? 'all' : scope;
- },
/**
* The empty state should only be rendered when the request is made to fetch all pipelines
@@ -97,46 +104,90 @@
hasCiEnabled() {
return this.hasCi !== undefined;
},
- paths() {
- return {
- allPath: this.allPath,
- pendingPath: this.pendingPath,
- finishedPath: this.finishedPath,
- runningPath: this.runningPath,
- branchesPath: this.branchesPath,
- tagsPath: this.tagsPath,
- };
- },
- pageParameter() {
- return getParameterByName('page') || this.pagenum;
- },
- scopeParameter() {
- return getParameterByName('scope') || this.apiScope;
+
+ tabs() {
+ const { count } = this.state;
+ return [
+ {
+ name: 'All',
+ scope: 'all',
+ count: count.all,
+ isActive: this.scope === 'all',
+ },
+ {
+ name: 'Pending',
+ scope: 'pending',
+ count: count.pending,
+ isActive: this.scope === 'pending',
+ },
+ {
+ name: 'Running',
+ scope: 'running',
+ count: count.running,
+ isActive: this.scope === 'running',
+ },
+ {
+ name: 'Finished',
+ scope: 'finished',
+ count: count.finished,
+ isActive: this.scope === 'finished',
+ },
+ {
+ name: 'Branches',
+ scope: 'branches',
+ isActive: this.scope === 'branches',
+ },
+ {
+ name: 'Tags',
+ scope: 'tags',
+ isActive: this.scope === 'tags',
+ },
+ ];
},
},
created() {
this.service = new PipelinesService(this.endpoint);
- this.requestData = { page: this.pageParameter, scope: this.scopeParameter };
+ this.requestData = { page: this.page, scope: this.scope };
},
methods: {
+ successCallback(resp) {
+ return resp.json().then((response) => {
+ // Because we are polling & the user is interacting verify if the response received
+ // matches the last request made
+ if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
+ this.store.storeCount(response.count);
+ this.store.storePagination(resp.headers);
+ this.setCommonData(response.pipelines);
+ }
+ });
+ },
/**
- * Will change the page number and update the URL.
- *
- * @param {Number} pageNumber desired page to go to.
+ * Handles URL and query parameter changes.
+ * When the user uses the pagination or the tabs,
+ * - update URL
+ * - Make API request to the server with new parameters
+ * - Update the polling function
+ * - Update the internal state
*/
- change(pageNumber) {
- const param = setParamInURL('page', pageNumber);
+ updateContent(parameters) {
+ this.updateInternalState(parameters);
- gl.utils.visitUrl(param);
- return param;
- },
+ // fetch new data
+ return this.service.getPipelines(this.requestData)
+ .then((response) => {
+ this.isLoading = false;
+ this.successCallback(response);
- successCallback(resp) {
- return resp.json().then((response) => {
- this.store.storeCount(response.count);
- this.store.storePagination(resp.headers);
- this.setCommonData(response.pipelines);
- });
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ })
+ .catch(() => {
+ this.isLoading = false;
+ this.errorCallback();
+
+ // restart polling
+ this.poll.restart();
+ });
},
},
};
@@ -145,7 +196,7 @@
<div class="pipelines-container">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
- v-if="!isLoading && !shouldRenderEmptyState">
+ v-if="!shouldRenderEmptyState">
<div class="fade-left">
<i
class="fa fa-angle-left"
@@ -158,17 +209,18 @@
aria-hidden="true">
</i>
</div>
+
<navigation-tabs
- :scope="scope"
- :count="state.count"
- :paths="paths"
+ :tabs="tabs"
+ @onChangeTab="onChangeTab"
+ scope="pipelines"
/>
<navigation-controls
:new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
- :ciLintPath="ciLintPath"
+ :ci-lint-path="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed "
/>
</div>
@@ -179,6 +231,7 @@
label="Loading Pipelines"
size="3"
v-if="isLoading"
+ class="prepend-top-20"
/>
<empty-state
@@ -187,15 +240,17 @@
:empty-state-svg-path="emptyStateSvgPath"
/>
- <error-state
+ <error-state
v-if="shouldRenderErrorState"
:error-state-svg-path="errorStateSvgPath"
/>
<div
- class="blank-state blank-state-no-icon"
+ class="blank-state-row"
v-if="shouldRenderNoPipelinesMessage">
- <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
+ <div class="blank-state-center">
+ <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
+ </div>
</div>
<div
@@ -206,13 +261,14 @@
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsPath"
+ :view-type="viewType"
/>
</div>
<table-pagination
v-if="shouldRenderPagination"
- :change="change"
- :pageInfo="state.pageInfo"
+ :change="onChangePage"
+ :page-info="state.pageInfo"
/>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index c4c63a52358..f3c0aca17ba 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -1,6 +1,4 @@
<script>
- /* global Flash */
- import '~/flash';
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index 7aa0c0e8a7f..16a705cbaff 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -21,6 +21,10 @@
type: String,
required: true,
},
+ viewType: {
+ type: String,
+ required: true,
+ },
},
components: {
pipelinesTableRowComponent,
@@ -59,6 +63,7 @@
:pipeline="model"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
+ :view-type="viewType"
/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 5b9bb6c3750..33fbce993b2 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -29,6 +29,10 @@ export default {
type: String,
required: true,
},
+ viewType: {
+ type: String,
+ required: true,
+ },
},
components: {
asyncButtonComponent,
@@ -203,9 +207,13 @@ export default {
displayPipelineActions() {
return this.pipeline.flags.retryable ||
- this.pipeline.flags.cancelable ||
- this.pipeline.details.manual_actions.length ||
- this.pipeline.details.artifacts.length;
+ this.pipeline.flags.cancelable ||
+ this.pipeline.details.manual_actions.length ||
+ this.pipeline.details.artifacts.length;
+ },
+
+ isChildView() {
+ return this.viewType === 'child';
},
},
};
@@ -218,7 +226,10 @@ export default {
Status
</div>
<div class="table-mobile-content">
- <ci-badge :status="pipelineStatus"/>
+ <ci-badge
+ :status="pipelineStatus"
+ :show-text="!isChildView"
+ />
</div>
</div>
@@ -240,7 +251,9 @@ export default {
:commit-url="commitUrl"
:short-sha="commitShortSha"
:title="commitTitle"
- :author="commitAuthor"/>
+ :author="commitAuthor"
+ :show-branch="!isChildView"
+ />
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index a4a27247406..ac9d9c901ca 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -13,8 +13,8 @@
* 4. Commit widget
*/
-/* global Flash */
-import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
+import Flash from '../../flash';
+import icon from '../../vue_shared/components/icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -45,6 +45,7 @@ export default {
components: {
loadingIcon,
+ icon,
},
updated() {
@@ -122,8 +123,8 @@ export default {
return `ci-status-icon-${this.stage.status.group}`;
},
- svgIcon() {
- return borderlessStatusIconEntityMap[this.stage.status.icon];
+ borderlessIcon() {
+ return `${this.stage.status.icon}_borderless`;
},
},
};
@@ -145,9 +146,10 @@ export default {
aria-expanded="false">
<span
- v-html="svgIcon"
aria-hidden="true"
:aria-label="stage.title">
+ <icon
+ :name="borderlessIcon"/>
</span>
<i
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index e97f5632dc8..50bdf80c3e3 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -1,6 +1,5 @@
-/* global Flash */
-import '~/flash';
import Visibility from 'visibilityjs';
+import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
import emptyState from '../components/empty_state.vue';
import errorState from '../components/error_state.vue';
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index bfc416da50b..206023d4ddb 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,6 +1,5 @@
-/* global Flash */
-
import Vue from 'vue';
+import Flash from '../flash';
import PipelinesMediator from './pipeline_details_mediatior';
import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue';
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js
index 385e7430a7d..823ccd849f4 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js
@@ -1,6 +1,5 @@
-/* global Flash */
-
import Visibility from 'visibilityjs';
+import Flash from '../flash';
import Poll from '../lib/utils/poll';
import PipelineStore from './stores/pipeline_store';
import PipelineService from './services/pipeline_service';
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index b2b34cb83e1..6348a2e331d 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -98,7 +98,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
@toggle="toggleOpen"
@submit="onSubmit">
- <template slot="body" scope="props">
+ <template slot="body" slot-scope="props">
<p v-html="props.text"></p>
<form
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 3deb242bc1f..0dc02f012e4 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,5 +1,5 @@
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
-/* global Flash */
+import Flash from '../flash';
import { getPagePath } from '../lib/utils/common_utils';
((global) => {
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index fe6602259e2..3131e71d9d6 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,139 +1,132 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
-/* global ProjectSelect */
+/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
import Cookies from 'js-cookie';
+import projectSelect from './project_select';
-(function() {
- this.Project = (function() {
- function Project() {
- const $cloneOptions = $('ul.clone-options-dropdown');
- const $projectCloneField = $('#project_clone');
- const $cloneBtnText = $('a.clone-dropdown-btn span');
+export default class Project {
+ constructor() {
+ const $cloneOptions = $('ul.clone-options-dropdown');
+ const $projectCloneField = $('#project_clone');
+ const $cloneBtnText = $('a.clone-dropdown-btn span');
- const selectedCloneOption = $cloneBtnText.text().trim();
- if (selectedCloneOption.length > 0) {
- $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
- }
-
- $('a', $cloneOptions).on('click', (e) => {
- const $this = $(e.currentTarget);
- const url = $this.attr('href');
-
- e.preventDefault();
-
- $('.is-active', $cloneOptions).not($this).removeClass('is-active');
- $this.toggleClass('is-active');
- $projectCloneField.val(url);
- $cloneBtnText.text($this.text());
-
- return $('.clone').text(url);
- });
- // Ref switcher
- this.initRefSwitcher();
- $('.project-refs-select').on('change', function() {
- return $(this).parents('form').submit();
- });
- $('.hide-no-ssh-message').on('click', function(e) {
- Cookies.set('hide_no_ssh_message', 'false');
- $(this).parents('.no-ssh-key-message').remove();
- return e.preventDefault();
- });
- $('.hide-no-password-message').on('click', function(e) {
- Cookies.set('hide_no_password_message', 'false');
- $(this).parents('.no-password-message').remove();
- return e.preventDefault();
- });
- this.projectSelectDropdown();
+ const selectedCloneOption = $cloneBtnText.text().trim();
+ if (selectedCloneOption.length > 0) {
+ $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
}
- Project.prototype.projectSelectDropdown = function() {
- new ProjectSelect();
- $('.project-item-select').on('click', (function(_this) {
- return function(e) {
- return _this.changeProject($(e.currentTarget).val());
- };
- })(this));
- };
-
- Project.prototype.changeProject = function(url) {
- return window.location = url;
- };
-
- Project.prototype.initRefSwitcher = function() {
- var refListItem = document.createElement('li');
- var refLink = document.createElement('a');
-
- refLink.href = '#';
-
- return $('.js-project-refs-dropdown').each(function() {
- var $dropdown, selected;
- $dropdown = $(this);
- selected = $dropdown.data('selected');
- return $dropdown.glDropdown({
- data: function(term, callback) {
- return $.ajax({
- url: $dropdown.data('refs-url'),
- data: {
- ref: $dropdown.data('ref'),
- search: term
- },
- dataType: "json"
- }).done(function(refs) {
- return callback(refs);
- });
- },
- selectable: true,
- filterable: true,
- filterRemote: true,
- filterByText: true,
- inputFieldName: $dropdown.data('input-field-name'),
- fieldName: $dropdown.data('field-name'),
- renderRow: function(ref) {
- var li = refListItem.cloneNode(false);
-
- if (ref.header != null) {
- li.className = 'dropdown-header';
- li.textContent = ref.header;
- } else {
- var link = refLink.cloneNode(false);
-
- if (ref === selected) {
- link.className = 'is-active';
- }
-
- link.textContent = ref;
- link.dataset.ref = ref;
-
- li.appendChild(link);
+ $('a', $cloneOptions).on('click', (e) => {
+ const $this = $(e.currentTarget);
+ const url = $this.attr('href');
+ const activeText = $this.find('.dropdown-menu-inner-title').text();
+
+ e.preventDefault();
+
+ $('.is-active', $cloneOptions).not($this).removeClass('is-active');
+ $this.toggleClass('is-active');
+ $projectCloneField.val(url);
+ $cloneBtnText.text(activeText);
+
+ return $('.clone').text(url);
+ });
+ // Ref switcher
+ Project.initRefSwitcher();
+ $('.project-refs-select').on('change', function() {
+ return $(this).parents('form').submit();
+ });
+ $('.hide-no-ssh-message').on('click', function(e) {
+ Cookies.set('hide_no_ssh_message', 'false');
+ $(this).parents('.no-ssh-key-message').remove();
+ return e.preventDefault();
+ });
+ $('.hide-no-password-message').on('click', function(e) {
+ Cookies.set('hide_no_password_message', 'false');
+ $(this).parents('.no-password-message').remove();
+ return e.preventDefault();
+ });
+ Project.projectSelectDropdown();
+ }
+
+ static projectSelectDropdown () {
+ projectSelect();
+ $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
+ }
+
+ static changeProject(url) {
+ return window.location = url;
+ }
+
+ static initRefSwitcher() {
+ var refListItem = document.createElement('li');
+ var refLink = document.createElement('a');
+
+ refLink.href = '#';
+
+ return $('.js-project-refs-dropdown').each(function() {
+ var $dropdown, selected;
+ $dropdown = $(this);
+ selected = $dropdown.data('selected');
+ return $dropdown.glDropdown({
+ data: function(term, callback) {
+ return $.ajax({
+ url: $dropdown.data('refs-url'),
+ data: {
+ ref: $dropdown.data('ref'),
+ search: term,
+ },
+ dataType: 'json',
+ }).done(function(refs) {
+ return callback(refs);
+ });
+ },
+ selectable: true,
+ filterable: true,
+ filterRemote: true,
+ filterByText: true,
+ inputFieldName: $dropdown.data('input-field-name'),
+ fieldName: $dropdown.data('field-name'),
+ renderRow: function(ref) {
+ var li = refListItem.cloneNode(false);
+
+ if (ref.header != null) {
+ li.className = 'dropdown-header';
+ li.textContent = ref.header;
+ } else {
+ var link = refLink.cloneNode(false);
+
+ if (ref === selected) {
+ link.className = 'is-active';
}
- return li;
- },
- id: function(obj, $el) {
- return $el.attr('data-ref');
- },
- toggleLabel: function(obj, $el) {
- return $el.text().trim();
- },
- clicked: function(options) {
- const { e } = options;
- e.preventDefault();
- if ($('input[name="ref"]').length) {
- var $form = $dropdown.closest('form');
-
- var $visit = $dropdown.data('visit');
- var shouldVisit = $visit ? true : $visit;
- var action = $form.attr('action');
- var divider = action.indexOf('?') === -1 ? '?' : '&';
- if (shouldVisit) {
- gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
- }
+ link.textContent = ref;
+ link.dataset.ref = ref;
+
+ li.appendChild(link);
+ }
+
+ return li;
+ },
+ id: function(obj, $el) {
+ return $el.attr('data-ref');
+ },
+ toggleLabel: function(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked: function(options) {
+ const { e } = options;
+ e.preventDefault();
+ if ($('input[name="ref"]').length) {
+ var $form = $dropdown.closest('form');
+
+ var $visit = $dropdown.data('visit');
+ var shouldVisit = $visit ? true : $visit;
+ var action = $form.attr('action');
+ var divider = action.indexOf('?') === -1 ? '?' : '&';
+ if (shouldVisit) {
+ gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
}
}
- });
+ },
});
- };
-
- return Project;
- })();
-}).call(window);
+ });
+ }
+}
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
index aabdfbf65e2..56627aa155c 100644
--- a/app/assets/javascripts/project_avatar.js
+++ b/app/assets/javascripts/project_avatar.js
@@ -1,20 +1,13 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
-(function() {
- this.ProjectAvatar = (function() {
- function ProjectAvatar() {
- $('.js-choose-project-avatar-button').bind('click', function() {
- var form;
- form = $(this).closest('form');
- return form.find('.js-project-avatar-input').click();
- });
- $('.js-project-avatar-input').bind('change', function() {
- var filename, form;
- form = $(this).closest('form');
- filename = $(this).val().replace(/^.*[\\\/]/, '');
- return form.find('.js-avatar-filename').text(filename);
- });
- }
+export default function projectAvatar() {
+ $('.js-choose-project-avatar-button').bind('click', function onClickAvatar() {
+ const form = $(this).closest('form');
+ return form.find('.js-project-avatar-input').click();
+ });
- return ProjectAvatar;
- })();
-}).call(window);
+ $('.js-project-avatar-input').bind('change', function onClickAvatarInput() {
+ const form = $(this).closest('form');
+ // eslint-disable-next-line no-useless-escape
+ const filename = $(this).val().replace(/^.*[\\\/]/, '');
+ return form.find('.js-avatar-filename').text(filename);
+ });
+}
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 11f9754780d..19682b20a4a 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */
-/* global fuzzaldrinPlus */
+
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
(function() {
this.ProjectFindFile = (function() {
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index 08334bf1ec5..d2d26d6f67e 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,13 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
+import { visitUrl } from './lib/utils/url_utility';
-(function() {
- this.ProjectImport = (function() {
- function ProjectImport() {
- setTimeout(function() {
- return gl.utils.visitUrl(location.href);
- }, 5000);
- }
+export default function projectImport() {
+ setTimeout(() => {
+ visitUrl(location.href);
+ }, 5000);
+}
- return ProjectImport;
- })();
-}).call(window);
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index 0a811627600..b65521b278f 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -1,55 +1,50 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */
+export default class ProjectLabelSubscription {
+ constructor(container) {
+ this.$container = $(container);
+ this.$buttons = this.$container.find('.js-subscribe-button');
-(function(global) {
- class ProjectLabelSubscription {
- constructor(container) {
- this.$container = $(container);
- this.$buttons = this.$container.find('.js-subscribe-button');
-
- this.$buttons.on('click', this.toggleSubscription.bind(this));
- }
+ this.$buttons.on('click', this.toggleSubscription.bind(this));
+ }
- toggleSubscription(event) {
- event.preventDefault();
+ toggleSubscription(event) {
+ event.preventDefault();
- const $btn = $(event.currentTarget);
- const $span = $btn.find('span');
- const url = $btn.attr('data-url');
- const oldStatus = $btn.attr('data-status');
+ const $btn = $(event.currentTarget);
+ const $span = $btn.find('span');
+ const url = $btn.attr('data-url');
+ const oldStatus = $btn.attr('data-status');
- $btn.addClass('disabled');
- $span.toggleClass('hidden');
+ $btn.addClass('disabled');
+ $span.toggleClass('hidden');
- $.ajax({
- type: 'POST',
- url: url
- }).done(() => {
- let newStatus, newAction;
+ $.ajax({
+ type: 'POST',
+ url,
+ }).done(() => {
+ let newStatus;
+ let newAction;
- if (oldStatus === 'unsubscribed') {
- [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
- } else {
- [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
- }
+ if (oldStatus === 'unsubscribed') {
+ [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
+ } else {
+ [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
+ }
- $span.toggleClass('hidden');
- $btn.removeClass('disabled');
+ $span.toggleClass('hidden');
+ $btn.removeClass('disabled');
- this.$buttons.attr('data-status', newStatus);
- this.$buttons.find('> span').text(newAction);
+ this.$buttons.attr('data-status', newStatus);
+ this.$buttons.find('> span').text(newAction);
- this.$buttons.map((button) => {
- const $button = $(button);
+ this.$buttons.map((button) => {
+ const $button = $(button);
- if ($button.attr('data-original-title')) {
- $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
- }
+ if ($button.attr('data-original-title')) {
+ $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
+ }
- return button;
- });
+ return button;
});
- }
+ });
}
-
- global.ProjectLabelSubscription = ProjectLabelSubscription;
-})(window.gl || (window.gl = {}));
+}
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index fd89a1a85c3..ca548d011b6 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
+/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/
import VisibilitySelect from './visibility_select';
@@ -7,153 +7,145 @@ function highlightChanges($elm) {
setTimeout(() => $elm.removeClass('highlight-changes'), 10);
}
-(function() {
- this.ProjectNew = (function() {
- function ProjectNew() {
- this.toggleSettings = this.toggleSettings.bind(this);
- this.$selects = $('.features select');
- this.$repoSelects = this.$selects.filter('.js-repo-select');
- this.$projectSelects = this.$selects.not('.js-repo-select');
-
- $('.project-edit-container').on('ajax:before', (function(_this) {
- return function() {
- $('.project-edit-container').hide();
- return $('.save-project-loader').show();
- };
- })(this));
-
- this.initVisibilitySelect();
-
- this.toggleSettings();
- this.toggleSettingsOnclick();
- this.toggleRepoVisibility();
- }
-
- ProjectNew.prototype.initVisibilitySelect = function() {
- const visibilityContainer = document.querySelector('.js-visibility-select');
- if (!visibilityContainer) return;
- const visibilitySelect = new VisibilitySelect(visibilityContainer);
- visibilitySelect.init();
-
- const $visibilitySelect = $(visibilityContainer).find('select');
- let projectVisibility = $visibilitySelect.val();
- const PROJECT_VISIBILITY_PRIVATE = '0';
-
- $visibilitySelect.on('change', () => {
- const newProjectVisibility = $visibilitySelect.val();
-
- if (projectVisibility !== newProjectVisibility) {
- this.$projectSelects.each((idx, select) => {
- const $select = $(select);
- const $options = $select.find('option');
- const values = $.map($options, e => e.value);
-
- // if switched to "private", limit visibility options
- if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
- if ($select.val() !== values[0] && $select.val() !== values[1]) {
- $select.val(values[1]).trigger('change');
- highlightChanges($select);
- }
- $options.slice(2).disable();
+export default class ProjectNew {
+ constructor() {
+ this.toggleSettings = this.toggleSettings.bind(this);
+ this.$selects = $('.features select');
+ this.$repoSelects = this.$selects.filter('.js-repo-select');
+ this.$projectSelects = this.$selects.not('.js-repo-select');
+
+ $('.project-edit-container').on('ajax:before', () => {
+ $('.project-edit-container').hide();
+ return $('.save-project-loader').show();
+ });
+
+ this.initVisibilitySelect();
+
+ this.toggleSettings();
+ this.toggleSettingsOnclick();
+ this.toggleRepoVisibility();
+ }
+
+ initVisibilitySelect() {
+ const visibilityContainer = document.querySelector('.js-visibility-select');
+ if (!visibilityContainer) return;
+ const visibilitySelect = new VisibilitySelect(visibilityContainer);
+ visibilitySelect.init();
+
+ const $visibilitySelect = $(visibilityContainer).find('select');
+ let projectVisibility = $visibilitySelect.val();
+ const PROJECT_VISIBILITY_PRIVATE = '0';
+
+ $visibilitySelect.on('change', () => {
+ const newProjectVisibility = $visibilitySelect.val();
+
+ if (projectVisibility !== newProjectVisibility) {
+ this.$projectSelects.each((idx, select) => {
+ const $select = $(select);
+ const $options = $select.find('option');
+ const values = $.map($options, e => e.value);
+
+ // if switched to "private", limit visibility options
+ if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
+ if ($select.val() !== values[0] && $select.val() !== values[1]) {
+ $select.val(values[1]).trigger('change');
+ highlightChanges($select);
}
+ $options.slice(2).disable();
+ }
- // if switched from "private", increase visibility for non-disabled options
- if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
- $options.enable();
- if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
- $select.val(values[values.length - 1]).trigger('change');
- highlightChanges($select);
- }
+ // if switched from "private", increase visibility for non-disabled options
+ if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
+ $options.enable();
+ if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
+ $select.val(values[values.length - 1]).trigger('change');
+ highlightChanges($select);
}
- });
+ }
+ });
- projectVisibility = newProjectVisibility;
- }
- });
- };
-
- ProjectNew.prototype.toggleSettings = function() {
- var self = this;
-
- this.$selects.each(function () {
- var $select = $(this);
- var className = $select.data('field')
- .replace(/_/g, '-')
- .replace('access-level', 'feature');
- self._showOrHide($select, '.' + className);
- });
- };
-
- ProjectNew.prototype.toggleSettingsOnclick = function() {
- this.$selects.on('change', this.toggleSettings);
- };
-
- ProjectNew.prototype._showOrHide = function(checkElement, container) {
- var $container = $(container);
-
- if ($(checkElement).val() !== '0') {
- return $container.show();
- } else {
- return $container.hide();
+ projectVisibility = newProjectVisibility;
}
- };
-
- ProjectNew.prototype.toggleRepoVisibility = function () {
- var $repoAccessLevel = $('.js-repo-access-level select');
- var $lfsEnabledOption = $('.js-lfs-enabled select');
- var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
- var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
- var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
-
- this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
- .nextAll()
- .hide();
-
- $repoAccessLevel.off('change')
- .on('change', function () {
- var selectedVal = parseInt($repoAccessLevel.val(), 10);
-
- this.$repoSelects.each(function () {
- var $this = $(this);
- var repoSelectVal = parseInt($this.val(), 10);
-
- $this.find('option').enable();
-
- if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
- $this.val(selectedVal).trigger('change');
- highlightChanges($this);
- }
-
- $this.find("option[value='" + selectedVal + "']").nextAll().disable();
- });
+ });
+ }
+
+ toggleSettings() {
+ this.$selects.each(function () {
+ var $select = $(this);
+ var className = $select.data('field')
+ .replace(/_/g, '-')
+ .replace('access-level', 'feature');
+ ProjectNew._showOrHide($select, '.' + className);
+ });
+ }
+
+ toggleSettingsOnclick() {
+ this.$selects.on('change', this.toggleSettings);
+ }
+
+ static _showOrHide(checkElement, container) {
+ const $container = $(container);
+
+ if ($(checkElement).val() !== '0') {
+ return $container.show();
+ }
+ return $container.hide();
+ }
+
+ toggleRepoVisibility() {
+ var $repoAccessLevel = $('.js-repo-access-level select');
+ var $lfsEnabledOption = $('.js-lfs-enabled select');
+ var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
+ var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
+ var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
+
+ this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
+ .nextAll()
+ .hide();
+
+ $repoAccessLevel
+ .off('change')
+ .on('change', function () {
+ var selectedVal = parseInt($repoAccessLevel.val(), 10);
+
+ this.$repoSelects.each(function () {
+ var $this = $(this);
+ var repoSelectVal = parseInt($this.val(), 10);
+
+ $this.find('option').enable();
+
+ if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
+ $this.val(selectedVal).trigger('change');
+ highlightChanges($this);
+ }
- if (selectedVal) {
- this.$repoSelects.removeClass('disabled');
+ $this.find("option[value='" + selectedVal + "']").nextAll().disable();
+ });
- if ($lfsEnabledOption.length) {
- $lfsEnabledOption.removeClass('disabled');
- highlightChanges($lfsEnabledOption);
- }
- if (containerRegistry) {
- containerRegistry.style.display = '';
- }
- } else {
- this.$repoSelects.addClass('disabled');
+ if (selectedVal) {
+ this.$repoSelects.removeClass('disabled');
- if ($lfsEnabledOption.length) {
- $lfsEnabledOption.val('false').addClass('disabled');
- highlightChanges($lfsEnabledOption);
- }
- if (containerRegistry) {
- containerRegistry.style.display = 'none';
- containerRegistryCheckbox.checked = false;
- }
+ if ($lfsEnabledOption.length) {
+ $lfsEnabledOption.removeClass('disabled');
+ highlightChanges($lfsEnabledOption);
+ }
+ if (containerRegistry) {
+ containerRegistry.style.display = '';
}
+ } else {
+ this.$repoSelects.addClass('disabled');
- prevSelectedVal = selectedVal;
- }.bind(this));
- };
+ if ($lfsEnabledOption.length) {
+ $lfsEnabledOption.val('false').addClass('disabled');
+ highlightChanges($lfsEnabledOption);
+ }
+ if (containerRegistry) {
+ containerRegistry.style.display = 'none';
+ containerRegistryCheckbox.checked = false;
+ }
+ }
- return ProjectNew;
- })();
-}).call(window);
+ prevSelectedVal = selectedVal;
+ }.bind(this));
+ }
+}
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index fb01390f91c..07a49d1506c 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -2,75 +2,73 @@
import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
-(function() {
- this.ProjectSelect = (function() {
- function ProjectSelect() {
- $('.ajax-project-select').each(function(i, select) {
- var placeholder;
- this.groupId = $(select).data('group-id');
- this.includeGroups = $(select).data('include-groups');
- this.orderBy = $(select).data('order-by') || 'id';
- this.withIssuesEnabled = $(select).data('with-issues-enabled');
- this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
+export default function projectSelect() {
+ $('.ajax-project-select').each(function(i, select) {
+ var placeholder;
+ const simpleFilter = $(select).data('simple-filter') || false;
+ this.groupId = $(select).data('group-id');
+ this.includeGroups = $(select).data('include-groups');
+ this.allProjects = $(select).data('all-projects') || false;
+ this.orderBy = $(select).data('order-by') || 'id';
+ this.withIssuesEnabled = $(select).data('with-issues-enabled');
+ this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
- placeholder = "Search for project";
- if (this.includeGroups) {
- placeholder += " or group";
- }
+ placeholder = "Search for project";
+ if (this.includeGroups) {
+ placeholder += " or group";
+ }
- $(select).select2({
- placeholder: placeholder,
- minimumInputLength: 0,
- query: (function(_this) {
- return function(query) {
- var finalCallback, projectsCallback;
- finalCallback = function(projects) {
+ $(select).select2({
+ placeholder: placeholder,
+ minimumInputLength: 0,
+ query: (function (_this) {
+ return function (query) {
+ var finalCallback, projectsCallback;
+ finalCallback = function (projects) {
+ var data;
+ data = {
+ results: projects
+ };
+ return query.callback(data);
+ };
+ if (_this.includeGroups) {
+ projectsCallback = function (projects) {
+ var groupsCallback;
+ groupsCallback = function (groups) {
var data;
- data = {
- results: projects
- };
- return query.callback(data);
+ data = groups.concat(projects);
+ return finalCallback(data);
};
- if (_this.includeGroups) {
- projectsCallback = function(projects) {
- var groupsCallback;
- groupsCallback = function(groups) {
- var data;
- data = groups.concat(projects);
- return finalCallback(data);
- };
- return Api.groups(query.term, {}, groupsCallback);
- };
- } else {
- projectsCallback = finalCallback;
- }
- if (_this.groupId) {
- return Api.groupProjects(_this.groupId, query.term, projectsCallback);
- } else {
- return Api.projects(query.term, {
- order_by: _this.orderBy,
- with_issues_enabled: _this.withIssuesEnabled,
- with_merge_requests_enabled: _this.withMergeRequestsEnabled
- }, projectsCallback);
- }
+ return Api.groups(query.term, {}, groupsCallback);
};
- })(this),
- id: function(project) {
- return JSON.stringify({
- name: project.name,
- url: project.web_url,
- });
- },
- text: function(project) {
- return project.name_with_namespace || project.name;
- },
- dropdownCssClass: "ajax-project-dropdown"
+ } else {
+ projectsCallback = finalCallback;
+ }
+ if (_this.groupId) {
+ return Api.groupProjects(_this.groupId, query.term, projectsCallback);
+ } else {
+ return Api.projects(query.term, {
+ order_by: _this.orderBy,
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ membership: !_this.allProjects,
+ }, projectsCallback);
+ }
+ };
+ })(this),
+ id: function(project) {
+ if (simpleFilter) return project.id;
+ return JSON.stringify({
+ name: project.name,
+ url: project.web_url,
});
-
- return new ProjectSelectComboButton(select);
- });
- }
-
- return ProjectSelect;
- })();
-}).call(window);
+ },
+ text: function (project) {
+ return project.name_with_namespace || project.name;
+ },
+ dropdownCssClass: "ajax-project-dropdown"
+ });
+ if (simpleFilter) return select;
+ return new ProjectSelectComboButton(select);
+ });
+}
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
deleted file mode 100644
index 3a51c1f26ac..00000000000
--- a/app/assets/javascripts/project_show.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife */
-
-(function() {
- this.ProjectShow = (function() {
- function ProjectShow() {}
-
- return ProjectShow;
- })();
-}).call(window);
-
-// I kept class for future
diff --git a/app/assets/javascripts/project_variables.js b/app/assets/javascripts/project_variables.js
index 4ee2e49306d..567c311f119 100644
--- a/app/assets/javascripts/project_variables.js
+++ b/app/assets/javascripts/project_variables.js
@@ -1,43 +1,39 @@
-(() => {
- const HIDDEN_VALUE_TEXT = '******';
- class ProjectVariables {
- constructor() {
- this.$revealBtn = $('.js-btn-toggle-reveal-values');
- this.$revealBtn.on('click', this.toggleRevealState.bind(this));
- }
+const HIDDEN_VALUE_TEXT = '******';
+
+export default class ProjectVariables {
+ constructor() {
+ this.$revealBtn = $('.js-btn-toggle-reveal-values');
+ this.$revealBtn.on('click', this.toggleRevealState.bind(this));
+ }
- toggleRevealState(e) {
- e.preventDefault();
+ toggleRevealState(e) {
+ e.preventDefault();
- const oldStatus = this.$revealBtn.attr('data-status');
- let newStatus = 'hidden';
- let newAction = 'Reveal Values';
+ const oldStatus = this.$revealBtn.attr('data-status');
+ let newStatus = 'hidden';
+ let newAction = 'Reveal Values';
- if (oldStatus === 'hidden') {
- newStatus = 'revealed';
- newAction = 'Hide Values';
- }
+ if (oldStatus === 'hidden') {
+ newStatus = 'revealed';
+ newAction = 'Hide Values';
+ }
- this.$revealBtn.attr('data-status', newStatus);
+ this.$revealBtn.attr('data-status', newStatus);
- const $variables = $('.variable-value');
+ const $variables = $('.variable-value');
- $variables.each((_, variable) => {
- const $variable = $(variable);
- let newText = HIDDEN_VALUE_TEXT;
+ $variables.each((_, variable) => {
+ const $variable = $(variable);
+ let newText = HIDDEN_VALUE_TEXT;
- if (newStatus === 'revealed') {
- newText = $variable.attr('data-value');
- }
+ if (newStatus === 'revealed') {
+ newText = $variable.attr('data-value');
+ }
- $variable.text(newText);
- });
+ $variable.text(newText);
+ });
- this.$revealBtn.text(newAction);
- }
+ this.$revealBtn.text(newAction);
}
-
- window.gl = window.gl || {};
- window.gl.ProjectVariables = ProjectVariables;
-})();
+}
diff --git a/app/assets/javascripts/projects/ci_cd_settings_bundle.js b/app/assets/javascripts/projects/ci_cd_settings_bundle.js
new file mode 100644
index 00000000000..90e418f6771
--- /dev/null
+++ b/app/assets/javascripts/projects/ci_cd_settings_bundle.js
@@ -0,0 +1,19 @@
+function updateAutoDevopsRadios(radioWrappers) {
+ radioWrappers.forEach((radioWrapper) => {
+ const radio = radioWrapper.querySelector('.js-auto-devops-enable-radio');
+ const runPipelineCheckboxWrapper = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox-wrapper');
+ const runPipelineCheckbox = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox');
+
+ if (runPipelineCheckbox) {
+ runPipelineCheckbox.checked = radio.checked;
+ runPipelineCheckboxWrapper.classList.toggle('hide', !radio.checked);
+ }
+ });
+}
+
+export default function initCiCdSettings() {
+ const radioWrappers = document.querySelectorAll('.js-auto-devops-enable-radio-wrapper');
+ radioWrappers.forEach(radioWrapper =>
+ radioWrapper.addEventListener('change', () => updateAutoDevopsRadios(radioWrappers)),
+ );
+}
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 7f972b6f6ee..3ecc0c2a6e5 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -29,6 +29,12 @@ const bindEvents = () => {
const $newProjectForm = $('#new_project');
const $projectImportUrl = $('#project_import_url');
const $projectPath = $('#project_path');
+ const $useTemplateBtn = $('.template-button > input');
+ const $projectFieldsForm = $('.project-fields-form');
+ const $selectedTemplateText = $('.selected-template');
+ const $changeTemplateBtn = $('.change-template');
+ const $selectedIcon = $('.selected-icon svg');
+ const $templateProjectNameInput = $('#template-project-name #project_path');
if ($newProjectForm.length !== 1) {
return;
@@ -48,6 +54,40 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`);
});
+ function chooseTemplate() {
+ $('.template-option').hide();
+ $projectFieldsForm.addClass('selected');
+ $selectedIcon.removeClass('active');
+ const value = $(this).val();
+ const templates = {
+ rails: {
+ text: 'Ruby on Rails',
+ icon: '.selected-icon .icon-rails',
+ },
+ express: {
+ text: 'NodeJS Express',
+ icon: '.selected-icon .icon-node-express',
+ },
+ spring: {
+ text: 'Spring',
+ icon: '.selected-icon .icon-java-spring',
+ },
+ };
+
+ const selectedTemplate = templates[value];
+ $selectedTemplateText.text(selectedTemplate.text);
+ $(selectedTemplate.icon).addClass('active');
+ $templateProjectNameInput.focus();
+ }
+
+ $useTemplateBtn.on('change', chooseTemplate);
+
+ $changeTemplateBtn.on('click', () => {
+ $('.template-option').show();
+ $projectFieldsForm.removeClass('selected');
+ $useTemplateBtn.prop('checked', false);
+ });
+
$newProjectForm.on('submit', () => {
$projectPath.val($projectPath.val().trim());
});
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
index fe5179de206..d482a7025de 100644
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
+++ b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
@@ -48,6 +48,27 @@ export default {
}
return this.projectName;
},
+ /**
+ * Smartly truncates project namespace by doing two things;
+ * 1. Only include Group names in path by removing project name
+ * 2. Only include first and last group names in the path
+ * when namespace has more than 2 groups present
+ *
+ * First part (removal of project name from namespace) can be
+ * done from backend but doing so involves migration of
+ * existing project namespaces which is not wise thing to do.
+ */
+ truncatedNamespace() {
+ const namespaceArr = this.namespace.split(' / ');
+ namespaceArr.splice(-1, 1);
+ let namespace = namespaceArr.join(' / ');
+
+ if (namespaceArr.length > 2) {
+ namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`;
+ }
+
+ return namespace;
+ },
},
};
</script>
@@ -87,9 +108,7 @@ export default {
<div
class="project-namespace"
:title="namespace"
- >
- {{namespace}}
- </div>
+ >{{truncatedNamespace}}</div>
</div>
</a>
</li>
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index a4d50a52315..55c93923cc8 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -81,7 +81,11 @@ export default class PrometheusMetrics {
loadActiveMetrics() {
this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
backOff((next, stop) => {
- $.getJSON(this.activeMetricsEndpoint)
+ $.ajax({
+ url: this.activeMetricsEndpoint,
+ dataType: 'json',
+ global: false,
+ })
.done((res) => {
if (res && res.success) {
stop(res);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 3b920942a3f..632625da8e7 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -1,6 +1,5 @@
/* eslint-disable no-new */
-/* global Flash */
-
+import Flash from '../flash';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
export default class ProtectedBranchEdit {
@@ -57,7 +56,7 @@ export default class ProtectedBranchEdit {
},
},
error() {
- new Flash('Failed to update branch!', null, $('.js-protected-branches-list'));
+ new Flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list'));
},
}).always(() => {
this.$allowedToMergeDropdown.enable();
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index 09a387c0f9e..dad0ad25b65 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -1,6 +1,5 @@
/* eslint-disable no-new */
-/* global Flash */
-
+import Flash from '../flash';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit {
@@ -43,7 +42,7 @@ export default class ProtectedTagEdit {
},
},
error() {
- new Flash('Failed to update tag!', null, $('.js-protected-tags-list'));
+ new Flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
},
}).always(() => {
this.$allowedToCreateDropdownButton.enable();
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index 41ea9742406..ac1c3ec253c 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -57,7 +57,7 @@
},
showError(message) {
- Flash((errorMessages[message]));
+ Flash(errorMessages[message]);
},
},
};
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index 4ce1571b0aa..14d43e135fe 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -8,6 +8,7 @@
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants';
+ import { numberToHumanSize } from '../../lib/utils/number_utils';
export default {
props: {
@@ -41,6 +42,10 @@
return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
},
+ formatSize(size) {
+ return numberToHumanSize(size);
+ },
+
handleDeleteRegistry(registry) {
this.deleteRegistry(registry)
.then(() => this.fetchList({ repo: this.repo }))
@@ -57,7 +62,7 @@
},
showError(message) {
- Flash((errorMessages[message]));
+ Flash(errorMessages[message]);
},
},
};
@@ -97,7 +102,7 @@
</span>
</td>
<td>
- {{item.size}}
+ {{formatSize(item.size)}}
<template v-if="item.size && item.layers">
&middot;
</template>
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index 34ed40b8b65..795b39bb3dc 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -29,11 +29,9 @@ export const fetchList = ({ commit }, { repo, page }) => {
});
};
-export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath)
- .then(res => res.json());
+export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath);
-export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath)
- .then(res => res.json());
+export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath);
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js
index e40382e7afc..208c3c39866 100644
--- a/app/assets/javascripts/registry/stores/mutations.js
+++ b/app/assets/javascripts/registry/stores/mutations.js
@@ -38,7 +38,7 @@ export default {
tag: element.name,
revision: element.revision,
shortRevision: element.short_revision,
- size: element.size,
+ size: element.total_size,
layers: element.layers,
location: element.location,
createdAt: element.created_at,
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
index bcdc0fd67b8..c91a0d9ba41 100644
--- a/app/assets/javascripts/render_gfm.js
+++ b/app/assets/javascripts/render_gfm.js
@@ -1,15 +1,15 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */
+import renderMath from './render_math';
+import renderMermaid from './render_mermaid';
// Render Gitlab flavoured Markdown
//
-// Delegates to syntax highlight and render math
+// Delegates to syntax highlight and render math & mermaid diagrams.
//
-(function() {
- $.fn.renderGFM = function() {
- this.find('.js-syntax-highlight').syntaxHighlight();
- this.find('.js-render-math').renderMath();
- return this;
- };
+$.fn.renderGFM = function renderGFM() {
+ this.find('.js-syntax-highlight').syntaxHighlight();
+ renderMath(this.find('.js-render-math'));
+ renderMermaid(this.find('.js-render-mermaid'));
+ return this;
+};
- $(() => $('body').renderGFM());
-}).call(window);
+$(() => $('body').renderGFM());
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
index 8b3fee49cb9..a759992cd54 100644
--- a/app/assets/javascripts/render_math.js
+++ b/app/assets/javascripts/render_math.js
@@ -1,4 +1,3 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len, no-console */
/* global katex */
// Renders math using KaTeX in any element with the
@@ -8,49 +7,45 @@
//
// <code class="js-render-math"></div>
//
-(function() {
// Only load once
- var katexLoaded = false;
+let katexLoaded = false;
- // Loop over all math elements and render math
- var renderWithKaTeX = function (elements) {
- elements.each(function () {
- var mathNode = $('<span></span>');
- var $this = $(this);
+// Loop over all math elements and render math
+function renderWithKaTeX(elements) {
+ elements.each(function katexElementsLoop() {
+ const mathNode = $('<span></span>');
+ const $this = $(this);
- var display = $this.attr('data-math-style') === 'display';
- try {
- katex.render($this.text(), mathNode.get(0), { displayMode: display });
- mathNode.insertAfter($this);
- $this.remove();
- } catch (err) {
- // What can we do??
- console.log(err.message);
- }
- });
- };
+ const display = $this.attr('data-math-style') === 'display';
+ try {
+ katex.render($this.text(), mathNode.get(0), { displayMode: display });
+ mathNode.insertAfter($this);
+ $this.remove();
+ } catch (err) {
+ throw err;
+ }
+ });
+}
- $.fn.renderMath = function() {
- var $this = this;
- if ($this.length === 0) return;
+export default function renderMath($els) {
+ if (!$els.length) return;
- if (katexLoaded) renderWithKaTeX($this);
- else {
- // Request CSS file so it is in the cache
- $.get(gon.katex_css_url, function() {
- var css = $('<link>',
- { rel: 'stylesheet',
- type: 'text/css',
- href: gon.katex_css_url,
- });
- css.appendTo('head');
+ if (katexLoaded) {
+ renderWithKaTeX($els);
+ } else {
+ $.get(gon.katex_css_url, () => {
+ const css = $('<link>', {
+ rel: 'stylesheet',
+ type: 'text/css',
+ href: gon.katex_css_url,
+ });
+ css.appendTo('head');
- // Load KaTeX js
- $.getScript(gon.katex_js_url, function() {
- katexLoaded = true;
- renderWithKaTeX($this); // Run KaTeX
- });
+ // Load KaTeX js
+ $.getScript(gon.katex_js_url, () => {
+ katexLoaded = true;
+ renderWithKaTeX($els); // Run KaTeX
});
- }
- };
-}).call(window);
+ });
+ }
+}
diff --git a/app/assets/javascripts/render_mermaid.js b/app/assets/javascripts/render_mermaid.js
new file mode 100644
index 00000000000..41942c04a4e
--- /dev/null
+++ b/app/assets/javascripts/render_mermaid.js
@@ -0,0 +1,32 @@
+// Renders diagrams and flowcharts from text using Mermaid in any element with the
+// `js-render-mermaid` class.
+//
+// Example markup:
+//
+// <pre class="js-render-mermaid">
+// graph TD;
+// A-- > B;
+// A-- > C;
+// B-- > D;
+// C-- > D;
+// </pre>
+//
+
+import Flash from './flash';
+
+export default function renderMermaid($els) {
+ if (!$els.length) return;
+
+ import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => {
+ mermaid.initialize({
+ loadOnStart: false,
+ theme: 'neutral',
+ });
+
+ $els.each((i, el) => {
+ mermaid.init(undefined, el);
+ });
+ }).catch((err) => {
+ Flash(`Can't load mermaid module: ${err}`);
+ });
+}
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list.vue b/app/assets/javascripts/repo/components/commit_sidebar/list.vue
new file mode 100644
index 00000000000..fb862e7bf01
--- /dev/null
+++ b/app/assets/javascripts/repo/components/commit_sidebar/list.vue
@@ -0,0 +1,89 @@
+<script>
+ import icon from '../../../vue_shared/components/icon.vue';
+ import listItem from './list_item.vue';
+ import listCollapsed from './list_collapsed.vue';
+
+ export default {
+ components: {
+ icon,
+ listItem,
+ listCollapsed,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ fileList: {
+ type: Array,
+ required: true,
+ },
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ methods: {
+ toggleCollapsed() {
+ this.$emit('toggleCollapsed');
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="multi-file-commit-panel-section">
+ <header
+ class="multi-file-commit-panel-header"
+ :class="{
+ 'is-collapsed': collapsed,
+ }"
+ >
+ <icon
+ name="list-bulleted"
+ :size="18"
+ css-classes="append-right-default"
+ />
+ <template v-if="!collapsed">
+ {{ title }}
+ <button
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn"
+ @click="toggleCollapsed"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-angle-double-right"
+ >
+ </i>
+ </button>
+ </template>
+ </header>
+ <div class="multi-file-commit-list">
+ <list-collapsed
+ v-if="collapsed"
+ />
+ <template v-else>
+ <ul
+ v-if="fileList.length"
+ class="list-unstyled append-bottom-0"
+ >
+ <li
+ v-for="file in fileList"
+ :key="file.key"
+ >
+ <list-item
+ :file="file"
+ />
+ </li>
+ </ul>
+ <div
+ v-else
+ class="help-block prepend-top-0"
+ >
+ No changes
+ </div>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue
new file mode 100644
index 00000000000..6a0262f271b
--- /dev/null
+++ b/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue
@@ -0,0 +1,35 @@
+<script>
+ import { mapGetters } from 'vuex';
+ import icon from '../../../vue_shared/components/icon.vue';
+
+ export default {
+ components: {
+ icon,
+ },
+ computed: {
+ ...mapGetters([
+ 'addedFiles',
+ 'modifiedFiles',
+ ]),
+ },
+ };
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-list-collapsed text-center"
+ >
+ <icon
+ name="file-addition"
+ :size="18"
+ css-classes="multi-file-addition append-bottom-10"
+ />
+ {{ addedFiles.length }}
+ <icon
+ name="file-modified"
+ :size="18"
+ css-classes="multi-file-modified prepend-top-10 append-bottom-10"
+ />
+ {{ modifiedFiles.length }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue b/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue
new file mode 100644
index 00000000000..742f746e02f
--- /dev/null
+++ b/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue
@@ -0,0 +1,36 @@
+<script>
+ import icon from '../../../vue_shared/components/icon.vue';
+
+ export default {
+ components: {
+ icon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ iconName() {
+ return this.file.tempFile ? 'file-addition' : 'file-modified';
+ },
+ iconClass() {
+ return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="multi-file-commit-list-item">
+ <icon
+ :name="iconName"
+ :size="16"
+ :css-classes="iconClass"
+ />
+ <span class="multi-file-commit-list-path">
+ {{ file.path }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/new_branch_form.vue b/app/assets/javascripts/repo/components/new_branch_form.vue
new file mode 100644
index 00000000000..ba7090e4a9d
--- /dev/null
+++ b/app/assets/javascripts/repo/components/new_branch_form.vue
@@ -0,0 +1,108 @@
+<script>
+ import { mapState, mapActions } from 'vuex';
+ import flash, { hideFlash } from '../../flash';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+ export default {
+ components: {
+ loadingIcon,
+ },
+ data() {
+ return {
+ branchName: '',
+ loading: false,
+ };
+ },
+ computed: {
+ ...mapState([
+ 'currentBranch',
+ ]),
+ btnDisabled() {
+ return this.loading || this.branchName === '';
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'createNewBranch',
+ ]),
+ toggleDropdown() {
+ this.$dropdown.dropdown('toggle');
+ },
+ submitNewBranch() {
+ // need to query as the element is appended outside of Vue
+ const flashEl = this.$refs.flashContainer.querySelector('.flash-alert');
+
+ this.loading = true;
+
+ if (flashEl) {
+ hideFlash(flashEl, false);
+ }
+
+ this.createNewBranch(this.branchName)
+ .then(() => {
+ this.loading = false;
+ this.branchName = '';
+
+ if (this.dropdownText) {
+ this.dropdownText.textContent = this.currentBranch;
+ }
+
+ this.toggleDropdown();
+ })
+ .catch(res => res.json().then((data) => {
+ this.loading = false;
+ flash(data.message, 'alert', this.$el);
+ }));
+ },
+ },
+ created() {
+ // Dropdown is outside of Vue instance & is controlled by Bootstrap
+ this.$dropdown = $('.git-revision-dropdown');
+
+ // text element is outside Vue app
+ this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
+ },
+ };
+</script>
+
+<template>
+ <div>
+ <div
+ class="flash-container"
+ ref="flashContainer"
+ >
+ </div>
+ <p>
+ Create from:
+ <code>{{ currentBranch }}</code>
+ </p>
+ <input
+ class="form-control js-new-branch-name"
+ type="text"
+ placeholder="Name new branch"
+ v-model="branchName"
+ @keyup.enter.stop.prevent="submitNewBranch"
+ />
+ <div class="prepend-top-default clearfix">
+ <button
+ type="button"
+ class="btn btn-primary pull-left"
+ :disabled="btnDisabled"
+ @click.stop.prevent="submitNewBranch"
+ >
+ <loading-icon
+ v-if="loading"
+ :inline="true"
+ />
+ <span>Create</span>
+ </button>
+ <button
+ type="button"
+ class="btn btn-default pull-right"
+ @click.stop.prevent="toggleDropdown"
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue
new file mode 100644
index 00000000000..a5ee4f71281
--- /dev/null
+++ b/app/assets/javascripts/repo/components/new_dropdown/index.vue
@@ -0,0 +1,84 @@
+<script>
+ import { mapState } from 'vuex';
+ import newModal from './modal.vue';
+ import upload from './upload.vue';
+
+ export default {
+ components: {
+ newModal,
+ upload,
+ },
+ data() {
+ return {
+ openModal: false,
+ modalType: '',
+ };
+ },
+ computed: {
+ ...mapState([
+ 'path',
+ ]),
+ },
+ methods: {
+ createNewItem(type) {
+ this.modalType = type;
+ this.toggleModalOpen();
+ },
+ toggleModalOpen() {
+ this.openModal = !this.openModal;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div>
+ <ul class="breadcrumb repo-breadcrumb">
+ <li class="dropdown">
+ <button
+ type="button"
+ class="btn btn-default dropdown-toggle add-to-tree"
+ data-toggle="dropdown"
+ aria-label="Create new file or directory"
+ >
+ <i
+ class="fa fa-plus"
+ aria-hidden="true"
+ >
+ </i>
+ </button>
+ <ul class="dropdown-menu">
+ <li>
+ <a
+ href="#"
+ role="button"
+ @click.prevent="createNewItem('blob')"
+ >
+ {{ __('New file') }}
+ </a>
+ </li>
+ <li>
+ <upload
+ :path="path"
+ />
+ </li>
+ <li>
+ <a
+ href="#"
+ role="button"
+ @click.prevent="createNewItem('tree')"
+ >
+ {{ __('New directory') }}
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ <new-modal
+ v-if="openModal"
+ :type="modalType"
+ :path="path"
+ @toggle="toggleModalOpen"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/repo/components/new_dropdown/modal.vue
new file mode 100644
index 00000000000..ac1f613bb71
--- /dev/null
+++ b/app/assets/javascripts/repo/components/new_dropdown/modal.vue
@@ -0,0 +1,98 @@
+<script>
+ import { mapActions } from 'vuex';
+ import { __ } from '../../../locale';
+ import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
+
+ export default {
+ props: {
+ type: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ entryName: this.path !== '' ? `${this.path}/` : '',
+ };
+ },
+ components: {
+ popupDialog,
+ },
+ methods: {
+ ...mapActions([
+ 'createTempEntry',
+ ]),
+ createEntryInStore() {
+ this.createTempEntry({
+ name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
+ type: this.type,
+ });
+
+ this.toggleModalOpen();
+ },
+ toggleModalOpen() {
+ this.$emit('toggle');
+ },
+ },
+ computed: {
+ modalTitle() {
+ if (this.type === 'tree') {
+ return __('Create new directory');
+ }
+
+ return __('Create new file');
+ },
+ buttonLabel() {
+ if (this.type === 'tree') {
+ return __('Create directory');
+ }
+
+ return __('Create file');
+ },
+ formLabelName() {
+ if (this.type === 'tree') {
+ return __('Directory name');
+ }
+
+ return __('File name');
+ },
+ },
+ mounted() {
+ this.$refs.fieldName.focus();
+ },
+ };
+</script>
+
+<template>
+ <popup-dialog
+ :title="modalTitle"
+ :primary-button-label="buttonLabel"
+ kind="success"
+ @toggle="toggleModalOpen"
+ @submit="createEntryInStore"
+ >
+ <form
+ class="form-horizontal"
+ slot="body"
+ @submit.prevent="createEntryInStore"
+ >
+ <fieldset class="form-group append-bottom-0">
+ <label class="label-light col-sm-3">
+ {{ formLabelName }}
+ </label>
+ <div class="col-sm-9">
+ <input
+ type="text"
+ class="form-control"
+ v-model="entryName"
+ ref="fieldName"
+ />
+ </div>
+ </fieldset>
+ </form>
+ </popup-dialog>
+</template>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/upload.vue b/app/assets/javascripts/repo/components/new_dropdown/upload.vue
new file mode 100644
index 00000000000..14ad32f4ae0
--- /dev/null
+++ b/app/assets/javascripts/repo/components/new_dropdown/upload.vue
@@ -0,0 +1,68 @@
+<script>
+ import { mapActions } from 'vuex';
+
+ export default {
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'createTempEntry',
+ ]),
+ createFile(target, file, isText) {
+ const { name } = file;
+ let { result } = target;
+
+ if (!isText) {
+ result = result.split('base64,')[1];
+ }
+
+ this.createTempEntry({
+ name,
+ type: 'blob',
+ content: result,
+ base64: !isText,
+ });
+ },
+ readFile(file) {
+ const reader = new FileReader();
+ const isText = file.type.match(/text.*/) !== null;
+
+ reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
+
+ if (isText) {
+ reader.readAsText(file);
+ } else {
+ reader.readAsDataURL(file);
+ }
+ },
+ openFile() {
+ Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
+ },
+ },
+ mounted() {
+ this.$refs.fileUpload.addEventListener('change', this.openFile);
+ },
+ beforeDestroy() {
+ this.$refs.fileUpload.removeEventListener('change', this.openFile);
+ },
+ };
+</script>
+
+<template>
+ <label
+ role="button"
+ class="menu-item"
+ >
+ {{ __('Upload file') }}
+ <input
+ id="file-upload"
+ type="file"
+ class="hidden"
+ ref="fileUpload"
+ />
+ </label>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
index cc60aa5939c..a00e1e9d809 100644
--- a/app/assets/javascripts/repo/components/repo.vue
+++ b/app/assets/javascripts/repo/components/repo.vue
@@ -1,70 +1,63 @@
<script>
+import { mapState, mapGetters } from 'vuex';
import RepoSidebar from './repo_sidebar.vue';
import RepoCommitSection from './repo_commit_section.vue';
import RepoTabs from './repo_tabs.vue';
import RepoFileButtons from './repo_file_buttons.vue';
import RepoPreview from './repo_preview.vue';
-import RepoMixin from '../mixins/repo_mixin';
-import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
-import Store from '../stores/repo_store';
-import Helper from '../helpers/repo_helper';
-import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
+import repoEditor from './repo_editor.vue';
export default {
- data: () => Store,
- mixins: [RepoMixin],
+ computed: {
+ ...mapState([
+ 'currentBlobView',
+ ]),
+ ...mapGetters([
+ 'isCollapsed',
+ 'changedFiles',
+ ]),
+ },
components: {
RepoSidebar,
RepoTabs,
RepoFileButtons,
- 'repo-editor': MonacoLoaderHelper.repoEditorLoader,
+ repoEditor,
RepoCommitSection,
- PopupDialog,
RepoPreview,
},
-
mounted() {
- Helper.getContent().catch(Helper.loadingError);
- },
-
- methods: {
- toggleDialogOpen(toggle) {
- this.dialog.open = toggle;
- },
-
- dialogSubmitted(status) {
- this.toggleDialogOpen(false);
- this.dialog.status = status;
- },
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = (e) => {
+ if (!this.changedFiles.length) return undefined;
- toggleBlobView: Store.toggleBlobView,
+ Object.assign(e, {
+ returnValue,
+ });
+ return returnValue;
+ };
},
};
</script>
<template>
- <div class="repository-view">
- <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isMini}">
- <repo-sidebar/>
- <div v-if="isMini"
- class="panel-right"
- :class="{'edit-mode': editMode}">
- <repo-tabs/>
- <component
- :is="currentBlobView"
- class="blob-viewer-container"/>
- <repo-file-buttons/>
- </div>
+ <div
+ class="multi-file"
+ :class="{
+ 'is-collapsed': isCollapsed
+ }"
+ >
+ <repo-sidebar/>
+ <div
+ v-if="isCollapsed"
+ class="multi-file-edit-pane"
+ >
+ <repo-tabs />
+ <component
+ class="multi-file-edit-pane-content"
+ :is="currentBlobView"
+ />
+ <repo-file-buttons />
</div>
- <repo-commit-section/>
- <popup-dialog
- v-show="dialog.open"
- :primary-button-label="__('Discard changes')"
- kind="warning"
- :title="__('Are you sure?')"
- :text="__('Are you sure you want to discard your changes?')"
- @toggle="toggleDialogOpen"
- @submit="dialogSubmitted"
- />
+ <repo-commit-section />
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue
index 119e38c583d..d3344d0c8dc 100644
--- a/app/assets/javascripts/repo/components/repo_commit_section.vue
+++ b/app/assets/javascripts/repo/components/repo_commit_section.vue
@@ -1,58 +1,92 @@
<script>
-/* global Flash */
-import Store from '../stores/repo_store';
-import RepoMixin from '../mixins/repo_mixin';
-import Service from '../services/repo_service';
+import { mapGetters, mapState, mapActions } from 'vuex';
+import tooltip from '../../vue_shared/directives/tooltip';
+import icon from '../../vue_shared/components/icon.vue';
+import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
+import commitFilesList from './commit_sidebar/list.vue';
export default {
- data: () => Store,
-
- mixins: [RepoMixin],
-
+ components: {
+ PopupDialog,
+ icon,
+ commitFilesList,
+ },
+ directives: {
+ tooltip,
+ },
+ data() {
+ return {
+ showNewBranchDialog: false,
+ submitCommitsLoading: false,
+ startNewMR: false,
+ commitMessage: '',
+ collapsed: true,
+ };
+ },
computed: {
- showCommitable() {
- return this.isCommitable && this.changedFiles.length;
- },
-
- branchPaths() {
- return this.changedFiles.map(f => f.path);
- },
-
- cantCommitYet() {
- return !this.commitMessage || this.submitCommitsLoading;
+ ...mapState([
+ 'currentBranch',
+ ]),
+ ...mapGetters([
+ 'changedFiles',
+ ]),
+ commitButtonDisabled() {
+ return this.commitMessage === '' || this.submitCommitsLoading || !this.changedFiles.length;
},
-
- filePluralize() {
- return this.changedFiles.length > 1 ? 'files' : 'file';
+ commitMessageCount() {
+ return this.commitMessage.length;
},
},
-
methods: {
- makeCommit() {
- // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
- const commitMessage = this.commitMessage;
- const actions = this.changedFiles.map(f => ({
- action: 'update',
- file_path: f.path,
- content: f.newContent,
- }));
+ ...mapActions([
+ 'checkCommitStatus',
+ 'commitChanges',
+ 'getTreeData',
+ ]),
+ makeCommit(newBranch = false) {
+ const createNewBranch = newBranch || this.startNewMR;
+
const payload = {
- branch: Store.currentBranch,
- commit_message: commitMessage,
- actions,
+ branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch,
+ commit_message: this.commitMessage,
+ actions: this.changedFiles.map(f => ({
+ action: f.tempFile ? 'create' : 'update',
+ file_path: f.path,
+ content: f.content,
+ encoding: f.base64 ? 'base64' : 'text',
+ })),
+ start_branch: createNewBranch ? this.currentBranch : undefined,
};
- Store.submitCommitsLoading = true;
- Service.commitFiles(payload)
- .then(this.resetCommitState)
- .catch(() => Flash('An error occurred while committing your changes'));
+
+ this.showNewBranchDialog = false;
+ this.submitCommitsLoading = true;
+
+ this.commitChanges({ payload, newMr: this.startNewMR })
+ .then(() => {
+ this.submitCommitsLoading = false;
+ this.getTreeData();
+ })
+ .catch(() => {
+ this.submitCommitsLoading = false;
+ });
},
+ tryCommit() {
+ this.submitCommitsLoading = true;
- resetCommitState() {
- this.submitCommitsLoading = false;
- this.changedFiles = [];
- this.commitMessage = '';
- this.editMode = false;
- window.scrollTo(0, 0);
+ this.checkCommitStatus()
+ .then((branchChanged) => {
+ if (branchChanged) {
+ this.showNewBranchDialog = true;
+ } else {
+ this.makeCommit();
+ }
+ })
+ .catch(() => {
+ this.submitCommitsLoading = false;
+ });
+ },
+ toggleCollapsed() {
+ this.collapsed = !this.collapsed;
},
},
};
@@ -60,73 +94,85 @@ export default {
<template>
<div
- v-if="showCommitable"
- id="commit-area">
+ class="multi-file-commit-panel"
+ :class="{
+ 'is-collapsed': collapsed,
+ }"
+>
+ <popup-dialog
+ v-if="showNewBranchDialog"
+ :primary-button-label="__('Create new branch')"
+ kind="primary"
+ :title="__('Branch has changed')"
+ :text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
+ @toggle="showNewBranchDialog = false"
+ @submit="makeCommit(true)"
+ />
+ <button
+ v-if="collapsed"
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn is-collapsed prepend-top-10 append-bottom-10"
+ @click="toggleCollapsed"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-angle-double-left"
+ >
+ </i>
+ </button>
+ <commit-files-list
+ title="Staged"
+ :file-list="changedFiles"
+ :collapsed="collapsed"
+ @toggleCollapsed="toggleCollapsed"
+ />
<form
- class="form-horizontal"
- @submit.prevent="makeCommit">
- <fieldset>
- <div class="form-group">
- <label class="col-md-4 control-label staged-files">
- Staged files ({{changedFiles.length}})
- </label>
- <div class="col-md-6">
- <ul class="list-unstyled changed-files">
- <li
- v-for="branchPath in branchPaths"
- :key="branchPath">
- <span class="help-block">
- {{branchPath}}
- </span>
- </li>
- </ul>
- </div>
- </div>
- <div class="form-group">
- <label
- class="col-md-4 control-label"
- for="commit-message">
- Commit message
- </label>
- <div class="col-md-6">
- <textarea
- id="commit-message"
- class="form-control"
- name="commit-message"
- v-model="commitMessage">
- </textarea>
- </div>
- </div>
- <div class="form-group target-branch">
- <label
- class="col-md-4 control-label"
- for="target-branch">
- Target branch
- </label>
- <div class="col-md-6">
- <span class="help-block">
- {{currentBranch}}
- </span>
- </div>
- </div>
- <div class="col-md-offset-4 col-md-6">
- <button
- ref="submitCommit"
- type="submit"
- :disabled="cantCommitYet"
- class="btn btn-success">
- <i
- v-if="submitCommitsLoading"
- class="fa fa-spinner fa-spin"
- aria-hidden="true"
- aria-label="loading">
- </i>
- <span class="commit-summary">
- Commit {{changedFiles.length}} {{filePluralize}}
- </span>
- </button>
+ class="form-horizontal multi-file-commit-form"
+ @submit.prevent="tryCommit"
+ v-if="!collapsed"
+ >
+ <div class="multi-file-commit-fieldset">
+ <textarea
+ class="form-control multi-file-commit-message"
+ name="commit-message"
+ v-model="commitMessage"
+ placeholder="Commit message"
+ >
+ </textarea>
+ </div>
+ <div class="multi-file-commit-fieldset">
+ <label
+ v-tooltip
+ title="Create a new merge request with these changes"
+ data-container="body"
+ data-placement="top"
+ >
+ <input
+ type="checkbox"
+ v-model="startNewMR"
+ />
+ Merge Request
+ </label>
+ <button
+ type="submit"
+ :disabled="commitButtonDisabled"
+ class="btn btn-default btn-sm append-right-10 prepend-left-10"
+ >
+ <i
+ v-if="submitCommitsLoading"
+ class="js-commit-loading-icon fa fa-spinner fa-spin"
+ aria-hidden="true"
+ aria-label="loading"
+ >
+ </i>
+ Commit
+ </button>
+ <div
+ class="multi-file-commit-message-count"
+ >
+ {{ commitMessageCount }}
</div>
- </fieldset>
+ </div>
</form>
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue
index 353142edeb7..6c1bb4b8566 100644
--- a/app/assets/javascripts/repo/components/repo_edit_button.vue
+++ b/app/assets/javascripts/repo/components/repo_edit_button.vue
@@ -1,48 +1,57 @@
<script>
-import Store from '../stores/repo_store';
-import RepoMixin from '../mixins/repo_mixin';
+import { mapGetters, mapActions, mapState } from 'vuex';
+import popupDialog from '../../vue_shared/components/popup_dialog.vue';
export default {
- data: () => Store,
- mixins: [RepoMixin],
+ components: {
+ popupDialog,
+ },
computed: {
+ ...mapState([
+ 'editMode',
+ 'discardPopupOpen',
+ ]),
+ ...mapGetters([
+ 'canEditFile',
+ ]),
buttonLabel() {
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
},
-
- showButton() {
- return this.isCommitable &&
- !this.activeFile.render_error &&
- !this.binary &&
- this.openedFiles.length;
- },
},
methods: {
- editCancelClicked() {
- if (this.changedFiles.length) {
- this.dialog.open = true;
- return;
- }
- this.editMode = !this.editMode;
- Store.toggleBlobView();
- },
+ ...mapActions([
+ 'toggleEditMode',
+ 'closeDiscardPopup',
+ ]),
},
};
</script>
<template>
-<button
- v-if="showButton"
- class="btn btn-default"
- type="button"
- @click.prevent="editCancelClicked">
- <i
- v-if="!editMode"
- class="fa fa-pencil"
- aria-hidden="true">
- </i>
- <span>
- {{buttonLabel}}
- </span>
-</button>
+ <div class="editable-mode">
+ <button
+ v-if="canEditFile"
+ class="btn btn-default"
+ type="button"
+ @click.prevent="toggleEditMode()">
+ <i
+ v-if="!editMode"
+ class="fa fa-pencil"
+ aria-hidden="true">
+ </i>
+ <span>
+ {{buttonLabel}}
+ </span>
+ </button>
+ <popup-dialog
+ v-if="discardPopupOpen"
+ class="text-left"
+ :primary-button-label="__('Discard changes')"
+ kind="warning"
+ :title="__('Are you sure?')"
+ :text="__('Are you sure you want to discard your changes?')"
+ @toggle="closeDiscardPopup"
+ @submit="toggleEditMode(true)"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue
index 02d9c775046..f37cbd1e961 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/repo/components/repo_editor.vue
@@ -1,121 +1,89 @@
<script>
/* global monaco */
-import Store from '../stores/repo_store';
-import Service from '../services/repo_service';
-import Helper from '../helpers/repo_helper';
-
-const RepoEditor = {
- data: () => Store,
-
- destroyed() {
- if (Helper.monacoInstance) {
- Helper.monacoInstance.destroy();
- }
+import { mapGetters, mapActions } from 'vuex';
+import flash from '../../flash';
+import monacoLoader from '../monaco_loader';
+import Editor from '../lib/editor';
+
+export default {
+ beforeDestroy() {
+ this.editor.dispose();
},
-
mounted() {
- Service.getRaw(this.activeFile.raw_path)
- .then((rawResponse) => {
- Store.blobRaw = rawResponse.data;
- Store.activeFile.plain = rawResponse.data;
-
- const monacoInstance = Helper.monaco.editor.create(this.$el, {
- model: null,
- readOnly: false,
- contextmenu: false,
- });
-
- Helper.monacoInstance = monacoInstance;
-
- this.addMonacoEvents();
-
- this.setupEditor();
- })
- .catch(Helper.loadingError);
+ if (this.editor && monaco) {
+ this.initMonaco();
+ } else {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ this.editor = Editor.create(monaco);
+
+ this.initMonaco();
+ });
+ }
},
-
methods: {
- setupEditor() {
- this.showHide();
-
- Helper.setMonacoModelFromLanguage();
- },
-
- showHide() {
- if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
- this.$el.style.display = 'none';
- } else {
- this.$el.style.display = 'inline-block';
- }
- },
-
- addMonacoEvents() {
- Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
- Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
+ ...mapActions([
+ 'getRawFileData',
+ 'changeFileContent',
+ ]),
+ initMonaco() {
+ if (this.shouldHideEditor) return;
+
+ this.editor.clearEditor();
+
+ this.getRawFileData(this.activeFile)
+ .then(() => {
+ this.editor.createInstance(this.$refs.editor);
+ })
+ .then(() => this.setupEditor())
+ .catch(() => flash('Error setting up monaco. Please try again.'));
},
+ setupEditor() {
+ if (!this.activeFile) return;
- onMonacoEditorKeysPressed() {
- Store.setActiveFileContents(Helper.monacoInstance.getValue());
- },
+ const model = this.editor.createModel(this.activeFile);
- onMonacoEditorMouseUp(e) {
- if (!e.target.position) return;
- const lineNumber = e.target.position.lineNumber;
- if (e.target.element.classList.contains('line-numbers')) {
- location.hash = `L${lineNumber}`;
- Store.setActiveLine(lineNumber);
- }
+ this.editor.attachModel(model);
+ model.onChange((m) => {
+ this.changeFileContent({
+ file: this.activeFile,
+ content: m.getValue(),
+ });
+ });
},
},
-
watch: {
- dialog: {
- handler(obj) {
- const newObj = obj;
- if (newObj.status) {
- newObj.status = false;
- this.openedFiles = this.openedFiles.map((file) => {
- const f = file;
- if (f.active) {
- this.blobRaw = f.plain;
- }
- f.changed = false;
- delete f.newContent;
-
- return f;
- });
- this.editMode = false;
- Store.toggleBlobView();
- }
- },
- deep: true,
- },
-
- blobRaw() {
- if (Helper.monacoInstance && !this.isTree) {
- this.setupEditor();
- }
- },
-
- activeLine() {
- if (Helper.monacoInstance) {
- Helper.monacoInstance.setPosition({
- lineNumber: this.activeLine,
- column: 1,
- });
+ activeFile(oldVal, newVal) {
+ if (newVal && !newVal.active) {
+ this.initMonaco();
}
},
},
computed: {
+ ...mapGetters([
+ 'activeFile',
+ 'activeFileExtension',
+ ]),
shouldHideEditor() {
- return !this.openedFiles.length || (this.binary && !this.activeFile.raw);
+ return this.activeFile.binary && !this.activeFile.raw;
},
},
};
-
-export default RepoEditor;
</script>
<template>
-<div id="ide" v-if='!shouldHideEditor'></div>
+ <div
+ id="ide"
+ class="blob-viewer-container blob-editor-container"
+ >
+ <div
+ v-show="shouldHideEditor"
+ v-html="activeFile.html"
+ >
+ </div>
+ <div
+ v-show="!shouldHideEditor"
+ ref="editor"
+ >
+ </div>
+ </div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue
index 8b9cbd23456..75787ad6103 100644
--- a/app/assets/javascripts/repo/components/repo_file.vue
+++ b/app/assets/javascripts/repo/components/repo_file.vue
@@ -1,107 +1,117 @@
<script>
-import TimeAgoMixin from '../../vue_shared/mixins/timeago';
+ import { mapActions, mapGetters } from 'vuex';
+ import timeAgoMixin from '../../vue_shared/mixins/timeago';
+ import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
-const RepoFile = {
- mixins: [TimeAgoMixin],
- props: {
- file: {
- type: Object,
- required: true,
+ export default {
+ mixins: [
+ timeAgoMixin,
+ ],
+ components: {
+ skeletonLoadingContainer,
},
- isMini: {
- type: Boolean,
- required: false,
- default: false,
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
},
- loading: {
- type: Object,
- required: false,
- default() { return { tree: false }; },
+ computed: {
+ ...mapGetters([
+ 'isCollapsed',
+ ]),
+ isSubmodule() {
+ return this.file.type === 'submodule';
+ },
+ fileIcon() {
+ return {
+ 'fa-spinner fa-spin': this.file.loading,
+ [this.file.icon]: !this.file.loading,
+ 'fa-folder-open': !this.file.loading && this.file.opened,
+ };
+ },
+ levelIndentation() {
+ return {
+ marginLeft: `${this.file.level * 16}px`,
+ };
+ },
+ shortId() {
+ return this.file.id.substr(0, 8);
+ },
+ submoduleColSpan() {
+ return !this.isCollapsed && this.isSubmodule ? 3 : 1;
+ },
},
- hasFiles: {
- type: Boolean,
- required: false,
- default: false,
+ methods: {
+ ...mapActions([
+ 'clickedTreeRow',
+ ]),
},
- activeFile: {
- type: Object,
- required: true,
- },
- },
-
- computed: {
- canShowFile() {
- return !this.loading.tree || this.hasFiles;
- },
-
- fileIcon() {
- const classObj = {
- 'fa-spinner fa-spin': this.file.loading,
- [this.file.icon]: !this.file.loading,
- };
- return classObj;
- },
-
- fileIndentation() {
- return {
- 'margin-left': `${this.file.level * 10}px`,
- };
- },
-
- activeFileClass() {
- return {
- active: this.activeFile.url === this.file.url,
- };
- },
- },
-
- methods: {
- linkClicked(file) {
- this.$emit('linkclicked', file);
- },
- },
-};
-
-export default RepoFile;
+ };
</script>
<template>
-<tr
- v-if="canShowFile"
- class="file"
- :class="activeFileClass"
- @click.prevent="linkClicked(file)">
- <td>
- <i
- class="fa fa-fw file-icon"
- :class="fileIcon"
- :style="fileIndentation"
- aria-label="file icon">
- </i>
- <a
- :href="file.url"
- class="repo-file-name"
- :title="file.url">
- {{file.name}}
- </a>
- </td>
+ <tr
+ class="file"
+ @click.prevent="clickedTreeRow(file)">
+ <td
+ class="multi-file-table-name"
+ :colspan="submoduleColSpan"
+ >
+ <i
+ class="fa fa-fw file-icon"
+ :class="fileIcon"
+ :style="levelIndentation"
+ aria-hidden="true"
+ >
+ </i>
+ <a
+ :href="file.url"
+ class="repo-file-name"
+ >
+ {{ file.name }}
+ </a>
+ <template v-if="isSubmodule && file.id">
+ @
+ <span class="commit-sha">
+ <a
+ @click.stop
+ :href="file.tree_url"
+ >
+ {{ shortId }}
+ </a>
+ </span>
+ </template>
+ </td>
- <template v-if="!isMini">
- <td class="hidden-sm hidden-xs">
- <div class="commit-message">
- <a @click.stop :href="file.lastCommitUrl">
- {{file.lastCommitMessage}}
+ <template v-if="!isCollapsed && !isSubmodule">
+ <td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
+ <a
+ v-if="file.lastCommit.message"
+ @click.stop
+ :href="file.lastCommit.url"
+ >
+ {{ file.lastCommit.message }}
</a>
- </div>
- </td>
+ <skeleton-loading-container
+ v-else
+ :small="true"
+ />
+ </td>
- <td class="hidden-xs text-right">
- <span
- class="commit-update"
- :title="tooltipTitle(file.lastCommitUpdate)">
- {{timeFormated(file.lastCommitUpdate)}}
- </span>
- </td>
- </template>
-</tr>
+ <td class="commit-update hidden-xs text-right">
+ <span
+ v-if="file.lastCommit.updatedAt"
+ :title="tooltipTitle(file.lastCommit.updatedAt)"
+ >
+ {{ timeFormated(file.lastCommit.updatedAt) }}
+ </span>
+ <skeleton-loading-container
+ v-else
+ class="animation-container-right"
+ :small="true"
+ />
+ </td>
+ </template>
+ </tr>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue
index e43ef366f47..34f0d51819a 100644
--- a/app/assets/javascripts/repo/components/repo_file_buttons.vue
+++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue
@@ -1,40 +1,35 @@
<script>
-import Store from '../stores/repo_store';
-import Helper from '../helpers/repo_helper';
-import RepoMixin from '../mixins/repo_mixin';
-
-const RepoFileButtons = {
- data: () => Store,
-
- mixins: [RepoMixin],
+import { mapGetters } from 'vuex';
+export default {
computed: {
-
- rawDownloadButtonLabel() {
- return this.binary ? 'Download' : 'Raw';
+ ...mapGetters([
+ 'activeFile',
+ ]),
+ showButtons() {
+ return this.activeFile.rawPath ||
+ this.activeFile.blamePath ||
+ this.activeFile.commitsPath ||
+ this.activeFile.permalink;
},
-
- canPreview() {
- return Helper.isRenderable();
+ rawDownloadButtonLabel() {
+ return this.activeFile.binary ? 'Download' : 'Raw';
},
},
-
- methods: {
- rawPreviewToggle: Store.toggleRawPreview,
- },
};
-
-export default RepoFileButtons;
</script>
<template>
- <div id="repo-file-buttons">
+ <div
+ v-if="showButtons"
+ class="multi-file-editor-btn-group"
+ >
<a
- :href="activeFile.raw_path"
+ :href="activeFile.rawPath"
target="_blank"
- class="btn btn-default raw"
+ class="btn btn-default btn-sm raw"
rel="noopener noreferrer">
- {{rawDownloadButtonLabel}}
+ {{ rawDownloadButtonLabel }}
</a>
<div
@@ -42,28 +37,20 @@ export default RepoFileButtons;
role="group"
aria-label="File actions">
<a
- :href="activeFile.blame_path"
- class="btn btn-default blame">
+ :href="activeFile.blamePath"
+ class="btn btn-default btn-sm blame">
Blame
</a>
<a
- :href="activeFile.commits_path"
- class="btn btn-default history">
+ :href="activeFile.commitsPath"
+ class="btn btn-default btn-sm history">
History
</a>
<a
:href="activeFile.permalink"
- class="btn btn-default permalink">
+ class="btn btn-default btn-sm permalink">
Permalink
</a>
</div>
-
- <a
- v-if="canPreview"
- href="#"
- @click.prevent="rawPreviewToggle"
- class="btn btn-default preview">
- {{activeFileLabel}}
- </a>
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_file_options.vue b/app/assets/javascripts/repo/components/repo_file_options.vue
deleted file mode 100644
index 6a15755f029..00000000000
--- a/app/assets/javascripts/repo/components/repo_file_options.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-<script>
-const RepoFileOptions = {
- props: {
- isMini: {
- type: Boolean,
- required: false,
- default: false,
- },
- projectName: {
- type: String,
- required: true,
- },
- },
-};
-
-export default RepoFileOptions;
-</script>
-
-<template>
- <tr v-if="isMini" class="repo-file-options">
- <td>
- <span class="title">{{projectName}}</span>
- </td>
- </tr>
-</template>
diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue
index bc8c64c8362..8fa637d771f 100644
--- a/app/assets/javascripts/repo/components/repo_loading_file.vue
+++ b/app/assets/javascripts/repo/components/repo_loading_file.vue
@@ -1,76 +1,44 @@
<script>
-const RepoLoadingFile = {
- props: {
- loading: {
- type: Object,
- required: false,
- default: {},
- },
- hasFiles: {
- type: Boolean,
- required: false,
- default: false,
- },
- isMini: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
+ import { mapGetters } from 'vuex';
+ import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
- computed: {
- showGhostLines() {
- return this.loading.tree && !this.hasFiles;
+ export default {
+ components: {
+ skeletonLoadingContainer,
},
- },
-
- methods: {
- lineOfCode(n) {
- return `skeleton-line-${n}`;
+ computed: {
+ ...mapGetters([
+ 'isCollapsed',
+ ]),
},
- },
-};
-
-export default RepoLoadingFile;
+ };
</script>
<template>
<tr
- v-if="showGhostLines"
- class="loading-file">
- <td>
- <div
- class="animation-container animation-container-small">
- <div
- v-for="n in 6"
- :key="n"
- :class="lineOfCode(n)">
- </div>
- </div>
- </td>
-
- <td
- v-if="!isMini"
- class="hidden-sm hidden-xs">
- <div class="animation-container">
- <div
- v-for="n in 6"
- :key="n"
- :class="lineOfCode(n)">
- </div>
- </div>
+ class="loading-file"
+ aria-label="Loading files"
+ >
+ <td class="multi-file-table-col-name">
+ <skeleton-loading-container
+ :small="true"
+ />
</td>
+ <template v-if="!isCollapsed">
+ <td
+ class="hidden-sm hidden-xs">
+ <skeleton-loading-container
+ :small="true"
+ />
+ </td>
- <td
- v-if="!isMini"
- class="hidden-xs">
- <div class="animation-container animation-container-small">
- <div
- v-for="n in 6"
- :key="n"
- :class="lineOfCode(n)">
- </div>
- </div>
- </td>
+ <td
+ class="hidden-xs">
+ <skeleton-loading-container
+ class="animation-container-right"
+ :small="true"
+ />
+ </td>
+ </template>
</tr>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/repo/components/repo_prev_directory.vue
index bbdbdc61e38..a2b305bbd05 100644
--- a/app/assets/javascripts/repo/components/repo_prev_directory.vue
+++ b/app/assets/javascripts/repo/components/repo_prev_directory.vue
@@ -1,38 +1,34 @@
<script>
-import RepoMixin from '../mixins/repo_mixin';
+ import { mapGetters, mapState, mapActions } from 'vuex';
-const RepoPreviousDirectory = {
- props: {
- prevUrl: {
- type: String,
- required: true,
+ export default {
+ computed: {
+ ...mapState([
+ 'parentTreeUrl',
+ ]),
+ ...mapGetters([
+ 'isCollapsed',
+ ]),
+ colSpanCondition() {
+ return this.isCollapsed ? undefined : 3;
+ },
},
- },
-
- mixins: [RepoMixin],
-
- computed: {
- colSpanCondition() {
- return this.isMini ? undefined : 3;
+ methods: {
+ ...mapActions([
+ 'getTreeData',
+ ]),
},
- },
-
- methods: {
- linkClicked(file) {
- this.$emit('linkclicked', file);
- },
- },
-};
-
-export default RepoPreviousDirectory;
+ };
</script>
<template>
-<tr class="prev-directory">
- <td
- :colspan="colSpanCondition"
- @click.prevent="linkClicked(prevUrl)">
- <a :href="prevUrl">..</a>
- </td>
-</tr>
+ <tr class="file prev-directory">
+ <td
+ :colspan="colSpanCondition"
+ class="table-cell"
+ @click.prevent="getTreeData({ endpoint: parentTreeUrl })"
+ >
+ <a :href="parentTreeUrl">...</a>
+ </td>
+ </tr>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue
index a87bef6084a..6ce9267f598 100644
--- a/app/assets/javascripts/repo/components/repo_preview.vue
+++ b/app/assets/javascripts/repo/components/repo_preview.vue
@@ -1,24 +1,20 @@
<script>
/* global LineHighlighter */
-
-import Store from '../stores/repo_store';
+import { mapGetters } from 'vuex';
export default {
- data: () => Store,
computed: {
- html() {
- return this.activeFile.html;
+ ...mapGetters([
+ 'activeFile',
+ ]),
+ renderErrorTooLarge() {
+ return this.activeFile.renderError === 'too_large';
},
},
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
- highlightLine() {
- if (Store.activeLine > -1) {
- this.lineHighlighter.highlightHash(`#L${Store.activeLine}`);
- }
- },
},
mounted() {
this.highlightFile();
@@ -27,16 +23,10 @@ export default {
scrollFileHolder: true,
});
},
- watch: {
- html() {
- this.$nextTick(() => {
- this.highlightFile();
- this.highlightLine();
- });
- },
- activeLine() {
- this.highlightLine();
- },
+ updated() {
+ this.$nextTick(() => {
+ this.highlightFile();
+ });
},
};
</script>
@@ -44,21 +34,30 @@ export default {
<template>
<div>
<div
- v-if="!activeFile.render_error"
- v-html="activeFile.html">
+ v-if="!activeFile.renderError"
+ v-html="activeFile.html"
+ class="multi-file-preview-holder"
+ >
+ </div>
+ <div
+ v-else-if="activeFile.tempFile"
+ class="vertical-center render-error">
+ <p class="text-center">
+ The source could not be displayed for this temporary file.
+ </p>
</div>
<div
- v-else-if="activeFile.tooLarge"
+ v-else-if="renderErrorTooLarge"
class="vertical-center render-error">
<p class="text-center">
- The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.
+ The source could not be displayed because it is too large. You can <a :href="activeFile.rawPath" download>download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
- The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.raw_path">download</a> it instead.
+ The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.rawPath" download>download</a> it instead.
</p>
</div>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
index e0f3c33003a..4ea21913129 100644
--- a/app/assets/javascripts/repo/components/repo_sidebar.vue
+++ b/app/assets/javascripts/repo/components/repo_sidebar.vue
@@ -1,129 +1,83 @@
<script>
-import Service from '../services/repo_service';
-import Helper from '../helpers/repo_helper';
-import Store from '../stores/repo_store';
+import { mapState, mapGetters, mapActions } from 'vuex';
import RepoPreviousDirectory from './repo_prev_directory.vue';
-import RepoFileOptions from './repo_file_options.vue';
import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue';
-import RepoMixin from '../mixins/repo_mixin';
export default {
- mixins: [RepoMixin],
components: {
- 'repo-file-options': RepoFileOptions,
'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile,
},
-
created() {
- window.addEventListener('popstate', this.checkHistory);
+ window.addEventListener('popstate', this.popHistoryState);
},
destroyed() {
- window.removeEventListener('popstate', this.checkHistory);
+ window.removeEventListener('popstate', this.popHistoryState);
+ },
+ mounted() {
+ this.getTreeData();
+ },
+ computed: {
+ ...mapState([
+ 'loading',
+ 'isRoot',
+ ]),
+ ...mapState({
+ projectName(state) {
+ return state.project.name;
+ },
+ }),
+ ...mapGetters([
+ 'treeList',
+ 'isCollapsed',
+ ]),
},
-
- data: () => Store,
-
methods: {
- checkHistory() {
- let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
- if (!selectedFile) {
- // Maybe it is not in the current tree but in the opened tabs
- selectedFile = Helper.getFileFromPath(location.pathname);
- }
-
- let lineNumber = null;
- if (location.hash.indexOf('#L') > -1) lineNumber = Number(location.hash.substr(2));
-
- if (selectedFile) {
- if (selectedFile.url !== this.activeFile.url) {
- this.fileClicked(selectedFile, lineNumber);
- } else {
- Store.setActiveLine(lineNumber);
- }
- } else {
- // Not opened at all lets open new tab
- this.fileClicked({
- url: location.href,
- }, lineNumber);
- }
- },
-
- fileClicked(clickedFile, lineNumber) {
- let file = clickedFile;
- if (file.loading) return;
- file.loading = true;
-
- if (file.type === 'tree' && file.opened) {
- file = Store.removeChildFilesOfTree(file);
- file.loading = false;
- Store.setActiveLine(lineNumber);
- } else {
- const openFile = Helper.getFileFromPath(file.url);
- if (openFile) {
- file.loading = false;
- Store.setActiveFiles(openFile);
- Store.setActiveLine(lineNumber);
- } else {
- Service.url = file.url;
- Helper.getContent(file)
- .then(() => {
- file.loading = false;
- Helper.scrollTabsRight();
- Store.setActiveLine(lineNumber);
- })
- .catch(Helper.loadingError);
- }
- }
- },
-
- goToPreviousDirectoryClicked(prevURL) {
- Service.url = prevURL;
- Helper.getContent(null)
- .then(() => Helper.scrollTabsRight())
- .catch(Helper.loadingError);
- },
+ ...mapActions([
+ 'getTreeData',
+ 'popHistoryState',
+ ]),
},
};
</script>
<template>
-<div id="sidebar" :class="{'sidebar-mini' : isMini}">
+<div class="ide-file-list">
<table class="table">
- <thead v-if="!isMini">
+ <thead>
<tr>
- <th class="name">Name</th>
- <th class="hidden-sm hidden-xs last-commit">Last commit</th>
- <th class="hidden-xs last-update text-right">Last update</th>
+ <th
+ v-if="isCollapsed"
+ >
+ </th>
+ <template v-else>
+ <th class="name multi-file-table-name">
+ Name
+ </th>
+ <th class="hidden-sm hidden-xs last-commit">
+ Last commit
+ </th>
+ <th class="hidden-xs last-update text-right">
+ Last update
+ </th>
+ </template>
</tr>
</thead>
<tbody>
- <repo-file-options
- :is-mini="isMini"
- :project-name="projectName"
- />
<repo-previous-directory
- v-if="isRoot"
- :prev-url="prevURL"
- @linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
+ v-if="!isRoot && treeList.length"
+ />
<repo-loading-file
+ v-if="!treeList.length && loading"
v-for="n in 5"
:key="n"
- :loading="loading"
- :has-files="!!files.length"
- :is-mini="isMini"
/>
<repo-file
- v-for="file in files"
- :key="file.id"
+ v-for="file in treeList"
+ :key="file.key"
:file="file"
- :is-mini="isMini"
- @linkclicked="fileClicked(file)"
- :is-tree="isTree"
- :has-files="!!files.length"
- :active-file="activeFile"
/>
</tbody>
</table>
diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue
index 0d0c34ec741..fb29a60df66 100644
--- a/app/assets/javascripts/repo/components/repo_tab.vue
+++ b/app/assets/javascripts/repo/components/repo_tab.vue
@@ -1,7 +1,7 @@
<script>
-import Store from '../stores/repo_store';
+import { mapActions } from 'vuex';
-const RepoTab = {
+export default {
props: {
tab: {
type: Object,
@@ -11,53 +11,57 @@ const RepoTab = {
computed: {
closeLabel() {
- if (this.tab.changed) {
+ if (this.tab.changed || this.tab.tempFile) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
changedClass() {
const tabChangedObj = {
- 'fa-times close-icon': !this.tab.changed,
- 'fa-circle unsaved-icon': this.tab.changed,
+ 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile,
+ 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile,
};
return tabChangedObj;
},
},
methods: {
- tabClicked: Store.setActiveFiles,
-
- closeTab(file) {
- if (file.changed) return;
- this.$emit('tabclosed', file);
- },
+ ...mapActions([
+ 'setFileActive',
+ 'closeFile',
+ ]),
},
};
-
-export default RepoTab;
</script>
<template>
-<li @click="tabClicked(tab)">
- <a
- href="#0"
- class="close"
- @click.stop.prevent="closeTab(tab)"
- :aria-label="closeLabel">
- <i
- class="fa"
- :class="changedClass"
- aria-hidden="true">
- </i>
- </a>
+ <li
+ @click="setFileActive(tab)"
+ >
+ <button
+ type="button"
+ class="multi-file-tab-close"
+ @click.stop.prevent="closeFile({ file: tab })"
+ :aria-label="closeLabel"
+ :class="{
+ 'modified': tab.changed,
+ }"
+ :disabled="tab.changed"
+ >
+ <i
+ class="fa"
+ :class="changedClass"
+ aria-hidden="true"
+ >
+ </i>
+ </button>
- <a
- href="#"
- class="repo-tab"
- :title="tab.url"
- @click.prevent="tabClicked(tab)">
- {{tab.name}}
- </a>
-</li>
+ <div
+ class="multi-file-tab"
+ :class="{active : tab.active }"
+ :title="tab.url"
+ >
+ {{ tab.name }}
+ </div>
+ </li>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue
index 9c5bfc5d0cf..ab0bef4f0ac 100644
--- a/app/assets/javascripts/repo/components/repo_tabs.vue
+++ b/app/assets/javascripts/repo/components/repo_tabs.vue
@@ -1,36 +1,27 @@
<script>
-import Store from '../stores/repo_store';
-import RepoTab from './repo_tab.vue';
-import RepoMixin from '../mixins/repo_mixin';
+ import { mapState } from 'vuex';
+ import RepoTab from './repo_tab.vue';
-const RepoTabs = {
- mixins: [RepoMixin],
-
- components: {
- 'repo-tab': RepoTab,
- },
-
- data: () => Store,
-
- methods: {
- tabClosed(file) {
- Store.removeFromOpenedFiles(file);
+ export default {
+ components: {
+ 'repo-tab': RepoTab,
},
- },
-};
-
-export default RepoTabs;
+ computed: {
+ ...mapState([
+ 'openFiles',
+ ]),
+ },
+ };
</script>
<template>
-<ul id="tabs">
- <repo-tab
- v-for="tab in openedFiles"
- :key="tab.id"
- :tab="tab"
- :class="{'active' : tab.active}"
- @tabclosed="tabClosed"
- />
- <li class="tabs-divider" />
-</ul>
+ <ul
+ class="multi-file-tabs list-unstyled append-bottom-0"
+ >
+ <repo-tab
+ v-for="tab in openFiles"
+ :key="tab.id"
+ :tab="tab"
+ />
+ </ul>
</template>
diff --git a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js
deleted file mode 100644
index f8729bbf585..00000000000
--- a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/* global monaco */
-import RepoEditor from '../components/repo_editor.vue';
-import Store from '../stores/repo_store';
-import Helper from '../helpers/repo_helper';
-import monacoLoader from '../monaco_loader';
-
-function repoEditorLoader() {
- Store.monacoLoading = true;
- return new Promise((resolve, reject) => {
- monacoLoader(['vs/editor/editor.main'], () => {
- Helper.monaco = monaco;
- Store.monacoLoading = false;
- resolve(RepoEditor);
- }, () => {
- Store.monacoLoading = false;
- reject();
- });
- });
-}
-
-const MonacoLoaderHelper = {
- repoEditorLoader,
-};
-
-export default MonacoLoaderHelper;
diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js
deleted file mode 100644
index 7483f8bc305..00000000000
--- a/app/assets/javascripts/repo/helpers/repo_helper.js
+++ /dev/null
@@ -1,279 +0,0 @@
-/* global Flash */
-import Service from '../services/repo_service';
-import Store from '../stores/repo_store';
-import '../../flash';
-
-const RepoHelper = {
- monacoInstance: null,
-
- getDefaultActiveFile() {
- return {
- active: true,
- binary: false,
- extension: '',
- html: '',
- mime_type: '',
- name: '',
- plain: '',
- size: 0,
- url: '',
- raw: false,
- newContent: '',
- changed: false,
- loading: false,
- };
- },
-
- key: '',
-
- isTree(data) {
- return Object.hasOwnProperty.call(data, 'blobs');
- },
-
- Time: window.performance
- && window.performance.now
- ? window.performance
- : Date,
-
- getFileExtension(fileName) {
- return fileName.split('.').pop();
- },
-
- getLanguageIDForFile(file, langs) {
- const ext = RepoHelper.getFileExtension(file.name);
- const foundLang = RepoHelper.findLanguage(ext, langs);
-
- return foundLang ? foundLang.id : 'plaintext';
- },
-
- setMonacoModelFromLanguage() {
- RepoHelper.monacoInstance.setModel(null);
- const languages = RepoHelper.monaco.languages.getLanguages();
- const languageID = RepoHelper.getLanguageIDForFile(Store.activeFile, languages);
- const newModel = RepoHelper.monaco.editor.createModel(Store.blobRaw, languageID);
- RepoHelper.monacoInstance.setModel(newModel);
- },
-
- findLanguage(ext, langs) {
- return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1);
- },
-
- setDirectoryOpen(tree, title) {
- const file = tree;
- if (!file) return undefined;
-
- file.opened = true;
- file.icon = 'fa-folder-open';
- RepoHelper.updateHistoryEntry(file.url, title);
- return file;
- },
-
- isRenderable() {
- const okExts = ['md', 'svg'];
- return okExts.indexOf(Store.activeFile.extension) > -1;
- },
-
- setBinaryDataAsBase64(file) {
- Service.getBase64Content(file.raw_path)
- .then((response) => {
- Store.blobRaw = response;
- file.base64 = response; // eslint-disable-line no-param-reassign
- })
- .catch(RepoHelper.loadingError);
- },
-
- // when you open a directory you need to put the directory files under
- // the directory... This will merge the list of the current directory and the new list.
- getNewMergedList(inDirectory, currentList, newList) {
- const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
- if (!inDirectory) return newListSorted;
- const indexOfFile = currentList.findIndex(file => file.url === inDirectory.url);
- if (!indexOfFile) return newListSorted;
- return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile);
- },
-
- // within the get new merged list this does the merging of the current list of files
- // and the new list of files. The files are never "in" another directory they just
- // appear like they are because of the margin.
- mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
- newList.reverse().forEach((newFile) => {
- const fileIndex = indexOfFile + 1;
- const file = newFile;
- file.level = inDirectory.level + 1;
- oldList.splice(fileIndex, 0, file);
- });
-
- return oldList;
- },
-
- compareFilesCaseInsensitive(a, b) {
- const aName = a.name.toLowerCase();
- const bName = b.name.toLowerCase();
- if (a.level > 0) return 0;
- if (aName < bName) { return -1; }
- if (aName > bName) { return 1; }
- return 0;
- },
-
- isRoot(url) {
- // the url we are requesting -> split by the project URL. Grab the right side.
- const isRoot = !!url.split(Store.projectUrl)[1]
- // remove the first "/"
- .slice(1)
- // split this by "/"
- .split('/')
- // remove the first two items of the array... usually /tree/master.
- .slice(2)
- // we want to know the length of the array.
- // If greater than 0 not root.
- .length;
- return isRoot;
- },
-
- getContent(treeOrFile) {
- let file = treeOrFile;
- return Service.getContent()
- .then((response) => {
- const data = response.data;
- if (response.headers && response.headers['page-title']) data.pageTitle = response.headers['page-title'];
-
- Store.isTree = RepoHelper.isTree(data);
- if (!Store.isTree) {
- if (!file) file = data;
- Store.binary = data.binary;
-
- if (data.binary) {
- // file might be undefined
- RepoHelper.setBinaryDataAsBase64(data);
- Store.setViewToPreview();
- } else if (!Store.isPreviewView()) {
- if (!data.render_error) {
- Service.getRaw(data.raw_path)
- .then((rawResponse) => {
- Store.blobRaw = rawResponse.data;
- data.plain = rawResponse.data;
- RepoHelper.setFile(data, file);
- }).catch(RepoHelper.loadingError);
- }
- }
-
- if (Store.isPreviewView()) {
- RepoHelper.setFile(data, file);
- }
-
- // if the file tree is empty
- if (Store.files.length === 0) {
- const parentURL = Service.blobURLtoParentTree(Service.url);
- Service.url = parentURL;
- RepoHelper.getContent();
- }
- } else {
- // it's a tree
- if (!file) Store.isRoot = RepoHelper.isRoot(Service.url);
- file = RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
- const newDirectory = RepoHelper.dataToListOfFiles(data);
- Store.addFilesToDirectory(file, Store.files, newDirectory);
- Store.prevURL = Service.blobURLtoParentTree(Service.url);
- }
- }).catch(RepoHelper.loadingError);
- },
-
- setFile(data, file) {
- const newFile = data;
- newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
-
- if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
- newFile.tooLarge = true;
- }
- newFile.newContent = '';
-
- Store.addToOpenedFiles(newFile);
- Store.setActiveFiles(newFile);
- },
-
- serializeBlob(blob) {
- const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
- simpleBlob.lastCommitMessage = blob.last_commit.message;
- simpleBlob.lastCommitUpdate = blob.last_commit.committed_date;
- simpleBlob.loading = false;
-
- return simpleBlob;
- },
-
- serializeTree(tree) {
- return RepoHelper.serializeRepoEntity('tree', tree);
- },
-
- serializeSubmodule(submodule) {
- return RepoHelper.serializeRepoEntity('submodule', submodule);
- },
-
- serializeRepoEntity(type, entity) {
- const { url, name, icon, last_commit } = entity;
- const returnObj = {
- type,
- name,
- url,
- icon: `fa-${icon}`,
- level: 0,
- loading: false,
- };
-
- if (entity.last_commit) {
- returnObj.lastCommitUrl = `${Store.projectUrl}/commit/${last_commit.id}`;
- } else {
- returnObj.lastCommitUrl = '';
- }
- return returnObj;
- },
-
- scrollTabsRight() {
- // wait for the transition. 0.1 seconds.
- setTimeout(() => {
- const tabs = document.getElementById('tabs');
- if (!tabs) return;
- tabs.scrollLeft = tabs.scrollWidth;
- }, 200);
- },
-
- dataToListOfFiles(data) {
- const { blobs, trees, submodules } = data;
- return [
- ...blobs.map(blob => RepoHelper.serializeBlob(blob)),
- ...trees.map(tree => RepoHelper.serializeTree(tree)),
- ...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)),
- ];
- },
-
- genKey() {
- return RepoHelper.Time.now().toFixed(3);
- },
-
- updateHistoryEntry(url, title) {
- const history = window.history;
-
- RepoHelper.key = RepoHelper.genKey();
-
- if (document.location.pathname !== url) {
- history.pushState({ key: RepoHelper.key }, '', url);
- }
-
- if (title) {
- document.title = title;
- }
- },
-
- findOpenedFileFromActive() {
- return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url);
- },
-
- getFileFromPath(path) {
- return Store.openedFiles.find(file => file.url === path);
- },
-
- loadingError() {
- Flash('Unable to load this content at this time.');
- },
-};
-
-export default RepoHelper;
diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js
index 7d0123e3d3a..b6801af7fcb 100644
--- a/app/assets/javascripts/repo/index.js
+++ b/app/assets/javascripts/repo/index.js
@@ -1,46 +1,50 @@
-import $ from 'jquery';
import Vue from 'vue';
-import Service from './services/repo_service';
-import Store from './stores/repo_store';
+import { mapActions } from 'vuex';
+import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue';
+import newBranchForm from './components/new_branch_form.vue';
+import newDropdown from './components/new_dropdown/index.vue';
+import store from './stores';
import Translate from '../vue_shared/translate';
-function initDropdowns() {
- $('.js-tree-ref-target-holder').hide();
-}
-
-function addEventsForNonVueEls() {
- window.onbeforeunload = function confirmUnload(e) {
- const hasChanged = Store.openedFiles
- .some(file => file.changed);
- if (!hasChanged) return undefined;
- const event = e || window.event;
- if (event) event.returnValue = 'Are you sure you want to lose unsaved changes?';
- // For Safari
- return 'Are you sure you want to lose unsaved changes?';
- };
-}
-
-function setInitialStore(data) {
- Store.service = Service;
- Store.service.url = data.url;
- Store.service.refsUrl = data.refsUrl;
- Store.projectId = data.projectId;
- Store.projectName = data.projectName;
- Store.projectUrl = data.projectUrl;
- Store.canCommit = data.canCommit;
- Store.onTopOfBranch = data.onTopOfBranch;
- Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
- Store.checkIsCommitable();
-}
-
function initRepo(el) {
+ if (!el) return null;
+
return new Vue({
el,
+ store,
components: {
repo: Repo,
},
+ methods: {
+ ...mapActions([
+ 'setInitialData',
+ ]),
+ },
+ created() {
+ const data = el.dataset;
+
+ this.setInitialData({
+ project: {
+ id: data.projectId,
+ name: data.projectName,
+ url: data.projectUrl,
+ },
+ endpoints: {
+ rootEndpoint: data.url,
+ newMergeRequestUrl: data.newMergeRequestUrl,
+ rootUrl: data.rootUrl,
+ },
+ canCommit: convertPermissionToBoolean(data.canCommit),
+ onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
+ currentRef: data.ref,
+ path: data.currentPath,
+ currentBranch: data.currentBranch,
+ isRoot: convertPermissionToBoolean(data.root),
+ isInitialRoot: convertPermissionToBoolean(data.root),
+ });
+ },
render(createElement) {
return createElement('repo');
},
@@ -50,25 +54,53 @@ function initRepo(el) {
function initRepoEditButton(el) {
return new Vue({
el,
+ store,
components: {
repoEditButton: RepoEditButton,
},
+ render(createElement) {
+ return createElement('repo-edit-button');
+ },
});
}
-function initRepoBundle() {
- const repo = document.getElementById('repo');
- const editButton = document.querySelector('.editable-mode');
- setInitialStore(repo.dataset);
- addEventsForNonVueEls();
- initDropdowns();
+function initNewDropdown(el) {
+ return new Vue({
+ el,
+ store,
+ components: {
+ newDropdown,
+ },
+ render(createElement) {
+ return createElement('new-dropdown');
+ },
+ });
+}
+
+function initNewBranchForm() {
+ const el = document.querySelector('.js-new-branch-dropdown');
- Vue.use(Translate);
+ if (!el) return null;
- initRepo(repo);
- initRepoEditButton(editButton);
+ return new Vue({
+ el,
+ components: {
+ newBranchForm,
+ },
+ store,
+ render(createElement) {
+ return createElement('new-branch-form');
+ },
+ });
}
-$(initRepoBundle);
+const repo = document.getElementById('repo');
+const editButton = document.querySelector('.editable-mode');
+const newDropdownHolder = document.querySelector('.js-new-dropdown');
+
+Vue.use(Translate);
-export default initRepoBundle;
+initRepo(repo);
+initRepoEditButton(editButton);
+initNewBranchForm();
+initNewDropdown(newDropdownHolder);
diff --git a/app/assets/javascripts/repo/lib/common/disposable.js b/app/assets/javascripts/repo/lib/common/disposable.js
new file mode 100644
index 00000000000..84b29bdb600
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/common/disposable.js
@@ -0,0 +1,14 @@
+export default class Disposable {
+ constructor() {
+ this.disposers = new Set();
+ }
+
+ add(...disposers) {
+ disposers.forEach(disposer => this.disposers.add(disposer));
+ }
+
+ dispose() {
+ this.disposers.forEach(disposer => disposer.dispose());
+ this.disposers.clear();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/common/model.js b/app/assets/javascripts/repo/lib/common/model.js
new file mode 100644
index 00000000000..23c4811e6c0
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/common/model.js
@@ -0,0 +1,56 @@
+/* global monaco */
+import Disposable from './disposable';
+
+export default class Model {
+ constructor(monaco, file) {
+ this.monaco = monaco;
+ this.disposable = new Disposable();
+ this.file = file;
+ this.content = file.content !== '' ? file.content : file.raw;
+
+ this.disposable.add(
+ this.originalModel = this.monaco.editor.createModel(
+ this.file.raw,
+ undefined,
+ new this.monaco.Uri(null, null, `original/${this.file.path}`),
+ ),
+ this.model = this.monaco.editor.createModel(
+ this.content,
+ undefined,
+ new this.monaco.Uri(null, null, this.file.path),
+ ),
+ );
+
+ this.events = new Map();
+ }
+
+ get url() {
+ return this.model.uri.toString();
+ }
+
+ get path() {
+ return this.file.path;
+ }
+
+ getModel() {
+ return this.model;
+ }
+
+ getOriginalModel() {
+ return this.originalModel;
+ }
+
+ onChange(cb) {
+ this.events.set(
+ this.path,
+ this.disposable.add(
+ this.model.onDidChangeContent(e => cb(this.model, e)),
+ ),
+ );
+ }
+
+ dispose() {
+ this.disposable.dispose();
+ this.events.clear();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/common/model_manager.js b/app/assets/javascripts/repo/lib/common/model_manager.js
new file mode 100644
index 00000000000..fd462252795
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/common/model_manager.js
@@ -0,0 +1,32 @@
+import Disposable from './disposable';
+import Model from './model';
+
+export default class ModelManager {
+ constructor(monaco) {
+ this.monaco = monaco;
+ this.disposable = new Disposable();
+ this.models = new Map();
+ }
+
+ hasCachedModel(path) {
+ return this.models.has(path);
+ }
+
+ addModel(file) {
+ if (this.hasCachedModel(file.path)) {
+ return this.models.get(file.path);
+ }
+
+ const model = new Model(this.monaco, file);
+ this.models.set(model.path, model);
+ this.disposable.add(model);
+
+ return model;
+ }
+
+ dispose() {
+ // dispose of all the models
+ this.disposable.dispose();
+ this.models.clear();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/decorations/controller.js b/app/assets/javascripts/repo/lib/decorations/controller.js
new file mode 100644
index 00000000000..0954b7973c4
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/decorations/controller.js
@@ -0,0 +1,43 @@
+export default class DecorationsController {
+ constructor(editor) {
+ this.editor = editor;
+ this.decorations = new Map();
+ this.editorDecorations = new Map();
+ }
+
+ getAllDecorationsForModel(model) {
+ if (!this.decorations.has(model.url)) return [];
+
+ const modelDecorations = this.decorations.get(model.url);
+ const decorations = [];
+
+ modelDecorations.forEach(val => decorations.push(...val));
+
+ return decorations;
+ }
+
+ addDecorations(model, decorationsKey, decorations) {
+ const decorationMap = this.decorations.get(model.url) || new Map();
+
+ decorationMap.set(decorationsKey, decorations);
+
+ this.decorations.set(model.url, decorationMap);
+
+ this.decorate(model);
+ }
+
+ decorate(model) {
+ const decorations = this.getAllDecorationsForModel(model);
+ const oldDecorations = this.editorDecorations.get(model.url) || [];
+
+ this.editorDecorations.set(
+ model.url,
+ this.editor.instance.deltaDecorations(oldDecorations, decorations),
+ );
+ }
+
+ dispose() {
+ this.decorations.clear();
+ this.editorDecorations.clear();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/diff/controller.js b/app/assets/javascripts/repo/lib/diff/controller.js
new file mode 100644
index 00000000000..dc0b1c95e59
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/controller.js
@@ -0,0 +1,71 @@
+/* global monaco */
+import { throttle } from 'underscore';
+import DirtyDiffWorker from './diff_worker';
+import Disposable from '../common/disposable';
+
+export const getDiffChangeType = (change) => {
+ if (change.modified) {
+ return 'modified';
+ } else if (change.added) {
+ return 'added';
+ } else if (change.removed) {
+ return 'removed';
+ }
+
+ return '';
+};
+
+export const getDecorator = change => ({
+ range: new monaco.Range(
+ change.lineNumber,
+ 1,
+ change.endLineNumber,
+ 1,
+ ),
+ options: {
+ isWholeLine: true,
+ linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
+ },
+});
+
+export default class DirtyDiffController {
+ constructor(modelManager, decorationsController) {
+ this.disposable = new Disposable();
+ this.editorSimpleWorker = null;
+ this.modelManager = modelManager;
+ this.decorationsController = decorationsController;
+ this.dirtyDiffWorker = new DirtyDiffWorker();
+ this.throttledComputeDiff = throttle(this.computeDiff, 250);
+ this.decorate = this.decorate.bind(this);
+
+ this.dirtyDiffWorker.addEventListener('message', this.decorate);
+ }
+
+ attachModel(model) {
+ model.onChange(() => this.throttledComputeDiff(model));
+ }
+
+ computeDiff(model) {
+ this.dirtyDiffWorker.postMessage({
+ path: model.path,
+ originalContent: model.getOriginalModel().getValue(),
+ newContent: model.getModel().getValue(),
+ });
+ }
+
+ reDecorate(model) {
+ this.decorationsController.decorate(model);
+ }
+
+ decorate({ data }) {
+ const decorations = data.changes.map(change => getDecorator(change));
+ this.decorationsController.addDecorations(data.path, 'dirtyDiff', decorations);
+ }
+
+ dispose() {
+ this.disposable.dispose();
+
+ this.dirtyDiffWorker.removeEventListener('message', this.decorate);
+ this.dirtyDiffWorker.terminate();
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/diff/diff.js b/app/assets/javascripts/repo/lib/diff/diff.js
new file mode 100644
index 00000000000..0e37f5c4704
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/diff.js
@@ -0,0 +1,30 @@
+import { diffLines } from 'diff';
+
+// eslint-disable-next-line import/prefer-default-export
+export const computeDiff = (originalContent, newContent) => {
+ const changes = diffLines(originalContent, newContent);
+
+ let lineNumber = 1;
+ return changes.reduce((acc, change) => {
+ const findOnLine = acc.find(c => c.lineNumber === lineNumber);
+
+ if (findOnLine) {
+ Object.assign(findOnLine, change, {
+ modified: true,
+ endLineNumber: (lineNumber + change.count) - 1,
+ });
+ } else if ('added' in change || 'removed' in change) {
+ acc.push(Object.assign({}, change, {
+ lineNumber,
+ modified: undefined,
+ endLineNumber: (lineNumber + change.count) - 1,
+ }));
+ }
+
+ if (!change.removed) {
+ lineNumber += change.count;
+ }
+
+ return acc;
+ }, []);
+};
diff --git a/app/assets/javascripts/repo/lib/diff/diff_worker.js b/app/assets/javascripts/repo/lib/diff/diff_worker.js
new file mode 100644
index 00000000000..e74c4046330
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/diff/diff_worker.js
@@ -0,0 +1,10 @@
+import { computeDiff } from './diff';
+
+self.addEventListener('message', (e) => {
+ const data = e.data;
+
+ self.postMessage({
+ path: data.path,
+ changes: computeDiff(data.originalContent, data.newContent),
+ });
+});
diff --git a/app/assets/javascripts/repo/lib/editor.js b/app/assets/javascripts/repo/lib/editor.js
new file mode 100644
index 00000000000..db499444402
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/editor.js
@@ -0,0 +1,79 @@
+import DecorationsController from './decorations/controller';
+import DirtyDiffController from './diff/controller';
+import Disposable from './common/disposable';
+import ModelManager from './common/model_manager';
+import editorOptions from './editor_options';
+
+export default class Editor {
+ static create(monaco) {
+ this.editorInstance = new Editor(monaco);
+
+ return this.editorInstance;
+ }
+
+ constructor(monaco) {
+ this.monaco = monaco;
+ this.currentModel = null;
+ this.instance = null;
+ this.dirtyDiffController = null;
+ this.disposable = new Disposable();
+
+ this.disposable.add(
+ this.modelManager = new ModelManager(this.monaco),
+ this.decorationsController = new DecorationsController(this),
+ );
+ }
+
+ createInstance(domElement) {
+ if (!this.instance) {
+ this.disposable.add(
+ this.instance = this.monaco.editor.create(domElement, {
+ model: null,
+ readOnly: false,
+ contextmenu: true,
+ scrollBeyondLastLine: false,
+ }),
+ this.dirtyDiffController = new DirtyDiffController(
+ this.modelManager, this.decorationsController,
+ ),
+ );
+ }
+ }
+
+ createModel(file) {
+ return this.modelManager.addModel(file);
+ }
+
+ attachModel(model) {
+ this.instance.setModel(model.getModel());
+ this.dirtyDiffController.attachModel(model);
+
+ this.currentModel = model;
+
+ this.instance.updateOptions(editorOptions.reduce((acc, obj) => {
+ Object.keys(obj).forEach((key) => {
+ Object.assign(acc, {
+ [key]: obj[key](model),
+ });
+ });
+ return acc;
+ }, {}));
+
+ this.dirtyDiffController.reDecorate(model);
+ }
+
+ clearEditor() {
+ if (this.instance) {
+ this.instance.setModel(null);
+ }
+ }
+
+ dispose() {
+ this.disposable.dispose();
+
+ // dispose main monaco instance
+ if (this.instance) {
+ this.instance = null;
+ }
+ }
+}
diff --git a/app/assets/javascripts/repo/lib/editor_options.js b/app/assets/javascripts/repo/lib/editor_options.js
new file mode 100644
index 00000000000..701affc466e
--- /dev/null
+++ b/app/assets/javascripts/repo/lib/editor_options.js
@@ -0,0 +1,2 @@
+export default [{
+}];
diff --git a/app/assets/javascripts/repo/mixins/repo_mixin.js b/app/assets/javascripts/repo/mixins/repo_mixin.js
deleted file mode 100644
index c8e8238a0d3..00000000000
--- a/app/assets/javascripts/repo/mixins/repo_mixin.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Store from '../stores/repo_store';
-
-const RepoMixin = {
- computed: {
- isMini() {
- return !!Store.openedFiles.length;
- },
-
- changedFiles() {
- const changedFileList = this.openedFiles
- .filter(file => file.changed);
- return changedFileList;
- },
- },
-};
-
-export default RepoMixin;
diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/repo/services/index.js
new file mode 100644
index 00000000000..994d325e991
--- /dev/null
+++ b/app/assets/javascripts/repo/services/index.js
@@ -0,0 +1,44 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import Api from '../../api';
+
+Vue.use(VueResource);
+
+export default {
+ getTreeData(endpoint) {
+ return Vue.http.get(endpoint, { params: { format: 'json' } });
+ },
+ getFileData(endpoint) {
+ return Vue.http.get(endpoint, { params: { format: 'json' } });
+ },
+ getRawFileData(file) {
+ if (file.tempFile) {
+ return Promise.resolve(file.content);
+ }
+
+ if (file.raw) {
+ return Promise.resolve(file.raw);
+ }
+
+ return Vue.http.get(file.rawPath, { params: { format: 'json' } })
+ .then(res => res.text());
+ },
+ getBranchData(projectId, currentBranch) {
+ return Api.branchSingle(projectId, currentBranch);
+ },
+ createBranch(projectId, payload) {
+ const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
+
+ return Vue.http.post(url, payload);
+ },
+ commit(projectId, payload) {
+ return Api.commitMultiple(projectId, payload);
+ },
+ getTreeLastCommit(endpoint) {
+ return Vue.http.get(endpoint, {
+ params: {
+ format: 'json',
+ },
+ });
+ },
+};
diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js
deleted file mode 100644
index af83497fa39..00000000000
--- a/app/assets/javascripts/repo/services/repo_service.js
+++ /dev/null
@@ -1,82 +0,0 @@
-/* global Flash */
-import axios from 'axios';
-import Store from '../stores/repo_store';
-import Api from '../../api';
-import Helper from '../helpers/repo_helper';
-
-const RepoService = {
- url: '',
- options: {
- params: {
- format: 'json',
- },
- },
- richExtensionRegExp: /md/,
-
- getRaw(url) {
- return axios.get(url, {
- // Stop Axios from parsing a JSON file into a JS object
- transformResponse: [res => res],
- });
- },
-
- buildParams(url = this.url) {
- // shallow clone object without reference
- const params = Object.assign({}, this.options.params);
-
- if (this.urlIsRichBlob(url)) params.viewer = 'rich';
-
- return params;
- },
-
- urlIsRichBlob(url = this.url) {
- const extension = Helper.getFileExtension(url);
-
- return this.richExtensionRegExp.test(extension);
- },
-
- getContent(url = this.url) {
- const params = this.buildParams(url);
-
- return axios.get(url, {
- params,
- });
- },
-
- getBase64Content(url = this.url) {
- const request = axios.get(url, {
- responseType: 'arraybuffer',
- });
-
- return request.then(response => this.bufferToBase64(response.data));
- },
-
- bufferToBase64(data) {
- return new Buffer(data, 'binary').toString('base64');
- },
-
- blobURLtoParentTree(url) {
- const urlArray = url.split('/');
- urlArray.pop();
- const blobIndex = urlArray.lastIndexOf('blob');
-
- if (blobIndex > -1) urlArray[blobIndex] = 'tree';
-
- return urlArray.join('/');
- },
-
- commitFiles(payload) {
- return Api.commitMultiple(Store.projectId, payload)
- .then(this.commitFlash);
- },
-
- commitFlash(data) {
- if (data.short_id && data.stats) {
- window.Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
- } else {
- window.Flash(data.message);
- }
- },
-};
-
-export default RepoService;
diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js
new file mode 100644
index 00000000000..120ce96f44d
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/actions.js
@@ -0,0 +1,145 @@
+import Vue from 'vue';
+import flash from '../../flash';
+import service from '../services';
+import * as types from './mutation_types';
+
+export const redirectToUrl = (_, url) => gl.utils.visitUrl(url);
+
+export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
+
+export const closeDiscardPopup = ({ commit }) => commit(types.TOGGLE_DISCARD_POPUP, false);
+
+export const discardAllChanges = ({ commit, getters, dispatch }) => {
+ const changedFiles = getters.changedFiles;
+
+ changedFiles.forEach((file) => {
+ commit(types.DISCARD_FILE_CHANGES, file);
+
+ if (file.tempFile) {
+ dispatch('closeFile', { file, force: true });
+ }
+ });
+};
+
+export const closeAllFiles = ({ state, dispatch }) => {
+ state.openFiles.forEach(file => dispatch('closeFile', { file }));
+};
+
+export const toggleEditMode = ({ state, commit, getters, dispatch }, force = false) => {
+ const changedFiles = getters.changedFiles;
+
+ if (changedFiles.length && !force) {
+ commit(types.TOGGLE_DISCARD_POPUP, true);
+ } else {
+ commit(types.TOGGLE_EDIT_MODE);
+ commit(types.TOGGLE_DISCARD_POPUP, false);
+ dispatch('toggleBlobView');
+
+ if (!state.editMode) {
+ dispatch('discardAllChanges');
+ }
+ }
+};
+
+export const toggleBlobView = ({ commit, state }) => {
+ if (state.editMode) {
+ commit(types.SET_EDIT_MODE);
+ } else {
+ commit(types.SET_PREVIEW_MODE);
+ }
+};
+
+export const checkCommitStatus = ({ state }) => service.getBranchData(
+ state.project.id,
+ state.currentBranch,
+)
+ .then((data) => {
+ const { id } = data.commit;
+
+ if (state.currentRef !== id) {
+ return true;
+ }
+
+ return false;
+ })
+ .catch(() => flash('Error checking branch data. Please try again.'));
+
+export const commitChanges = ({ commit, state, dispatch, getters }, { payload, newMr }) =>
+ service.commit(state.project.id, payload)
+ .then((data) => {
+ const { branch } = payload;
+ if (!data.short_id) {
+ flash(data.message);
+ return;
+ }
+
+ const lastCommit = {
+ commit_path: `${state.project.url}/commit/${data.id}`,
+ commit: {
+ message: data.message,
+ authored_date: data.committed_date,
+ },
+ };
+
+ flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
+
+ if (newMr) {
+ dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
+ } else {
+ commit(types.SET_COMMIT_REF, data.id);
+
+ getters.changedFiles.forEach((entry) => {
+ commit(types.SET_LAST_COMMIT_DATA, {
+ entry,
+ lastCommit,
+ });
+ });
+
+ dispatch('discardAllChanges');
+ dispatch('closeAllFiles');
+ dispatch('toggleEditMode');
+
+ window.scrollTo(0, 0);
+ }
+ })
+ .catch(() => flash('Error committing changes. Please try again.'));
+
+export const createTempEntry = ({ state, dispatch }, { name, type, content = '', base64 = false }) => {
+ if (type === 'tree') {
+ dispatch('createTempTree', name);
+ } else if (type === 'blob') {
+ dispatch('createTempFile', {
+ tree: state,
+ name,
+ base64,
+ content,
+ });
+ }
+};
+
+export const popHistoryState = ({ state, dispatch, getters }) => {
+ const treeList = getters.treeList;
+ const tree = treeList.find(file => file.url === state.previousUrl);
+
+ if (!tree) return;
+
+ if (tree.type === 'tree') {
+ dispatch('toggleTreeOpen', { endpoint: tree.url, tree });
+ }
+};
+
+export const scrollToTab = () => {
+ Vue.nextTick(() => {
+ const tabs = document.getElementById('tabs');
+
+ if (tabs) {
+ const tabEl = tabs.querySelector('.active .repo-tab');
+
+ tabEl.focus();
+ }
+ });
+};
+
+export * from './actions/tree';
+export * from './actions/file';
+export * from './actions/branch';
diff --git a/app/assets/javascripts/repo/stores/actions/branch.js b/app/assets/javascripts/repo/stores/actions/branch.js
new file mode 100644
index 00000000000..61d9a5af3e3
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/actions/branch.js
@@ -0,0 +1,20 @@
+import service from '../../services';
+import * as types from '../mutation_types';
+import { pushState } from '../utils';
+
+// eslint-disable-next-line import/prefer-default-export
+export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
+ state.project.id,
+ {
+ branch,
+ ref: state.currentBranch,
+ },
+).then(res => res.json())
+.then((data) => {
+ const branchName = data.name;
+ const url = location.href.replace(state.currentBranch, branchName);
+
+ pushState(url);
+
+ commit(types.SET_CURRENT_BRANCH, branchName);
+});
diff --git a/app/assets/javascripts/repo/stores/actions/file.js b/app/assets/javascripts/repo/stores/actions/file.js
new file mode 100644
index 00000000000..5bae4fa826a
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/actions/file.js
@@ -0,0 +1,110 @@
+import { normalizeHeaders } from '../../../lib/utils/common_utils';
+import flash from '../../../flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+import {
+ findEntry,
+ pushState,
+ setPageTitle,
+ createTemp,
+ findIndexOfFile,
+} from '../utils';
+
+export const closeFile = ({ commit, state, dispatch }, { file, force = false }) => {
+ if ((file.changed || file.tempFile) && !force) return;
+
+ const indexOfClosedFile = findIndexOfFile(state.openFiles, file);
+ const fileWasActive = file.active;
+
+ commit(types.TOGGLE_FILE_OPEN, file);
+ commit(types.SET_FILE_ACTIVE, { file, active: false });
+
+ if (state.openFiles.length > 0 && fileWasActive) {
+ const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
+ const nextFileToOpen = state.openFiles[nextIndexToOpen];
+
+ dispatch('setFileActive', nextFileToOpen);
+ } else if (!state.openFiles.length) {
+ pushState(file.parentTreeUrl);
+ }
+
+ dispatch('getLastCommitData');
+};
+
+export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
+ const currentActiveFile = getters.activeFile;
+
+ if (file.active) return;
+
+ if (currentActiveFile) {
+ commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false });
+ }
+
+ commit(types.SET_FILE_ACTIVE, { file, active: true });
+ dispatch('scrollToTab');
+
+ // reset hash for line highlighting
+ location.hash = '';
+};
+
+export const getFileData = ({ state, commit, dispatch }, file) => {
+ commit(types.TOGGLE_LOADING, file);
+
+ service.getFileData(file.url)
+ .then((res) => {
+ const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
+
+ setPageTitle(pageTitle);
+
+ return res.json();
+ })
+ .then((data) => {
+ commit(types.SET_FILE_DATA, { data, file });
+ commit(types.TOGGLE_FILE_OPEN, file);
+ dispatch('setFileActive', file);
+ commit(types.TOGGLE_LOADING, file);
+
+ pushState(file.url);
+ })
+ .catch(() => {
+ commit(types.TOGGLE_LOADING, file);
+ flash('Error loading file data. Please try again.');
+ });
+};
+
+export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file)
+ .then((raw) => {
+ commit(types.SET_FILE_RAW_DATA, { file, raw });
+ })
+ .catch(() => flash('Error loading file content. Please try again.'));
+
+export const changeFileContent = ({ commit }, { file, content }) => {
+ commit(types.UPDATE_FILE_CONTENT, { file, content });
+};
+
+export const createTempFile = ({ state, commit, dispatch }, { tree, name, content = '', base64 = '' }) => {
+ const file = createTemp({
+ name: name.replace(`${state.path}/`, ''),
+ path: tree.path,
+ type: 'blob',
+ level: tree.level !== undefined ? tree.level + 1 : 0,
+ changed: true,
+ content,
+ base64,
+ });
+
+ if (findEntry(tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
+
+ commit(types.CREATE_TMP_FILE, {
+ parent: tree,
+ file,
+ });
+ commit(types.TOGGLE_FILE_OPEN, file);
+ dispatch('setFileActive', file);
+
+ if (!state.editMode && !file.base64) {
+ dispatch('toggleEditMode', true);
+ }
+
+ return Promise.resolve(file);
+};
diff --git a/app/assets/javascripts/repo/stores/actions/tree.js b/app/assets/javascripts/repo/stores/actions/tree.js
new file mode 100644
index 00000000000..aa830e946a2
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/actions/tree.js
@@ -0,0 +1,162 @@
+import { normalizeHeaders } from '../../../lib/utils/common_utils';
+import flash from '../../../flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+import {
+ pushState,
+ setPageTitle,
+ findEntry,
+ createTemp,
+ createOrMergeEntry,
+} from '../utils';
+
+export const getTreeData = (
+ { commit, state, dispatch },
+ { endpoint = state.endpoints.rootEndpoint, tree = state } = {},
+) => {
+ commit(types.TOGGLE_LOADING, tree);
+
+ service.getTreeData(endpoint)
+ .then((res) => {
+ const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
+
+ setPageTitle(pageTitle);
+
+ return res.json();
+ })
+ .then((data) => {
+ const prevLastCommitPath = tree.lastCommitPath;
+ if (!state.isInitialRoot) {
+ commit(types.SET_ROOT, data.path === '/');
+ }
+
+ dispatch('updateDirectoryData', { data, tree });
+ commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
+ commit(types.SET_LAST_COMMIT_URL, { tree, url: data.last_commit_path });
+ commit(types.TOGGLE_LOADING, tree);
+
+ if (prevLastCommitPath !== null) {
+ dispatch('getLastCommitData', tree);
+ }
+
+ pushState(endpoint);
+ })
+ .catch(() => {
+ flash('Error loading tree data. Please try again.');
+ commit(types.TOGGLE_LOADING, tree);
+ });
+};
+
+export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
+ if (tree.opened) {
+ // send empty data to clear the tree
+ const data = { trees: [], blobs: [], submodules: [] };
+
+ pushState(tree.parentTreeUrl);
+
+ commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
+ dispatch('updateDirectoryData', { data, tree });
+ } else {
+ commit(types.SET_PREVIOUS_URL, endpoint);
+ dispatch('getTreeData', { endpoint, tree });
+ }
+
+ commit(types.TOGGLE_TREE_OPEN, tree);
+};
+
+export const clickedTreeRow = ({ commit, dispatch }, row) => {
+ if (row.type === 'tree') {
+ dispatch('toggleTreeOpen', {
+ endpoint: row.url,
+ tree: row,
+ });
+ } else if (row.type === 'submodule') {
+ commit(types.TOGGLE_LOADING, row);
+
+ gl.utils.visitUrl(row.url);
+ } else if (row.type === 'blob' && row.opened) {
+ dispatch('setFileActive', row);
+ } else {
+ dispatch('getFileData', row);
+ }
+};
+
+export const createTempTree = ({ state, commit, dispatch }, name) => {
+ let tree = state;
+ const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
+
+ dirNames.forEach((dirName) => {
+ const foundEntry = findEntry(tree, 'tree', dirName);
+
+ if (!foundEntry) {
+ const tmpEntry = createTemp({
+ name: dirName,
+ path: tree.path,
+ type: 'tree',
+ level: tree.level !== undefined ? tree.level + 1 : 0,
+ });
+
+ commit(types.CREATE_TMP_TREE, {
+ parent: tree,
+ tmpEntry,
+ });
+ commit(types.TOGGLE_TREE_OPEN, tmpEntry);
+
+ tree = tmpEntry;
+ } else {
+ tree = foundEntry;
+ }
+ });
+
+ if (tree.tempFile) {
+ dispatch('createTempFile', {
+ tree,
+ name: '.gitkeep',
+ });
+ }
+};
+
+export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
+ if (tree.lastCommitPath === null || getters.isCollapsed) return;
+
+ service.getTreeLastCommit(tree.lastCommitPath)
+ .then((res) => {
+ const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
+
+ commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
+
+ return res.json();
+ })
+ .then((data) => {
+ data.forEach((lastCommit) => {
+ const entry = findEntry(tree, lastCommit.type, lastCommit.file_name);
+
+ if (entry) {
+ commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
+ }
+ });
+
+ dispatch('getLastCommitData', tree);
+ })
+ .catch(() => flash('Error fetching log data.'));
+};
+
+export const updateDirectoryData = ({ commit, state }, { data, tree }) => {
+ const level = tree.level !== undefined ? tree.level + 1 : 0;
+ const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
+ const createEntry = (entry, type) => createOrMergeEntry({
+ tree,
+ entry,
+ level,
+ type,
+ parentTreeUrl,
+ });
+
+ const formattedData = [
+ ...data.trees.map(t => createEntry(t, 'tree')),
+ ...data.submodules.map(m => createEntry(m, 'submodule')),
+ ...data.blobs.map(b => createEntry(b, 'blob')),
+ ];
+
+ commit(types.SET_DIRECTORY_DATA, { tree, data: formattedData });
+};
diff --git a/app/assets/javascripts/repo/stores/getters.js b/app/assets/javascripts/repo/stores/getters.js
new file mode 100644
index 00000000000..5ce9f449905
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/getters.js
@@ -0,0 +1,40 @@
+import _ from 'underscore';
+
+/*
+ Takes the multi-dimensional tree and returns a flattened array.
+ This allows for the table to recursively render the table rows but keeps the data
+ structure nested to make it easier to add new files/directories.
+*/
+export const treeList = (state) => {
+ const mapTree = arr => (!arr.tree.length ? [] : _.map(arr.tree, a => [a, mapTree(a)]));
+
+ return _.chain(state.tree)
+ .map(arr => [arr, mapTree(arr)])
+ .flatten()
+ .value();
+};
+
+export const changedFiles = state => state.openFiles.filter(file => file.changed);
+
+export const activeFile = state => state.openFiles.find(file => file.active);
+
+export const activeFileExtension = (state) => {
+ const file = activeFile(state);
+ return file ? `.${file.path.split('.').pop()}` : '';
+};
+
+export const isCollapsed = state => !!state.openFiles.length;
+
+export const canEditFile = (state) => {
+ const currentActiveFile = activeFile(state);
+ const openedFiles = state.openFiles;
+
+ return state.canCommit &&
+ state.onTopOfBranch &&
+ openedFiles.length &&
+ (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
+};
+
+export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
+
+export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
diff --git a/app/assets/javascripts/repo/stores/index.js b/app/assets/javascripts/repo/stores/index.js
new file mode 100644
index 00000000000..6ac9bfd8189
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ state: state(),
+ actions,
+ mutations,
+ getters,
+});
diff --git a/app/assets/javascripts/repo/stores/mutation_types.js b/app/assets/javascripts/repo/stores/mutation_types.js
new file mode 100644
index 00000000000..bc3390f1506
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutation_types.js
@@ -0,0 +1,30 @@
+export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
+export const TOGGLE_LOADING = 'TOGGLE_LOADING';
+export const SET_COMMIT_REF = 'SET_COMMIT_REF';
+export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
+export const SET_ROOT = 'SET_ROOT';
+export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL';
+export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
+
+// Tree mutation types
+export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
+export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
+export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
+export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
+
+// File mutation types
+export const SET_FILE_DATA = 'SET_FILE_DATA';
+export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
+export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
+export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
+export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
+export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
+export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
+
+// Viewer mutation types
+export const SET_PREVIEW_MODE = 'SET_PREVIEW_MODE';
+export const SET_EDIT_MODE = 'SET_EDIT_MODE';
+export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
+export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
+
+export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
diff --git a/app/assets/javascripts/repo/stores/mutations.js b/app/assets/javascripts/repo/stores/mutations.js
new file mode 100644
index 00000000000..ae2ba5bedf7
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutations.js
@@ -0,0 +1,61 @@
+import * as types from './mutation_types';
+import fileMutations from './mutations/file';
+import treeMutations from './mutations/tree';
+import branchMutations from './mutations/branch';
+
+export default {
+ [types.SET_INITIAL_DATA](state, data) {
+ Object.assign(state, data);
+ },
+ [types.SET_PREVIEW_MODE](state) {
+ Object.assign(state, {
+ currentBlobView: 'repo-preview',
+ });
+ },
+ [types.SET_EDIT_MODE](state) {
+ Object.assign(state, {
+ currentBlobView: 'repo-editor',
+ });
+ },
+ [types.TOGGLE_LOADING](state, entry) {
+ Object.assign(entry, {
+ loading: !entry.loading,
+ });
+ },
+ [types.TOGGLE_EDIT_MODE](state) {
+ Object.assign(state, {
+ editMode: !state.editMode,
+ });
+ },
+ [types.TOGGLE_DISCARD_POPUP](state, discardPopupOpen) {
+ Object.assign(state, {
+ discardPopupOpen,
+ });
+ },
+ [types.SET_COMMIT_REF](state, ref) {
+ Object.assign(state, {
+ currentRef: ref,
+ });
+ },
+ [types.SET_ROOT](state, isRoot) {
+ Object.assign(state, {
+ isRoot,
+ isInitialRoot: isRoot,
+ });
+ },
+ [types.SET_PREVIOUS_URL](state, previousUrl) {
+ Object.assign(state, {
+ previousUrl,
+ });
+ },
+ [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
+ Object.assign(entry.lastCommit, {
+ url: lastCommit.commit_path,
+ message: lastCommit.commit.message,
+ updatedAt: lastCommit.commit.authored_date,
+ });
+ },
+ ...fileMutations,
+ ...treeMutations,
+ ...branchMutations,
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/branch.js b/app/assets/javascripts/repo/stores/mutations/branch.js
new file mode 100644
index 00000000000..d8229e8a620
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutations/branch.js
@@ -0,0 +1,9 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_BRANCH](state, currentBranch) {
+ Object.assign(state, {
+ currentBranch,
+ });
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/file.js b/app/assets/javascripts/repo/stores/mutations/file.js
new file mode 100644
index 00000000000..f9ba80b9dc2
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutations/file.js
@@ -0,0 +1,54 @@
+import * as types from '../mutation_types';
+import { findIndexOfFile } from '../utils';
+
+export default {
+ [types.SET_FILE_ACTIVE](state, { file, active }) {
+ Object.assign(file, {
+ active,
+ });
+ },
+ [types.TOGGLE_FILE_OPEN](state, file) {
+ Object.assign(file, {
+ opened: !file.opened,
+ });
+
+ if (file.opened) {
+ state.openFiles.push(file);
+ } else {
+ state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1);
+ }
+ },
+ [types.SET_FILE_DATA](state, { data, file }) {
+ Object.assign(file, {
+ blamePath: data.blame_path,
+ commitsPath: data.commits_path,
+ permalink: data.permalink,
+ rawPath: data.raw_path,
+ binary: data.binary,
+ html: data.html,
+ renderError: data.render_error,
+ });
+ },
+ [types.SET_FILE_RAW_DATA](state, { file, raw }) {
+ Object.assign(file, {
+ raw,
+ });
+ },
+ [types.UPDATE_FILE_CONTENT](state, { file, content }) {
+ const changed = content !== file.raw;
+
+ Object.assign(file, {
+ content,
+ changed,
+ });
+ },
+ [types.DISCARD_FILE_CHANGES](state, file) {
+ Object.assign(file, {
+ content: '',
+ changed: false,
+ });
+ },
+ [types.CREATE_TMP_FILE](state, { file, parent }) {
+ parent.tree.push(file);
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/tree.js b/app/assets/javascripts/repo/stores/mutations/tree.js
new file mode 100644
index 00000000000..130221c9fda
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/mutations/tree.js
@@ -0,0 +1,27 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.TOGGLE_TREE_OPEN](state, tree) {
+ Object.assign(tree, {
+ opened: !tree.opened,
+ });
+ },
+ [types.SET_DIRECTORY_DATA](state, { data, tree }) {
+ Object.assign(tree, {
+ tree: data,
+ });
+ },
+ [types.SET_PARENT_TREE_URL](state, url) {
+ Object.assign(state, {
+ parentTreeUrl: url,
+ });
+ },
+ [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
+ Object.assign(tree, {
+ lastCommitPath: url,
+ });
+ },
+ [types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
+ parent.tree.push(tmpEntry);
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js
deleted file mode 100644
index 93b39cff27e..00000000000
--- a/app/assets/javascripts/repo/stores/repo_store.js
+++ /dev/null
@@ -1,203 +0,0 @@
-/* global Flash */
-import Helper from '../helpers/repo_helper';
-import Service from '../services/repo_service';
-
-const RepoStore = {
- monaco: {},
- monacoLoading: false,
- service: '',
- canCommit: false,
- onTopOfBranch: false,
- editMode: false,
- isTree: false,
- isRoot: false,
- prevURL: '',
- projectId: '',
- projectName: '',
- projectUrl: '',
- blobRaw: '',
- currentBlobView: 'repo-preview',
- openedFiles: [],
- submitCommitsLoading: false,
- dialog: {
- open: false,
- title: '',
- status: false,
- },
- activeFile: Helper.getDefaultActiveFile(),
- activeFileIndex: 0,
- activeLine: -1,
- activeFileLabel: 'Raw',
- files: [],
- isCommitable: false,
- binary: false,
- currentBranch: '',
- commitMessage: '',
- binaryTypes: {
- png: false,
- md: false,
- svg: false,
- unknown: false,
- },
- loading: {
- tree: false,
- blob: false,
- },
-
- resetBinaryTypes() {
- Object.keys(RepoStore.binaryTypes).forEach((key) => {
- RepoStore.binaryTypes[key] = false;
- });
- },
-
- // mutations
- checkIsCommitable() {
- RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
- },
-
- addFilesToDirectory(inDirectory, currentList, newList) {
- RepoStore.files = Helper.getNewMergedList(inDirectory, currentList, newList);
- },
-
- toggleRawPreview() {
- RepoStore.activeFile.raw = !RepoStore.activeFile.raw;
- RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source';
- },
-
- setActiveFiles(file) {
- if (RepoStore.isActiveFile(file)) return;
- RepoStore.openedFiles = RepoStore.openedFiles
- .map((openedFile, i) => RepoStore.setFileActivity(file, openedFile, i));
-
- RepoStore.setActiveToRaw();
-
- if (file.binary) {
- RepoStore.blobRaw = file.base64;
- } else if (file.newContent || file.plain) {
- RepoStore.blobRaw = file.newContent || file.plain;
- } else {
- Service.getRaw(file.raw_path)
- .then((rawResponse) => {
- RepoStore.blobRaw = rawResponse.data;
- Helper.findOpenedFileFromActive().plain = rawResponse.data;
- }).catch(Helper.loadingError);
- }
-
- if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name);
- RepoStore.binary = file.binary;
- RepoStore.setActiveLine(-1);
- },
-
- setFileActivity(file, openedFile, i) {
- const activeFile = openedFile;
- activeFile.active = file.url === activeFile.url;
-
- if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
-
- return activeFile;
- },
-
- setActiveFile(activeFile, i) {
- RepoStore.activeFile = Object.assign({}, RepoStore.activeFile, activeFile);
- RepoStore.activeFileIndex = i;
- },
-
- setActiveLine(activeLine) {
- if (!isNaN(activeLine)) RepoStore.activeLine = activeLine;
- },
-
- setActiveToRaw() {
- RepoStore.activeFile.raw = false;
- // can't get vue to listen to raw for some reason so RepoStore for now.
- RepoStore.activeFileLabel = 'Display source';
- },
-
- removeChildFilesOfTree(tree) {
- let foundTree = false;
- const treeToClose = tree;
- let canStopSearching = false;
- RepoStore.files = RepoStore.files.filter((file) => {
- const isItTheTreeWeWant = file.url === treeToClose.url;
- // if it's the next tree
- if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
- canStopSearching = true;
- return true;
- }
- if (canStopSearching) return true;
-
- if (isItTheTreeWeWant) foundTree = true;
-
- if (foundTree) return file.level <= treeToClose.level;
- return true;
- });
-
- treeToClose.opened = false;
- treeToClose.icon = 'fa-folder';
- return treeToClose;
- },
-
- removeFromOpenedFiles(file) {
- if (file.type === 'tree') return;
- let foundIndex;
- RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => {
- if (openedFile.path === file.path) foundIndex = i;
- return openedFile.path !== file.path;
- });
-
- // now activate the right tab based on what you closed.
- if (RepoStore.openedFiles.length === 0) {
- RepoStore.activeFile = {};
- return;
- }
-
- if (RepoStore.openedFiles.length === 1 || foundIndex === 0) {
- RepoStore.setActiveFiles(RepoStore.openedFiles[0]);
- return;
- }
-
- if (foundIndex && foundIndex > 0) {
- RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
- }
- },
-
- addToOpenedFiles(file) {
- const openFile = file;
-
- const openedFilesAlreadyExists = RepoStore.openedFiles
- .some(openedFile => openedFile.path === openFile.path);
-
- if (openedFilesAlreadyExists) return;
-
- openFile.changed = false;
- RepoStore.openedFiles.push(openFile);
- },
-
- setActiveFileContents(contents) {
- if (!RepoStore.editMode) return;
- const currentFile = RepoStore.openedFiles[RepoStore.activeFileIndex];
- RepoStore.activeFile.newContent = contents;
- RepoStore.activeFile.changed = RepoStore.activeFile.plain !== RepoStore.activeFile.newContent;
- currentFile.changed = RepoStore.activeFile.changed;
- currentFile.newContent = contents;
- },
-
- toggleBlobView() {
- RepoStore.currentBlobView = RepoStore.isPreviewView() ? 'repo-editor' : 'repo-preview';
- },
-
- setViewToPreview() {
- RepoStore.currentBlobView = 'repo-preview';
- },
-
- // getters
-
- isActiveFile(file) {
- return file && file.url === RepoStore.activeFile.url;
- },
-
- isPreviewView() {
- return RepoStore.currentBlobView === 'repo-preview';
- },
-};
-
-export default RepoStore;
diff --git a/app/assets/javascripts/repo/stores/state.js b/app/assets/javascripts/repo/stores/state.js
new file mode 100644
index 00000000000..0068834831e
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/state.js
@@ -0,0 +1,24 @@
+export default () => ({
+ canCommit: false,
+ currentBranch: '',
+ currentBlobView: 'repo-preview',
+ currentRef: '',
+ discardPopupOpen: false,
+ editMode: false,
+ endpoints: {},
+ isRoot: false,
+ isInitialRoot: false,
+ lastCommitPath: '',
+ loading: false,
+ onTopOfBranch: false,
+ openFiles: [],
+ path: '',
+ project: {
+ id: 0,
+ name: '',
+ url: '',
+ },
+ parentTreeUrl: '',
+ previousUrl: '',
+ tree: [],
+});
diff --git a/app/assets/javascripts/repo/stores/utils.js b/app/assets/javascripts/repo/stores/utils.js
new file mode 100644
index 00000000000..fae1f4439a9
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/utils.js
@@ -0,0 +1,127 @@
+export const dataStructure = () => ({
+ id: '',
+ key: '',
+ type: '',
+ name: '',
+ url: '',
+ path: '',
+ level: 0,
+ tempFile: false,
+ icon: '',
+ tree: [],
+ loading: false,
+ opened: false,
+ active: false,
+ changed: false,
+ lastCommitPath: '',
+ lastCommit: {
+ url: '',
+ message: '',
+ updatedAt: '',
+ },
+ tree_url: '',
+ blamePath: '',
+ commitsPath: '',
+ permalink: '',
+ rawPath: '',
+ binary: false,
+ html: '',
+ raw: '',
+ content: '',
+ parentTreeUrl: '',
+ renderError: false,
+ base64: false,
+});
+
+export const decorateData = (entity) => {
+ const {
+ id,
+ type,
+ url,
+ name,
+ icon,
+ tree_url,
+ path,
+ renderError,
+ content = '',
+ tempFile = false,
+ active = false,
+ opened = false,
+ changed = false,
+ parentTreeUrl = '',
+ level = 0,
+ base64 = false,
+ } = entity;
+
+ return {
+ ...dataStructure(),
+ id,
+ key: `${name}-${type}-${id}`,
+ type,
+ name,
+ url,
+ tree_url,
+ path,
+ level,
+ tempFile,
+ icon: `fa-${icon}`,
+ opened,
+ active,
+ parentTreeUrl,
+ changed,
+ renderError,
+ content,
+ base64,
+ };
+};
+
+export const findEntry = (state, type, name) => state.tree.find(
+ f => f.type === type && f.name === name,
+);
+export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
+
+export const setPageTitle = (title) => {
+ document.title = title;
+};
+
+export const pushState = (url) => {
+ history.pushState({ url }, '', url);
+};
+
+export const createTemp = ({ name, path, type, level, changed, content, base64 }) => {
+ const treePath = path ? `${path}/${name}` : name;
+
+ return decorateData({
+ id: new Date().getTime().toString(),
+ name,
+ type,
+ tempFile: true,
+ path: treePath,
+ icon: type === 'tree' ? 'folder' : 'file-text-o',
+ changed,
+ content,
+ parentTreeUrl: '',
+ level,
+ base64,
+ renderError: base64,
+ });
+};
+
+export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) => {
+ const found = findEntry(tree, type, entry.name);
+
+ if (found) {
+ return Object.assign({}, found, {
+ id: entry.id,
+ url: entry.url,
+ tempFile: false,
+ });
+ }
+
+ return decorateData({
+ ...entry,
+ type,
+ parentTreeUrl,
+ level,
+ });
+};
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index 05caf177aec..07fee53d814 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -1,5 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */
-/* global Flash */
+import Flash from './flash';
import Api from './api';
(function() {
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index f15452ec683..9dec5d7645a 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -162,13 +162,19 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
items = [
{
header: "" + name
- }, {
+ }
+ ];
+ const issueItems = [
+ {
text: 'Issues assigned to me',
url: issuesPath + "/?assignee_username=" + userName
}, {
text: "Issues I've created",
url: issuesPath + "/?author_username=" + userName
- }, 'separator', {
+ }
+ ];
+ const mergeRequestItems = [
+ {
text: 'Merge requests assigned to me',
url: mrPath + "/?assignee_username=" + userName
}, {
@@ -176,6 +182,11 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
url: mrPath + "/?author_username=" + userName
}
];
+ if (options.issuesDisabled) {
+ items = items.concat(mergeRequestItems);
+ } else {
+ items = items.concat(...issueItems, 'separator', ...mergeRequestItems);
+ }
if (!name) {
items.splice(0, 1);
}
@@ -408,6 +419,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'),
issuesPath: $projectOptionsDataEl.data('issues-path'),
+ issuesDisabled: $projectOptionsDataEl.data('issues-disabled'),
mrPath: $projectOptionsDataEl.data('mr-path')
};
}
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index 8635ccece6e..d34a21b37e1 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -1,34 +1,26 @@
-function expandSectionParent($section, $content) {
- $section.addClass('expanded');
- $content.off('animationend.expandSectionParent');
-}
-
function expandSection($section) {
$section.find('.js-settings-toggle').text('Collapse');
-
- const $content = $section.find('.settings-content');
- $content.addClass('expanded').off('scroll.expandSection').scrollTop(0);
-
- if ($content.hasClass('no-animate')) {
- expandSectionParent($section, $content);
- } else {
- $content.on('animationend.expandSectionParent', () => expandSectionParent($section, $content));
+ $section.find('.settings-content').off('scroll.expandSection').scrollTop(0);
+ $section.addClass('expanded');
+ if (!$section.hasClass('no-animate')) {
+ $section.addClass('animating')
+ .one('animationend.animateSection', () => $section.removeClass('animating'));
}
}
function closeSection($section) {
$section.find('.js-settings-toggle').text('Expand');
-
- const $content = $section.find('.settings-content');
- $content.removeClass('expanded').on('scroll.expandSection', () => expandSection($section));
-
+ $section.find('.settings-content').on('scroll.expandSection', () => expandSection($section));
$section.removeClass('expanded');
+ if (!$section.hasClass('no-animate')) {
+ $section.addClass('animating')
+ .one('animationend.animateSection', () => $section.removeClass('animating'));
+ }
}
function toggleSection($section) {
- const $content = $section.find('.settings-content');
- $content.removeClass('no-animate');
- if ($content.hasClass('expanded')) {
+ $section.removeClass('no-animate');
+ if ($section.hasClass('expanded')) {
closeSection($section);
} else {
expandSection($section);
@@ -39,10 +31,19 @@ export default function initSettingsPanels() {
$('.settings').each((i, elm) => {
const $section = $(elm);
$section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section));
- $section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section));
+
+ if (!$section.hasClass('expanded')) {
+ $section.find('.settings-content').on('scroll.expandSection', () => {
+ $section.removeClass('no-animate');
+ expandSection($section);
+ });
+ }
});
if (location.hash) {
- expandSection($(location.hash));
+ const $target = $(location.hash);
+ if ($target.length && $target.hasClass('.settings')) {
+ expandSection($target);
+ }
}
}
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index f63b99ba1c5..ebe7a99ffae 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,128 +1,116 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
-/* global Mousetrap */
import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
-
import findAndFollowLink from './shortcuts_dashboard_navigation';
-(function() {
- this.Shortcuts = (function() {
- function Shortcuts(skipResetBindings) {
- this.onToggleHelp = this.onToggleHelp.bind(this);
- this.enabledHelp = [];
- if (!skipResetBindings) {
- Mousetrap.reset();
- }
- Mousetrap.bind('?', this.onToggleHelp);
- Mousetrap.bind('s', Shortcuts.focusSearch);
- Mousetrap.bind('f', (e => this.focusFilter(e)));
- Mousetrap.bind('p b', this.onTogglePerfBar);
-
- const findFileURL = document.body.dataset.findFile;
-
- Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
- Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
- Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
- Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests'));
- Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects'));
- Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups'));
- Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones'));
- Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets'));
-
- Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview);
- if (typeof findFileURL !== "undefined" && findFileURL !== null) {
- Mousetrap.bind('t', function() {
- return gl.utils.visitUrl(findFileURL);
- });
- }
+const defaultStopCallback = Mousetrap.stopCallback;
+Mousetrap.stopCallback = (e, element, combo) => {
+ if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
+ return false;
+ }
+
+ return defaultStopCallback(e, element, combo);
+};
+
+export default class Shortcuts {
+ constructor(skipResetBindings) {
+ this.onToggleHelp = this.onToggleHelp.bind(this);
+ this.enabledHelp = [];
+ if (!skipResetBindings) {
+ Mousetrap.reset();
}
+ Mousetrap.bind('?', this.onToggleHelp);
+ Mousetrap.bind('s', Shortcuts.focusSearch);
+ Mousetrap.bind('f', this.focusFilter.bind(this));
+ Mousetrap.bind('p b', Shortcuts.onTogglePerfBar);
- Shortcuts.prototype.onToggleHelp = function(e) {
- e.preventDefault();
- return Shortcuts.toggleHelp(this.enabledHelp);
- };
+ const findFileURL = document.body.dataset.findFile;
+
+ Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
+ Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
+ Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
+ Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests'));
+ Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects'));
+ Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups'));
+ Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones'));
+ Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets'));
+
+ Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], Shortcuts.toggleMarkdownPreview);
- Shortcuts.prototype.onTogglePerfBar = function(e) {
+ if (typeof findFileURL !== 'undefined' && findFileURL !== null) {
+ Mousetrap.bind('t', () => {
+ gl.utils.visitUrl(findFileURL);
+ });
+ }
+
+ $(document).on('click.more_help', '.js-more-help-button', function clickMoreHelp(e) {
+ $(this).remove();
+ $('.hidden-shortcut').show();
e.preventDefault();
- const performanceBarCookieName = 'perf_bar_enabled';
- if (Cookies.get(performanceBarCookieName) === 'true') {
- Cookies.remove(performanceBarCookieName, { path: '/' });
- } else {
- Cookies.set(performanceBarCookieName, 'true', { path: '/' });
- }
- gl.utils.refreshCurrentPage();
- };
-
- Shortcuts.prototype.toggleMarkdownPreview = function(e) {
- // Check if short-cut was triggered while in Write Mode
- const $target = $(e.target);
- const $form = $target.closest('form');
-
- if ($target.hasClass('js-note-text')) {
- $('.js-md-preview-button', $form).focus();
- }
- return $(document).triggerHandler('markdown-preview:toggle', [e]);
- };
-
- Shortcuts.toggleHelp = function(location) {
- var $modal;
- $modal = $('#modal-shortcuts');
- if ($modal.length) {
- $modal.modal('toggle');
- return;
- }
- return $.ajax({
- url: gon.shortcuts_path,
- dataType: 'script',
- success: function(e) {
- var i, l, len, results;
- if (location && location.length > 0) {
- results = [];
- for (i = 0, len = location.length; i < len; i += 1) {
- l = location[i];
- results.push($(l).show());
- }
- return results;
- } else {
- $('.hidden-shortcut').show();
- return $('.js-more-help-button').remove();
+ });
+ }
+
+ onToggleHelp(e) {
+ e.preventDefault();
+ Shortcuts.toggleHelp(this.enabledHelp);
+ }
+
+ static onTogglePerfBar(e) {
+ e.preventDefault();
+ const performanceBarCookieName = 'perf_bar_enabled';
+ if (Cookies.get(performanceBarCookieName) === 'true') {
+ Cookies.remove(performanceBarCookieName, { path: '/' });
+ } else {
+ Cookies.set(performanceBarCookieName, 'true', { path: '/' });
+ }
+ gl.utils.refreshCurrentPage();
+ }
+
+ static toggleMarkdownPreview(e) {
+ // Check if short-cut was triggered while in Write Mode
+ const $target = $(e.target);
+ const $form = $target.closest('form');
+
+ if ($target.hasClass('js-note-text')) {
+ $('.js-md-preview-button', $form).focus();
+ }
+ $(document).triggerHandler('markdown-preview:toggle', [e]);
+ }
+
+ static toggleHelp(location) {
+ const $modal = $('#modal-shortcuts');
+
+ if ($modal.length) {
+ $modal.modal('toggle');
+ }
+
+ $.ajax({
+ url: gon.shortcuts_path,
+ dataType: 'script',
+ success() {
+ if (location && location.length > 0) {
+ const results = [];
+ for (let i = 0, len = location.length; i < len; i += 1) {
+ results.push($(location[i]).show());
}
+ return results;
}
- });
- };
-
- Shortcuts.prototype.focusFilter = function(e) {
- if (this.filterInput == null) {
- this.filterInput = $('input[type=search]', '.nav-controls');
- }
- this.filterInput.focus();
- return e.preventDefault();
- };
-
- Shortcuts.focusSearch = function(e) {
- $('#search').focus();
- return e.preventDefault();
- };
-
- return Shortcuts;
- })();
-
- $(document).on('click.more_help', '.js-more-help-button', function(e) {
- $(this).remove();
- $('.hidden-shortcut').show();
- return e.preventDefault();
- });
-
- Mousetrap.stopCallback = (function() {
- var defaultStopCallback;
- defaultStopCallback = Mousetrap.stopCallback;
- return function(e, element, combo) {
- // allowed shortcuts if textarea, input, contenteditable are focused
- if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
- return false;
- } else {
- return defaultStopCallback.apply(this, arguments);
- }
- };
- })();
-}).call(window);
+
+ $('.hidden-shortcut').show();
+ return $('.js-more-help-button').remove();
+ },
+ });
+ }
+
+ focusFilter(e) {
+ if (!this.filterInput) {
+ this.filterInput = $('input[type=search]', '.nav-controls');
+ }
+ this.filterInput.focus();
+ e.preventDefault();
+ }
+
+ static focusSearch(e) {
+ $('#search').focus();
+ e.preventDefault();
+ }
+}
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
index ccbf7c59165..fbc57bb4304 100644
--- a/app/assets/javascripts/shortcuts_blob.js
+++ b/app/assets/javascripts/shortcuts_blob.js
@@ -1,7 +1,6 @@
/* global Mousetrap */
-/* global Shortcuts */
-import './shortcuts';
+import Shortcuts from './shortcuts';
const defaults = {
skipResetBindings: false,
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index b18b6139b35..81286c0010c 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -1,38 +1,30 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife */
/* global Mousetrap */
-/* global ShortcutsNavigation */
-import './shortcuts_navigation';
+import ShortcutsNavigation from './shortcuts_navigation';
-(function() {
- var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
+export default class ShortcutsFindFile extends ShortcutsNavigation {
+ constructor(projectFindFile) {
+ super();
- this.ShortcutsFindFile = (function(superClass) {
- extend(ShortcutsFindFile, superClass);
+ const oldStopCallback = Mousetrap.stopCallback;
+ this.projectFindFile = projectFindFile;
- function ShortcutsFindFile(projectFindFile) {
- var _oldStopCallback;
- this.projectFindFile = projectFindFile;
- ShortcutsFindFile.__super__.constructor.call(this);
- _oldStopCallback = Mousetrap.stopCallback;
- Mousetrap.stopCallback = (function(_this) {
- // override to fire shortcuts action when focus in textbox
- return function(event, element, combo) {
- if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) {
- // when press up/down key in textbox, cusor prevent to move to home/end
- event.preventDefault();
- return false;
- }
- return _oldStopCallback(event, element, combo);
- };
- })(this);
- Mousetrap.bind('up', this.projectFindFile.selectRowUp);
- Mousetrap.bind('down', this.projectFindFile.selectRowDown);
- Mousetrap.bind('esc', this.projectFindFile.goToTree);
- Mousetrap.bind('enter', this.projectFindFile.goToBlob);
- }
+ Mousetrap.stopCallback = (e, element, combo) => {
+ if (
+ element === this.projectFindFile.inputElement[0] &&
+ (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')
+ ) {
+ // when press up/down key in textbox, cusor prevent to move to home/end
+ event.preventDefault();
+ return false;
+ }
- return ShortcutsFindFile;
- })(ShortcutsNavigation);
-}).call(window);
+ return oldStopCallback(e, element, combo);
+ };
+
+ Mousetrap.bind('up', this.projectFindFile.selectRowUp);
+ Mousetrap.bind('down', this.projectFindFile.selectRowDown);
+ Mousetrap.bind('esc', this.projectFindFile.goToTree);
+ Mousetrap.bind('enter', this.projectFindFile.goToBlob);
+ }
+}
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 78b257bf192..4f4f606d293 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,100 +1,75 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */
/* global Mousetrap */
-/* global ShortcutsNavigation */
/* global sidebar */
import _ from 'underscore';
import 'mousetrap';
-import './shortcuts_navigation';
-
-(function() {
- var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
- this.ShortcutsIssuable = (function(superClass) {
- extend(ShortcutsIssuable, superClass);
-
- function ShortcutsIssuable(isMergeRequest) {
- ShortcutsIssuable.__super__.constructor.call(this);
- Mousetrap.bind('a', this.openSidebarDropdown.bind(this, 'assignee'));
- Mousetrap.bind('m', this.openSidebarDropdown.bind(this, 'milestone'));
- Mousetrap.bind('r', (function(_this) {
- return function() {
- _this.replyWithSelectedText(isMergeRequest);
- return false;
- };
- })(this));
- Mousetrap.bind('e', (function(_this) {
- return function() {
- _this.editIssue();
- return false;
- };
- })(this));
- Mousetrap.bind('l', this.openSidebarDropdown.bind(this, 'labels'));
- if (isMergeRequest) {
- this.enabledHelp.push('.hidden-shortcut.merge_requests');
- } else {
- this.enabledHelp.push('.hidden-shortcut.issues');
- }
+import ShortcutsNavigation from './shortcuts_navigation';
+import { CopyAsGFM } from './behaviors/copy_as_gfm';
+
+export default class ShortcutsIssuable extends ShortcutsNavigation {
+ constructor(isMergeRequest) {
+ super();
+
+ this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form');
+ this.editBtn = document.querySelector('.issuable-edit');
+
+ Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
+ Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
+ Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
+ Mousetrap.bind('r', this.replyWithSelectedText.bind(this));
+ Mousetrap.bind('e', this.editIssue.bind(this));
+
+ if (isMergeRequest) {
+ this.enabledHelp.push('.hidden-shortcut.merge_requests');
+ } else {
+ this.enabledHelp.push('.hidden-shortcut.issues');
}
+ }
- ShortcutsIssuable.prototype.replyWithSelectedText = function(isMergeRequest) {
- var quote, documentFragment, el, selected, separator;
- 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) {
- replyField.focus();
- return;
- }
-
- el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
- selected = window.gl.CopyAsGFM.nodeToGFM(el);
-
- if (selected.trim() === "") {
- return;
- }
- 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) {
- return current + separator + quote.join('') + "\n";
- });
-
- // Trigger autosave
- replyField.trigger('input').trigger('change');
-
- // Trigger autosize
- var event = document.createEvent('Event');
- event.initEvent('autosize:update', true, false);
- replyField.get(0).dispatchEvent(event);
-
- // Focus the input field
- return replyField.focus();
- };
-
- ShortcutsIssuable.prototype.editIssue = function() {
- var $editBtn;
- $editBtn = $('.issuable-edit');
- // Need to click the element as on issues, editing is inline
- // on merge request, editing is on a different page
- $editBtn.get(0).click();
- };
-
- ShortcutsIssuable.prototype.openSidebarDropdown = function(name) {
- sidebar.openDropdown(name);
+ replyWithSelectedText() {
+ const documentFragment = window.gl.utils.getSelectedFragment();
+
+ if (!documentFragment) {
+ this.$replyField.focus();
return false;
- };
+ }
+
+ const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
+ const selected = CopyAsGFM.nodeToGFM(el);
+
+ if (selected.trim() === '') {
+ return false;
+ }
+
+ const quote = _.map(selected.split('\n'), val => `${(`> ${val}`).trim()}\n`);
+
+ // If replyField already has some content, add a newline before our quote
+ const separator = (this.$replyField.val().trim() !== '' && '\n\n') || '';
+ this.$replyField.val((a, current) => `${current}${separator}${quote.join('')}\n`)
+ .trigger('input')
+ .trigger('change');
+
+ // Trigger autosize
+ const event = document.createEvent('Event');
+ event.initEvent('autosize:update', true, false);
+ this.$replyField.get(0).dispatchEvent(event);
+
+ // Focus the input field
+ this.$replyField.focus();
+
+ return false;
+ }
+
+ editIssue() {
+ // Need to click the element as on issues, editing is inline
+ // on merge request, editing is on a different page
+ this.editBtn.click();
+
+ return false;
+ }
- return ShortcutsIssuable;
- })(ShortcutsNavigation);
-}).call(window);
+ static openSidebarDropdown(name) {
+ sidebar.openDropdown(name);
+ return false;
+ }
+}
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 55bae0c08a1..b4562701a3e 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -1,36 +1,27 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */
/* global Mousetrap */
-/* global Shortcuts */
import findAndFollowLink from './shortcuts_dashboard_navigation';
-import './shortcuts';
+import Shortcuts from './shortcuts';
-(function() {
- var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
+export default class ShortcutsNavigation extends Shortcuts {
+ constructor() {
+ super();
- this.ShortcutsNavigation = (function(superClass) {
- extend(ShortcutsNavigation, superClass);
+ Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
+ Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
+ Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
+ Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
+ Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
+ Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network'));
+ Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts'));
+ Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
+ Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
+ Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
+ Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
+ Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
+ Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
+ Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
- function ShortcutsNavigation() {
- ShortcutsNavigation.__super__.constructor.call(this);
- Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
- Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
- Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
- Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
- Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
- Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network'));
- Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts'));
- Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
- Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
- Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
- Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
- Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
- Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
- Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
- this.enabledHelp.push('.hidden-shortcut.project');
- }
-
- return ShortcutsNavigation;
- })(Shortcuts);
-}).call(window);
+ this.enabledHelp.push('.hidden-shortcut.project');
+ }
+}
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
index cc44082efa9..21823085ac4 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -1,28 +1,17 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, max-len */
/* global Mousetrap */
-/* global ShortcutsNavigation */
+import ShortcutsNavigation from './shortcuts_navigation';
-import './shortcuts_navigation';
+export default class ShortcutsNetwork extends ShortcutsNavigation {
+ constructor(graph) {
+ super();
-(function() {
- var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
+ Mousetrap.bind(['left', 'h'], graph.scrollLeft);
+ Mousetrap.bind(['right', 'l'], graph.scrollRight);
+ Mousetrap.bind(['up', 'k'], graph.scrollUp);
+ Mousetrap.bind(['down', 'j'], graph.scrollDown);
+ Mousetrap.bind(['shift+up', 'shift+k'], graph.scrollTop);
+ Mousetrap.bind(['shift+down', 'shift+j'], graph.scrollBottom);
- this.ShortcutsNetwork = (function(superClass) {
- extend(ShortcutsNetwork, superClass);
-
- function ShortcutsNetwork(graph) {
- this.graph = graph;
- ShortcutsNetwork.__super__.constructor.call(this);
- Mousetrap.bind(['left', 'h'], this.graph.scrollLeft);
- Mousetrap.bind(['right', 'l'], this.graph.scrollRight);
- Mousetrap.bind(['up', 'k'], this.graph.scrollUp);
- Mousetrap.bind(['down', 'j'], this.graph.scrollDown);
- Mousetrap.bind(['shift+up', 'shift+k'], this.graph.scrollTop);
- Mousetrap.bind(['shift+down', 'shift+j'], this.graph.scrollBottom);
- this.enabledHelp.push('.hidden-shortcut.network');
- }
-
- return ShortcutsNetwork;
- })(ShortcutsNavigation);
-}).call(window);
+ this.enabledHelp.push('.hidden-shortcut.network');
+ }
+}
diff --git a/app/assets/javascripts/shortcuts_wiki.js b/app/assets/javascripts/shortcuts_wiki.js
index 8a075062a48..59b967dbe09 100644
--- a/app/assets/javascripts/shortcuts_wiki.js
+++ b/app/assets/javascripts/shortcuts_wiki.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
/* global Mousetrap */
-/* global ShortcutsNavigation */
+import ShortcutsNavigation from './shortcuts_navigation';
import findAndFollowLink from './shortcuts_dashboard_navigation';
export default class ShortcutsWiki extends ShortcutsNavigation {
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
index f83c3b037ed..74c17bc14a2 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
@@ -1,5 +1,4 @@
-/* global Flash */
-
+import Flash from '../../../flash';
import AssigneeTitle from './assignee_title';
import Assignees from './assignees';
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index f2b1099a678..6ee4d487c0b 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,10 +1,12 @@
<script>
-/* global Flash */
+import Flash from '../../../flash';
import editForm from './edit_form.vue';
+import Icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
editForm,
+ Icon,
},
props: {
isConfidential: {
@@ -26,11 +28,8 @@ export default {
};
},
computed: {
- faEye() {
- const eye = this.isConfidential ? 'fa-eye-slash' : 'fa-eye';
- return {
- [eye]: true,
- };
+ confidentialityIcon() {
+ return this.isConfidential ? 'eye-slash' : 'eye';
},
},
methods: {
@@ -49,7 +48,11 @@ export default {
<template>
<div class="block issuable-sidebar-item confidentiality">
<div class="sidebar-collapsed-icon">
- <i class="fa" :class="faEye" aria-hidden="true"></i>
+ <icon
+ :name="confidentialityIcon"
+ :size="16"
+ aria-hidden="true">
+ </icon>
</div>
<div class="title hide-collapsed">
Confidentiality
@@ -70,11 +73,21 @@ export default {
:update-confidential-attribute="updateConfidentialAttribute"
/>
<div v-if="!isConfidential" class="no-value sidebar-item-value">
- <i class="fa fa-eye sidebar-item-icon"></i>
+ <icon
+ name="eye"
+ :size="16"
+ aria-hidden="true"
+ class="sidebar-item-icon inline">
+ </icon>
Not confidential
</div>
<div v-else class="value sidebar-item-value hide-collapsed">
- <i aria-hidden="true" class="fa fa-eye-slash sidebar-item-icon is-active"></i>
+ <icon
+ name="eye-slash"
+ :size="16"
+ aria-hidden="true"
+ class="sidebar-item-icon inline is-active">
+ </icon>
This issue is confidential
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index c4b2900e020..9aff53cf8af 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -2,6 +2,7 @@
/* global Flash */
import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
+import Icon from '../../../vue_shared/components/icon.vue';
export default {
props: {
@@ -35,11 +36,12 @@ export default {
components: {
editForm,
+ Icon,
},
computed: {
- lockIconClass() {
- return this.isLocked ? 'fa-lock' : 'fa-unlock';
+ lockIcon() {
+ return this.isLocked ? 'lock' : 'lock-open';
},
isLockDialogOpen() {
@@ -66,11 +68,12 @@ export default {
<template>
<div class="block issuable-sidebar-item lock">
<div class="sidebar-collapsed-icon">
- <i
- class="fa"
- :class="lockIconClass"
+ <icon
+ :name="lockIcon"
+ :size="16"
aria-hidden="true"
- ></i>
+ class="sidebar-item-icon is-active">
+ </icon>
</div>
<div class="title hide-collapsed">
@@ -98,10 +101,12 @@ export default {
v-if="isLocked"
class="value sidebar-item-value"
>
- <i
+ <icon
+ name="lock"
+ :size="16"
aria-hidden="true"
- class="fa fa-lock sidebar-item-icon is-active"
- ></i>
+ class="sidebar-item-icon inline is-active">
+ </icon>
{{ __('Locked') }}
</div>
@@ -109,10 +114,12 @@ export default {
v-else
class="no-value sidebar-item-value hide-collapsed"
>
- <i
+ <icon
+ name="lock-open"
+ :size="16"
aria-hidden="true"
- class="fa fa-unlock sidebar-item-icon"
- ></i>
+ class="sidebar-item-icon inline">
+ </icon>
{{ __('Unlocked') }}
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
new file mode 100644
index 00000000000..b8510a6ce3a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -0,0 +1,125 @@
+<script>
+import { __, n__, sprintf } from '../../../locale';
+import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue';
+
+export default {
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ participants: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ numberOfLessParticipants: {
+ type: Number,
+ required: false,
+ default: 7,
+ },
+ },
+ data() {
+ return {
+ isShowingMoreParticipants: false,
+ };
+ },
+ components: {
+ loadingIcon,
+ userAvatarImage,
+ },
+ computed: {
+ lessParticipants() {
+ return this.participants.slice(0, this.numberOfLessParticipants);
+ },
+ visibleParticipants() {
+ return this.isShowingMoreParticipants ? this.participants : this.lessParticipants;
+ },
+ hasMoreParticipants() {
+ return this.participants.length > this.numberOfLessParticipants;
+ },
+ toggleLabel() {
+ let label = '';
+ if (this.isShowingMoreParticipants) {
+ label = __('- show less');
+ } else {
+ label = sprintf(__('+ %{moreCount} more'), {
+ moreCount: this.participants.length - this.numberOfLessParticipants,
+ });
+ }
+
+ return label;
+ },
+ participantLabel() {
+ return sprintf(
+ n__('%{count} participant', '%{count} participants', this.participants.length),
+ { count: this.loading ? '' : this.participantCount },
+ );
+ },
+ participantCount() {
+ return this.participants.length;
+ },
+ },
+ methods: {
+ toggleMoreParticipants() {
+ this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="sidebar-collapsed-icon">
+ <i
+ class="fa fa-users"
+ aria-hidden="true">
+ </i>
+ <loading-icon
+ v-if="loading"
+ class="js-participants-collapsed-loading-icon" />
+ <span
+ v-else
+ class="js-participants-collapsed-count">
+ {{ participantCount }}
+ </span>
+ </div>
+ <div class="title hide-collapsed">
+ <loading-icon
+ v-if="loading"
+ :inline="true"
+ class="js-participants-expanded-loading-icon" />
+ {{ participantLabel }}
+ </div>
+ <div class="participants-list hide-collapsed">
+ <div
+ v-for="participant in visibleParticipants"
+ :key="participant.id"
+ class="participants-author js-participants-author">
+ <a
+ class="author_link"
+ :href="participant.web_url">
+ <user-avatar-image
+ :lazy="true"
+ :img-src="participant.avatar_url"
+ css-classes="avatar-inline"
+ :size="24"
+ :tooltip-text="participant.name"
+ tooltip-placement="bottom" />
+ </a>
+ </div>
+ </div>
+ <div
+ v-if="hasMoreParticipants"
+ class="participants-more hide-collapsed">
+ <button
+ type="button"
+ class="btn-transparent btn-blank js-toggle-participants-button"
+ @click="toggleMoreParticipants">
+ {{ toggleLabel }}
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
new file mode 100644
index 00000000000..c1296b28db7
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
@@ -0,0 +1,26 @@
+<script>
+import Store from '../../stores/sidebar_store';
+import Mediator from '../../sidebar_mediator';
+import participants from './participants.vue';
+
+export default {
+ data() {
+ return {
+ mediator: new Mediator(),
+ store: new Store(),
+ };
+ },
+ components: {
+ participants,
+ },
+};
+</script>
+
+<template>
+ <div class="block participants">
+ <participants
+ :loading="store.isFetching.participants"
+ :participants="store.participants"
+ :number-of-less-participants="7" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
new file mode 100644
index 00000000000..25acc099699
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -0,0 +1,46 @@
+<script>
+import Store from '../../stores/sidebar_store';
+import Mediator from '../../sidebar_mediator';
+import eventHub from '../../event_hub';
+import Flash from '../../../flash';
+import { __ } from '../../../locale';
+import subscriptions from './subscriptions.vue';
+
+export default {
+ data() {
+ return {
+ mediator: new Mediator(),
+ store: new Store(),
+ };
+ },
+
+ components: {
+ subscriptions,
+ },
+
+ methods: {
+ onToggleSubscription() {
+ this.mediator.toggleSubscription()
+ .catch(() => {
+ Flash(__('Error occurred when toggling the notification subscription'));
+ });
+ },
+ },
+
+ created() {
+ eventHub.$on('toggleSubscription', this.onToggleSubscription);
+ },
+
+ beforeDestroy() {
+ eventHub.$off('toggleSubscription', this.onToggleSubscription);
+ },
+};
+</script>
+
+<template>
+ <div class="block subscriptions">
+ <subscriptions
+ :loading="store.isFetching.subscriptions"
+ :subscribed="store.subscribed" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
new file mode 100644
index 00000000000..940e1764f3d
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -0,0 +1,64 @@
+<script>
+import { __ } from '../../../locale';
+import eventHub from '../../event_hub';
+import loadingButton from '../../../vue_shared/components/loading_button.vue';
+
+export default {
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ subscribed: {
+ type: Boolean,
+ required: false,
+ },
+ id: {
+ type: Number,
+ required: false,
+ },
+ },
+ components: {
+ loadingButton,
+ },
+ computed: {
+ buttonLabel() {
+ let label;
+ if (this.subscribed === false) {
+ label = __('Subscribe');
+ } else if (this.subscribed === true) {
+ label = __('Unsubscribe');
+ }
+
+ return label;
+ },
+ },
+ methods: {
+ toggleSubscription() {
+ eventHub.$emit('toggleSubscription', this.id);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="sidebar-collapsed-icon">
+ <i
+ class="fa fa-rss"
+ aria-hidden="true">
+ </i>
+ </div>
+ <span class="issuable-header-text hide-collapsed pull-left">
+ {{ __('Notifications') }}
+ </span>
+ <loading-button
+ ref="loadingButton"
+ class="btn btn-default pull-right hide-collapsed js-issuable-subscribe-button"
+ :loading="loading"
+ :label="buttonLabel"
+ @click="toggleSubscription"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 3c9de02407e..977dd83a7ea 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,5 +1,3 @@
-/* global Flash */
-
function isValidProjectId(id) {
return id > 0;
}
@@ -38,7 +36,7 @@ class SidebarMoveIssue {
data: (searchTerm, callback) => {
this.mediator.fetchAutocompleteProjects(searchTerm)
.then(callback)
- .catch(() => new Flash('An error occurred while fetching projects autocomplete.'));
+ .catch(() => new window.Flash('An error occurred while fetching projects autocomplete.'));
},
renderRow: project => `
<li>
@@ -73,7 +71,7 @@ class SidebarMoveIssue {
this.mediator.moveIssue()
.catch(() => {
- Flash('An error occurred while moving the issue.');
+ window.Flash('An error occurred while moving the issue.');
this.$confirmButton
.enable()
.removeClass('is-loading');
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 604648407a4..37c97225bfd 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -7,6 +7,7 @@ export default class SidebarService {
constructor(endpointMap) {
if (!SidebarService.singleton) {
this.endpoint = endpointMap.endpoint;
+ this.toggleSubscriptionEndpoint = endpointMap.toggleSubscriptionEndpoint;
this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
@@ -36,6 +37,10 @@ export default class SidebarService {
});
}
+ toggleSubscription() {
+ return Vue.http.post(this.toggleSubscriptionEndpoint);
+ }
+
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 09b9d75c02d..2650bb725d4 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -4,6 +4,8 @@ import SidebarAssignees from './components/assignees/sidebar_assignees';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
+import sidebarParticipants from './components/participants/sidebar_participants.vue';
+import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
import Translate from '../vue_shared/translate';
import Mediator from './sidebar_mediator';
@@ -49,6 +51,36 @@ function mountLockComponent(mediator) {
}).$mount(el);
}
+function mountParticipantsComponent() {
+ const el = document.querySelector('.js-sidebar-participants-entry-point');
+
+ if (!el) return;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ sidebarParticipants,
+ },
+ render: createElement => createElement('sidebar-participants', {}),
+ });
+}
+
+function mountSubscriptionsComponent() {
+ const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
+
+ if (!el) return;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ sidebarSubscriptions,
+ },
+ render: createElement => createElement('sidebar-subscriptions', {}),
+ });
+}
+
function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions);
@@ -63,6 +95,8 @@ function domContentLoaded() {
mountConfidentialComponent(mediator);
mountLockComponent(mediator);
+ mountParticipantsComponent();
+ mountSubscriptionsComponent();
new SidebarMoveIssue(
mediator,
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 2fe6e5b31f0..2bda5a47791 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,5 +1,4 @@
-/* global Flash */
-
+import Flash from '../flash';
import Service from './services/sidebar_service';
import Store from './stores/sidebar_store';
@@ -9,6 +8,7 @@ export default class SidebarMediator {
this.store = new Store(options);
this.service = new Service({
endpoint: options.endpoint,
+ toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
moveIssueEndpoint: options.moveIssueEndpoint,
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
});
@@ -40,10 +40,25 @@ export default class SidebarMediator {
.then((data) => {
this.store.setAssigneeData(data);
this.store.setTimeTrackingData(data);
+ this.store.setParticipantsData(data);
+ this.store.setSubscriptionsData(data);
})
.catch(() => new Flash('Error occurred when fetching sidebar data'));
}
+ toggleSubscription() {
+ this.store.setFetchingState('subscriptions', true);
+ return this.service.toggleSubscription()
+ .then(() => {
+ this.store.setSubscribedState(!this.store.subscribed);
+ this.store.setFetchingState('subscriptions', false);
+ })
+ .catch((err) => {
+ this.store.setFetchingState('subscriptions', false);
+ throw err;
+ });
+ }
+
fetchAutocompleteProjects(searchTerm) {
return this.service.getProjectsAutocomplete(searchTerm)
.then(response => response.json())
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index d5d04103f3f..3150221b685 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -12,10 +12,14 @@ export default class SidebarStore {
this.assignees = [];
this.isFetching = {
assignees: true,
+ participants: true,
+ subscriptions: true,
};
this.autocompleteProjects = [];
this.moveToProjectId = 0;
this.isLockDialogOpen = false;
+ this.participants = [];
+ this.subscribed = null;
SidebarStore.singleton = this;
}
@@ -37,6 +41,20 @@ export default class SidebarStore {
this.humanTotalTimeSpent = data.human_total_time_spent;
}
+ setParticipantsData(data) {
+ this.isFetching.participants = false;
+ this.participants = data.participants || [];
+ }
+
+ setSubscriptionsData(data) {
+ this.isFetching.subscriptions = false;
+ this.subscribed = data.subscribed || false;
+ }
+
+ setFetchingState(key, value) {
+ this.isFetching[key] = value;
+ }
+
addAssignee(assignee) {
if (!this.findAssignee(assignee)) {
this.assignees.push(assignee);
@@ -61,6 +79,10 @@ export default class SidebarStore {
this.autocompleteProjects = projects;
}
+ setSubscribedState(subscribed) {
+ this.subscribed = subscribed;
+ }
+
setMoveToProjectId(moveToProjectId) {
this.moveToProjectId = moveToProjectId;
}
diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js
index 2bf7a3a5d61..8e931995fc6 100644
--- a/app/assets/javascripts/smart_interval.js
+++ b/app/assets/javascripts/smart_interval.js
@@ -3,9 +3,10 @@
* and controllable by a public API.
*/
-class SmartInterval {
+export default class SmartInterval {
/**
- * @param { function } opts.callback Function to be called on each iteration (required)
+ * @param { function } opts.callback Function that returns a promise, called on each iteration
+ * unless still in progress (required)
* @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
* @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
* @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
@@ -42,13 +43,16 @@ class SmartInterval {
const cfg = this.cfg;
const state = this.state;
- if (cfg.immediateExecution) {
+ if (cfg.immediateExecution && !this.isLoading) {
cfg.immediateExecution = false;
- cfg.callback();
+ this.triggerCallback();
}
state.intervalId = window.setInterval(() => {
- cfg.callback();
+ if (this.isLoading) {
+ return;
+ }
+ this.triggerCallback();
if (this.getCurrentInterval() === cfg.maxInterval) {
return;
@@ -76,7 +80,7 @@ class SmartInterval {
// start a timer, using the existing interval
resume() {
- this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
+ this.stopTimer(); // stop existing timer, in case timer was not previously stopped
this.start();
}
@@ -104,6 +108,18 @@ class SmartInterval {
this.initPageUnloadHandling();
}
+ triggerCallback() {
+ this.isLoading = true;
+ this.cfg.callback()
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch((err) => {
+ this.isLoading = false;
+ throw err;
+ });
+ }
+
initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling)
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
@@ -154,4 +170,3 @@ class SmartInterval {
}
}
-window.gl.SmartInterval = SmartInterval;
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 3a06b477d7c..d5606e153f6 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,28 +1,32 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
-/* global Flash */
-
+import Flash from './flash';
import { __, s__ } from './locale';
+import { spriteIcon } from './lib/utils/common_utils';
export default class Star {
constructor() {
- $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) {
- var $starIcon, $starSpan, $this, toggleStar;
- $this = $(this);
- $starSpan = $this.find('span');
- $starIcon = $this.find('i');
- toggleStar = function(isStarred) {
- $this.parent().find('.star-count').text(data.star_count);
- if (isStarred) {
- $starSpan.removeClass('starred').text(s__('StarProject|Star'));
- $starIcon.removeClass('fa-star').addClass('fa-star-o');
- } else {
- $starSpan.addClass('starred').text(__('Unstar'));
- $starIcon.removeClass('fa-star-o').addClass('fa-star');
+ $('.project-home-panel .toggle-star')
+ .on('ajax:success', function handleSuccess(e, data) {
+ const $this = $(this);
+ const $starSpan = $this.find('span');
+ const $startIcon = $this.find('svg');
+
+ function toggleStar(isStarred) {
+ $this.parent().find('.star-count').text(data.star_count);
+ if (isStarred) {
+ $starSpan.removeClass('starred').text(s__('StarProject|Star'));
+ $startIcon.remove();
+ $this.prepend(spriteIcon('star-o'));
+ } else {
+ $starSpan.addClass('starred').text(__('Unstar'));
+ $startIcon.remove();
+ $this.prepend(spriteIcon('star'));
+ }
}
- };
- toggleStar($starSpan.hasClass('starred'));
- }).on('ajax:error', function(e, xhr, status, error) {
- new Flash('Star toggle failed. Try again later.', 'alert');
- });
+
+ toggleStar($starSpan.hasClass('starred'));
+ })
+ .on('ajax:error', () => {
+ Flash('Star toggle failed. Try again later.', 'alert');
+ });
}
}
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
deleted file mode 100644
index bb4d68fcd49..00000000000
--- a/app/assets/javascripts/subscription.js
+++ /dev/null
@@ -1,45 +0,0 @@
-class Subscription {
- constructor(containerElm) {
- this.containerElm = containerElm;
-
- const subscribeButton = containerElm.querySelector('.js-subscribe-button');
- if (subscribeButton) {
- // remove class so we don't bind twice
- subscribeButton.classList.remove('js-subscribe-button');
- subscribeButton.addEventListener('click', this.toggleSubscription.bind(this));
- }
- }
-
- toggleSubscription(event) {
- const button = event.currentTarget;
- const buttonSpan = button.querySelector('span');
- if (!buttonSpan || button.classList.contains('disabled')) {
- return;
- }
- button.classList.add('disabled');
-
- const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe';
- const toggleActionUrl = this.containerElm.dataset.url;
-
- $.post(toggleActionUrl, () => {
- button.classList.remove('disabled');
-
- // hack to allow this to work with the issue boards Vue object
- if (document.querySelector('html').classList.contains('issue-boards-page')) {
- gl.issueBoards.boardStoreIssueSet(
- 'subscribed',
- !gl.issueBoards.BoardsStore.detail.issue.subscribed,
- );
- } else {
- buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe';
- }
- });
- }
-
- static bindAll(selector) {
- [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm));
- }
-}
-
-window.gl = window.gl || {};
-window.gl.Subscription = Subscription;
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 37e39ce5477..1ab4c2229ca 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,33 +1,24 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
+export default function subscriptionSelect() {
+ $('.js-subscription-event').each((i, element) => {
+ const fieldName = $(element).data('field-name');
-class SubscriptionSelect {
- constructor() {
- $('.js-subscription-event').each(function(i, el) {
- var fieldName;
- fieldName = $(el).data("field-name");
- return $(el).glDropdown({
- selectable: true,
- fieldName: fieldName,
- toggleLabel: (function(_this) {
- return function(selected, el, instance) {
- var $item, label;
- label = 'Subscription';
- $item = instance.dropdown.find('.is-active');
- if ($item.length) {
- label = $item.text();
- }
- return label;
- };
- })(this),
- clicked: function(options) {
- return options.e.preventDefault();
- },
- id: function(obj, el) {
- return $(el).data("id");
+ return $(element).glDropdown({
+ selectable: true,
+ fieldName,
+ toggleLabel(selected, el, instance) {
+ let label = 'Subscription';
+ const $item = instance.dropdown.find('.is-active');
+ if ($item.length) {
+ label = $item.text();
}
- });
+ return label;
+ },
+ clicked(options) {
+ return options.e.preventDefault();
+ },
+ id(obj, el) {
+ return $(el).data('id');
+ },
});
- }
+ });
}
-
-window.SubscriptionSelect = SubscriptionSelect;
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index c39f569da5e..dcbec40c79e 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -1,6 +1,5 @@
-/* global Flash */
-
import 'deckar01-task_list';
+import Flash from './flash';
export default class TaskList {
constructor(options = {}) {
diff --git a/app/assets/javascripts/test_utils/index.js b/app/assets/javascripts/test_utils/index.js
index 8875590f0f2..a55a338eea8 100644
--- a/app/assets/javascripts/test_utils/index.js
+++ b/app/assets/javascripts/test_utils/index.js
@@ -1,6 +1,8 @@
import 'core-js/es6/map';
import 'core-js/es6/set';
import simulateDrag from './simulate_drag';
+import simulateInput from './simulate_input';
// Export to global space for rspec to use
window.simulateDrag = simulateDrag;
+window.simulateInput = simulateInput;
diff --git a/app/assets/javascripts/test_utils/simulate_input.js b/app/assets/javascripts/test_utils/simulate_input.js
new file mode 100644
index 00000000000..90c1b7cb57e
--- /dev/null
+++ b/app/assets/javascripts/test_utils/simulate_input.js
@@ -0,0 +1,23 @@
+function triggerEvents(input) {
+ input.dispatchEvent(new Event('keydown'));
+ input.dispatchEvent(new Event('keypress'));
+ input.dispatchEvent(new Event('input'));
+ input.dispatchEvent(new Event('keyup'));
+}
+
+export default function simulateInput(target, text) {
+ const input = document.querySelector(target);
+ if (!input || !input.matches('textarea, input')) {
+ return false;
+ }
+
+ if (text.length > 0) {
+ Array.prototype.forEach.call(text, (char) => {
+ input.value += char;
+ triggerEvents(input);
+ });
+ } else {
+ triggerEvents(input);
+ }
+ return true;
+}
diff --git a/app/assets/javascripts/two_factor_auth.js b/app/assets/javascripts/two_factor_auth.js
index d26f61562a5..e3414d9afff 100644
--- a/app/assets/javascripts/two_factor_auth.js
+++ b/app/assets/javascripts/two_factor_auth.js
@@ -1,4 +1,5 @@
-/* global U2FRegister */
+import U2FRegister from './u2f/register';
+
document.addEventListener('DOMContentLoaded', () => {
const twoFactorNode = document.querySelector('.js-two-factor-auth');
const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true';
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index 8821b22477f..a3cc04e35fe 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -1,118 +1,108 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */
+/* eslint-disable func-names, wrap-iife */
/* global u2f */
-/* global U2FError */
-/* global U2FUtil */
-
import _ from 'underscore';
+import isU2FSupported from './util';
+import U2FError from './error';
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> authenticated -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup
-(function() {
- const global = window.gl || (window.gl = {});
-
- global.U2FAuthenticate = (function() {
- function U2FAuthenticate(container, form, u2fParams, fallbackButton, fallbackUI) {
- this.container = container;
- this.renderNotSupported = this.renderNotSupported.bind(this);
- this.renderAuthenticated = this.renderAuthenticated.bind(this);
- this.renderError = this.renderError.bind(this);
- this.renderInProgress = this.renderInProgress.bind(this);
- this.renderTemplate = this.renderTemplate.bind(this);
- this.authenticate = this.authenticate.bind(this);
- this.start = this.start.bind(this);
- this.appId = u2fParams.app_id;
- this.challenge = u2fParams.challenge;
- this.form = form;
- this.fallbackButton = fallbackButton;
- this.fallbackUI = fallbackUI;
- if (this.fallbackButton) this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
- this.signRequests = u2fParams.sign_requests.map(function(request) {
- // The U2F Javascript API v1.1 requires a single challenge, with
- // _no challenges per-request_. The U2F Javascript API v1.0 requires a
- // challenge per-request, which is done by copying the single challenge
- // into every request.
- //
- // In either case, we don't need the per-request challenges that the server
- // has generated, so we can remove them.
- //
- // Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
- // This can be removed once we upgrade.
- // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
- return _(request).omit('challenge');
- });
+export default class U2FAuthenticate {
+ constructor(container, form, u2fParams, fallbackButton, fallbackUI) {
+ this.container = container;
+ this.renderNotSupported = this.renderNotSupported.bind(this);
+ this.renderAuthenticated = this.renderAuthenticated.bind(this);
+ this.renderError = this.renderError.bind(this);
+ this.renderInProgress = this.renderInProgress.bind(this);
+ this.renderTemplate = this.renderTemplate.bind(this);
+ this.authenticate = this.authenticate.bind(this);
+ this.start = this.start.bind(this);
+ this.appId = u2fParams.app_id;
+ this.challenge = u2fParams.challenge;
+ this.form = form;
+ this.fallbackButton = fallbackButton;
+ this.fallbackUI = fallbackUI;
+ if (this.fallbackButton) {
+ this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
}
- U2FAuthenticate.prototype.start = function() {
- if (U2FUtil.isU2FSupported()) {
- return this.renderInProgress();
- } else {
- return this.renderNotSupported();
- }
- };
+ // The U2F Javascript API v1.1 requires a single challenge, with
+ // _no challenges per-request_. The U2F Javascript API v1.0 requires a
+ // challenge per-request, which is done by copying the single challenge
+ // into every request.
+ //
+ // In either case, we don't need the per-request challenges that the server
+ // has generated, so we can remove them.
+ //
+ // Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
+ // This can be removed once we upgrade.
+ // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
+ this.signRequests = u2fParams.sign_requests.map(request => _(request).omit('challenge'));
- U2FAuthenticate.prototype.authenticate = function() {
- return u2f.sign(this.appId, this.challenge, this.signRequests, (function(_this) {
- return function(response) {
- var error;
- if (response.errorCode) {
- error = new U2FError(response.errorCode, 'authenticate');
- return _this.renderError(error);
- } else {
- return _this.renderAuthenticated(JSON.stringify(response));
- }
- };
- })(this), 10);
+ this.templates = {
+ notSupported: '#js-authenticate-u2f-not-supported',
+ setup: '#js-authenticate-u2f-setup',
+ inProgress: '#js-authenticate-u2f-in-progress',
+ error: '#js-authenticate-u2f-error',
+ authenticated: '#js-authenticate-u2f-authenticated',
};
+ }
- // Rendering #
- U2FAuthenticate.prototype.templates = {
- "notSupported": "#js-authenticate-u2f-not-supported",
- "setup": '#js-authenticate-u2f-setup',
- "inProgress": '#js-authenticate-u2f-in-progress',
- "error": '#js-authenticate-u2f-error',
- "authenticated": '#js-authenticate-u2f-authenticated'
- };
+ start() {
+ if (isU2FSupported()) {
+ return this.renderInProgress();
+ }
+ return this.renderNotSupported();
+ }
- U2FAuthenticate.prototype.renderTemplate = function(name, params) {
- var template, templateString;
- templateString = $(this.templates[name]).html();
- template = _.template(templateString);
- return this.container.html(template(params));
- };
+ authenticate() {
+ return u2f.sign(this.appId, this.challenge, this.signRequests, (function (_this) {
+ return function (response) {
+ if (response.errorCode) {
+ const error = new U2FError(response.errorCode, 'authenticate');
+ return _this.renderError(error);
+ }
+ return _this.renderAuthenticated(JSON.stringify(response));
+ };
+ })(this), 10);
+ }
- U2FAuthenticate.prototype.renderInProgress = function() {
- this.renderTemplate('inProgress');
- return this.authenticate();
- };
+ renderTemplate(name, params) {
+ const templateString = $(this.templates[name]).html();
+ const template = _.template(templateString);
+ return this.container.html(template(params));
+ }
- U2FAuthenticate.prototype.renderError = function(error) {
- this.renderTemplate('error', {
- error_message: error.message(),
- error_code: error.errorCode
- });
- return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress);
- };
+ renderInProgress() {
+ this.renderTemplate('inProgress');
+ return this.authenticate();
+ }
- U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) {
- this.renderTemplate('authenticated');
- const container = this.container[0];
- container.querySelector('#js-device-response').value = deviceResponse;
- container.querySelector(this.form).submit();
- this.fallbackButton.classList.add('hidden');
- };
+ renderError(error) {
+ this.renderTemplate('error', {
+ error_message: error.message(),
+ error_code: error.errorCode,
+ });
+ return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress);
+ }
- U2FAuthenticate.prototype.renderNotSupported = function() {
- return this.renderTemplate('notSupported');
- };
+ renderAuthenticated(deviceResponse) {
+ this.renderTemplate('authenticated');
+ const container = this.container[0];
+ container.querySelector('#js-device-response').value = deviceResponse;
+ container.querySelector(this.form).submit();
+ this.fallbackButton.classList.add('hidden');
+ }
- U2FAuthenticate.prototype.switchToFallbackUI = function() {
- this.fallbackButton.classList.add('hidden');
- this.container[0].classList.add('hidden');
- this.fallbackUI.classList.remove('hidden');
- };
+ renderNotSupported() {
+ return this.renderTemplate('notSupported');
+ }
+
+ switchToFallbackUI() {
+ this.fallbackButton.classList.add('hidden');
+ this.container[0].classList.add('hidden');
+ this.fallbackUI.classList.remove('hidden');
+ }
- return U2FAuthenticate;
- })();
-})();
+}
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index 3119b3480c3..1a98564ff55 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -1,25 +1,22 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, max-len */
-/* global u2f */
+export default class U2FError {
+ constructor(errorCode, u2fFlowType) {
+ this.errorCode = errorCode;
+ this.message = this.message.bind(this);
+ this.httpsDisabled = window.location.protocol !== 'https:';
+ this.u2fFlowType = u2fFlowType;
+ }
-(function() {
- this.U2FError = (function() {
- function U2FError(errorCode, u2fFlowType) {
- this.errorCode = errorCode;
- this.message = this.message.bind(this);
- this.httpsDisabled = window.location.protocol !== 'https:';
- this.u2fFlowType = u2fFlowType;
- }
-
- U2FError.prototype.message = function() {
- if (this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
- return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
- } else if (this.errorCode === u2f.ErrorCodes.DEVICE_INELIGIBLE) {
- if (this.u2fFlowType === 'authenticate') return 'This device has not been registered with us.';
- if (this.u2fFlowType === 'register') return 'This device has already been registered with us.';
+ message() {
+ if (this.errorCode === window.u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
+ return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
+ } else if (this.errorCode === window.u2f.ErrorCodes.DEVICE_INELIGIBLE) {
+ if (this.u2fFlowType === 'authenticate') {
+ return 'This device has not been registered with us.';
}
- return "There was a problem communicating with your device.";
- };
-
- return U2FError;
- })();
-}).call(window);
+ if (this.u2fFlowType === 'register') {
+ return 'This device has already been registered with us.';
+ }
+ }
+ return 'There was a problem communicating with your device.';
+ }
+}
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index 3a2534d553b..cc3f02e75f6 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,98 +1,89 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */
+/* eslint-disable func-names, wrap-iife */
/* global u2f */
-/* global U2FError */
-/* global U2FUtil */
import _ from 'underscore';
+import isU2FSupported from './util';
+import U2FError from './error';
// Register U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> registered -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup
-(function() {
- this.U2FRegister = (function() {
- function U2FRegister(container, u2fParams) {
- this.container = container;
- this.renderNotSupported = this.renderNotSupported.bind(this);
- this.renderRegistered = this.renderRegistered.bind(this);
- this.renderError = this.renderError.bind(this);
- this.renderInProgress = this.renderInProgress.bind(this);
- this.renderSetup = this.renderSetup.bind(this);
- this.renderTemplate = this.renderTemplate.bind(this);
- this.register = this.register.bind(this);
- this.start = this.start.bind(this);
- this.appId = u2fParams.app_id;
- this.registerRequests = u2fParams.register_requests;
- this.signRequests = u2fParams.sign_requests;
- }
+export default class U2FRegister {
+ constructor(container, u2fParams) {
+ this.container = container;
+ this.renderNotSupported = this.renderNotSupported.bind(this);
+ this.renderRegistered = this.renderRegistered.bind(this);
+ this.renderError = this.renderError.bind(this);
+ this.renderInProgress = this.renderInProgress.bind(this);
+ this.renderSetup = this.renderSetup.bind(this);
+ this.renderTemplate = this.renderTemplate.bind(this);
+ this.register = this.register.bind(this);
+ this.start = this.start.bind(this);
+ this.appId = u2fParams.app_id;
+ this.registerRequests = u2fParams.register_requests;
+ this.signRequests = u2fParams.sign_requests;
- U2FRegister.prototype.start = function() {
- if (U2FUtil.isU2FSupported()) {
- return this.renderSetup();
- } else {
- return this.renderNotSupported();
- }
+ this.templates = {
+ notSupported: '#js-register-u2f-not-supported',
+ setup: '#js-register-u2f-setup',
+ inProgress: '#js-register-u2f-in-progress',
+ error: '#js-register-u2f-error',
+ registered: '#js-register-u2f-registered',
};
+ }
- U2FRegister.prototype.register = function() {
- return u2f.register(this.appId, this.registerRequests, this.signRequests, (function(_this) {
- return function(response) {
- var error;
- if (response.errorCode) {
- error = new U2FError(response.errorCode, 'register');
- return _this.renderError(error);
- } else {
- return _this.renderRegistered(JSON.stringify(response));
- }
- };
- })(this), 10);
- };
+ start() {
+ if (isU2FSupported()) {
+ return this.renderSetup();
+ }
+ return this.renderNotSupported();
+ }
- // Rendering #
- U2FRegister.prototype.templates = {
- "notSupported": "#js-register-u2f-not-supported",
- "setup": '#js-register-u2f-setup',
- "inProgress": '#js-register-u2f-in-progress',
- "error": '#js-register-u2f-error',
- "registered": '#js-register-u2f-registered'
- };
+ register() {
+ return u2f.register(this.appId, this.registerRequests, this.signRequests, (function (_this) {
+ return function (response) {
+ if (response.errorCode) {
+ const error = new U2FError(response.errorCode, 'register');
+ return _this.renderError(error);
+ }
+ return _this.renderRegistered(JSON.stringify(response));
+ };
+ })(this), 10);
+ }
- U2FRegister.prototype.renderTemplate = function(name, params) {
- var template, templateString;
- templateString = $(this.templates[name]).html();
- template = _.template(templateString);
- return this.container.html(template(params));
- };
+ renderTemplate(name, params) {
+ const templateString = $(this.templates[name]).html();
+ const template = _.template(templateString);
+ return this.container.html(template(params));
+ }
- U2FRegister.prototype.renderSetup = function() {
- this.renderTemplate('setup');
- return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress);
- };
+ renderSetup() {
+ this.renderTemplate('setup');
+ return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress);
+ }
- U2FRegister.prototype.renderInProgress = function() {
- this.renderTemplate('inProgress');
- return this.register();
- };
+ renderInProgress() {
+ this.renderTemplate('inProgress');
+ return this.register();
+ }
- U2FRegister.prototype.renderError = function(error) {
- this.renderTemplate('error', {
- error_message: error.message(),
- error_code: error.errorCode
- });
- return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
- };
+ renderError(error) {
+ this.renderTemplate('error', {
+ error_message: error.message(),
+ error_code: error.errorCode,
+ });
+ return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
+ }
- U2FRegister.prototype.renderRegistered = function(deviceResponse) {
- this.renderTemplate('registered');
- // Prefer to do this instead of interpolating using Underscore templates
- // because of JSON escaping issues.
- return this.container.find("#js-device-response").val(deviceResponse);
- };
-
- U2FRegister.prototype.renderNotSupported = function() {
- return this.renderTemplate('notSupported');
- };
+ renderRegistered(deviceResponse) {
+ this.renderTemplate('registered');
+ // Prefer to do this instead of interpolating using Underscore templates
+ // because of JSON escaping issues.
+ return this.container.find('#js-device-response').val(deviceResponse);
+ }
- return U2FRegister;
- })();
-}).call(window);
+ renderNotSupported() {
+ return this.renderTemplate('notSupported');
+ }
+}
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
index 813d363db00..9771ff935c2 100644
--- a/app/assets/javascripts/u2f/util.js
+++ b/app/assets/javascripts/u2f/util.js
@@ -1,12 +1,3 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife */
-(function() {
- this.U2FUtil = (function() {
- function U2FUtil() {}
-
- U2FUtil.isU2FSupported = function() {
- return window.u2f;
- };
-
- return U2FUtil;
- })();
-}).call(window);
+export default function isU2FSupported() {
+ return window.u2f;
+}
diff --git a/app/assets/javascripts/users/index.js b/app/assets/javascripts/users/index.js
index 33a83f8dae5..9fd8452a2b6 100644
--- a/app/assets/javascripts/users/index.js
+++ b/app/assets/javascripts/users/index.js
@@ -1,7 +1,7 @@
import Cookies from 'js-cookie';
import UserTabs from './user_tabs';
-export default function initUserProfile(action) {
+function initUserProfile(action) {
// place profile avatars to top
$('.profile-groups-avatars').tooltip({
placement: 'top',
@@ -17,3 +17,9 @@ export default function initUserProfile(action) {
$(this).parents('.project-limit-message').remove();
});
}
+
+document.addEventListener('DOMContentLoaded', () => {
+ const page = $('body').attr('data-page');
+ const action = page.split(':')[1];
+ initUserProfile(action);
+});
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 73676bd6de7..759cc9925f4 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -6,7 +6,7 @@ import _ from 'underscore';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
-function UsersSelect(currentUser, els) {
+function UsersSelect(currentUser, els, options = {}) {
var $els;
this.users = this.users.bind(this);
this.user = this.user.bind(this);
@@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) {
}
}
+ const { handleClick } = options;
+
$els = $(els);
if (!els) {
@@ -424,7 +426,7 @@ function UsersSelect(currentUser, els) {
}
var isIssueIndex, isMRIndex, page, selected;
- page = $('body').data('page');
+ page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
@@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) {
}
if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
+ } else if (handleClick) {
+ e.preventDefault();
+ handleClick(user, isMarking);
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
index e98d147733c..e86a0f7e749 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
@@ -1,6 +1,5 @@
-/* global Flash */
-
import '~/lib/utils/datetime_utility';
+import Flash from '../../flash';
import MemoryUsage from './mr_widget_memory_usage';
import StatusIcon from './mr_widget_status_icon';
import MRWidgetService from '../services/mr_widget_service';
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 219ff94924e..13e4cb5717e 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
@@ -1,5 +1,5 @@
import tooltip from '../../vue_shared/directives/tooltip';
-import '../../lib/utils/text_utility';
+import { pluralize } from '../../lib/utils/text_utility';
export default {
name: 'MRWidgetHeader',
@@ -14,7 +14,7 @@ export default {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
- return gl.text.pluralize('commit', this.mr.divergedCommitsCount);
+ return pluralize('commit', this.mr.divergedCommitsCount);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
deleted file mode 100644
index c79b5c720eb..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import PipelineStage from '../../pipelines/components/stage.vue';
-import ciIcon from '../../vue_shared/components/ci_icon.vue';
-import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
-
-export default {
- name: 'MRWidgetPipeline',
- props: {
- mr: { type: Object, required: true },
- },
- components: {
- 'pipeline-stage': PipelineStage,
- ciIcon,
- },
- computed: {
- hasPipeline() {
- return this.mr.pipeline && Object.keys(this.mr.pipeline).length > 0;
- },
- hasCIError() {
- const { hasCI, ciStatus } = this.mr;
-
- return hasCI && !ciStatus;
- },
- svg() {
- return statusIconEntityMap.icon_status_failed;
- },
- stageText() {
- return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
- },
- status() {
- return this.mr.pipeline.details.status || {};
- },
- },
- template: `
- <div
- v-if="hasPipeline || hasCIError"
- class="mr-widget-heading">
- <div class="ci-widget media">
- <template v-if="hasCIError">
- <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
- <span
- v-html="svg"
- aria-hidden="true"></span>
- </div>
- <div class="media-body">
- Could not connect to the CI server. Please check your settings and try again
- </div>
- </template>
- <template v-else-if="hasPipeline">
- <div class="ci-status-icon append-right-10">
- <a
- class="icon-link"
- :href="this.status.details_path">
- <ci-icon :status="status" />
- </a>
- </div>
- <div class="media-body">
- <span>
- Pipeline
- <a
- :href="mr.pipeline.path"
- class="pipeline-id">#{{mr.pipeline.id}}</a>
- </span>
- <span class="mr-widget-pipeline-graph">
- <span class="stage-cell">
- <div
- v-if="mr.pipeline.details.stages.length > 0"
- v-for="stage in mr.pipeline.details.stages"
- class="stage-container dropdown js-mini-pipeline-graph">
- <pipeline-stage :stage="stage" />
- </div>
- </span>
- </span>
- <span>
- {{mr.pipeline.details.status.label}} for
- <a
- :href="mr.pipeline.commit.commit_path"
- class="commit-sha js-commit-link">
- {{mr.pipeline.commit.short_id}}</a>.
- </span>
- <span
- v-if="mr.pipeline.coverage"
- class="js-mr-coverage">
- Coverage {{mr.pipeline.coverage}}%
- </span>
- </div>
- </template>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
new file mode 100644
index 00000000000..dbc65462377
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -0,0 +1,104 @@
+<script>
+ import pipelineStage from '../../pipelines/components/stage.vue';
+ import ciIcon from '../../vue_shared/components/ci_icon.vue';
+ import icon from '../../vue_shared/components/icon.vue';
+
+ export default {
+ name: 'MRWidgetPipeline',
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ // This prop needs to be camelCase, html attributes are case insensive
+ // https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
+ hasCi: {
+ type: Boolean,
+ required: false,
+ },
+ ciStatus: {
+ type: String,
+ required: false,
+ },
+ },
+ components: {
+ pipelineStage,
+ ciIcon,
+ icon,
+ },
+ computed: {
+ hasPipeline() {
+ return this.pipeline && Object.keys(this.pipeline).length > 0;
+ },
+ hasCIError() {
+ return this.hasCi && !this.ciStatus;
+ },
+ status() {
+ return this.pipeline.details &&
+ this.pipeline.details.status ? this.pipeline.details.status : {};
+ },
+ hasStages() {
+ return this.pipeline.details &&
+ this.pipeline.details.stages &&
+ this.pipeline.details.stages.length;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ v-if="hasPipeline || hasCIError"
+ class="mr-widget-heading">
+ <div class="ci-widget media">
+ <template v-if="hasCIError">
+ <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
+ <icon name="status_failed" />
+ </div>
+ <div class="media-body">
+ Could not connect to the CI server. Please check your settings and try again
+ </div>
+ </template>
+ <template v-else-if="hasPipeline">
+ <a
+ class="append-right-10"
+ :href="this.status.details_path">
+ <ci-icon :status="status" />
+ </a>
+
+ <div class="media-body">
+ Pipeline
+ <a
+ :href="pipeline.path"
+ class="pipeline-id">
+ #{{pipeline.id}}
+ </a>
+
+ {{pipeline.details.status.label}} for
+
+ <a
+ :href="pipeline.commit.commit_path"
+ class="commit-sha js-commit-link">
+ {{pipeline.commit.short_id}}</a>.
+
+ <span class="mr-widget-pipeline-graph">
+ <span class="stage-cell">
+ <div
+ v-if="hasStages"
+ v-for="(stage, i) in pipeline.details.stages"
+ :key="i"
+ class="stage-container dropdown js-mini-pipeline-graph">
+ <pipeline-stage :stage="stage" />
+ </div>
+ </span>
+ </span>
+
+ <template v-if="pipeline.coverage">
+ Coverage {{pipeline.coverage}}%
+ </template>
+
+ </div>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
index 4998a47b691..eeb990908f6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
@@ -14,7 +14,7 @@ export default {
statusObj() {
return {
group: this.status,
- icon: `icon_status_${this.status}`,
+ icon: `status_${this.status}`,
};
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
index bdfd4d9667c..05c4a28be88 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
@@ -1,4 +1,4 @@
-/* global Flash */
+import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon';
import MRWidgetAuthor from '../../components/mr_widget_author';
import eventHub from '../../event_hub';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
index 74fc52796a0..2dfd87ed904 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
@@ -1,5 +1,4 @@
-/* global Flash */
-
+import Flash from '../../../flash';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import tooltip from '../../../vue_shared/directives/tooltip';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index 61734163b6e..be37dd87de9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -1,7 +1,7 @@
-/* global Flash */
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
+import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub';
@@ -286,6 +286,7 @@ export default {
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
+ class="js-remove-source-branch-checkbox"
:disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch
</label>
@@ -311,8 +312,8 @@ export default {
</button>
</template>
<template v-else>
- <span class="bold">
- The pipeline for this merge request has not succeeded yet
+ <span class="bold js-resolve-mr-widget-items-message">
+ You can only merge once the items above are resolved
</span>
</template>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
index 54be1fbe675..4f83350e07c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
@@ -1,4 +1,3 @@
-/* global Flash */
import statusIcon from '../mr_widget_status_icon';
import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
@@ -27,12 +26,12 @@ export default {
.then(res => res.json())
.then((res) => {
eventHub.$emit('UpdateWidgetData', res);
- new Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
+ new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
$('.merge-request .detail-page-description .title').text(this.mr.title);
})
.catch(() => {
this.isMakingRequest = false;
- new Flash('Something went wrong. Please try again.'); // eslint-disable-line
+ new window.Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 49340c232c8..5bd8b99420a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -13,7 +13,7 @@ export { default as Vue } from 'vue';
export { default as SmartInterval } from '~/smart_interval';
export { default as WidgetHeader } from './components/mr_widget_header';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help';
-export { default as WidgetPipeline } from './components/mr_widget_pipeline';
+export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links';
export { default as MergedState } from './components/states/mr_widget_merged';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 044b664484b..1274db2c4c8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -1,5 +1,5 @@
-/* global Flash */
-
+import SmartInterval from '~/smart_interval';
+import Flash from '../flash';
import {
WidgetHeader,
WidgetMergeHelp,
@@ -61,7 +61,7 @@ export default {
return this.mr.hasCI;
},
shouldRenderRelatedLinks() {
- return this.mr.relatedLinks;
+ return !!this.mr.relatedLinks;
},
shouldRenderDeployments() {
return this.mr.deployments.length;
@@ -82,7 +82,7 @@ export default {
return new MRWidgetService(endpoints);
},
checkStatus(cb) {
- this.service.checkStatus()
+ return this.service.checkStatus()
.then(res => res.json())
.then((res) => {
this.handleNotification(res);
@@ -98,7 +98,7 @@ export default {
});
},
initPolling() {
- this.pollingInterval = new gl.SmartInterval({
+ this.pollingInterval = new SmartInterval({
callback: this.checkStatus,
startingInterval: 10000,
maxInterval: 30000,
@@ -107,7 +107,7 @@ export default {
});
},
initDeploymentsPolling() {
- this.deploymentsInterval = new gl.SmartInterval({
+ this.deploymentsInterval = new SmartInterval({
callback: this.fetchDeployments,
startingInterval: 30000,
maxInterval: 120000,
@@ -122,7 +122,7 @@ export default {
}
},
fetchDeployments() {
- this.service.fetchDeployments()
+ return this.service.fetchDeployments()
.then(res => res.json())
.then((res) => {
if (res.length) {
@@ -236,7 +236,10 @@ export default {
<mr-widget-header :mr="mr" />
<mr-widget-pipeline
v-if="shouldRenderPipelines"
- :mr="mr" />
+ :pipeline="mr.pipeline"
+ :ci-status="mr.ciStatus"
+ :has-ci="mr.hasCI"
+ />
<mr-widget-deployment
v-if="shouldRenderDeployments"
:mr="mr"
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index 79c3d335679..99f5c305df5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -11,7 +11,7 @@ export default class MRWidgetService {
this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath);
- this.pollResource = Vue.resource(`${endpoints.statusPath}?basic=true`);
+ this.pollResource = Vue.resource(`${endpoints.statusPath}?serializer=basic`);
this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
}
diff --git a/app/assets/javascripts/vue_shared/ci_action_icons.js b/app/assets/javascripts/vue_shared/ci_action_icons.js
deleted file mode 100644
index b21f0ab49fd..00000000000
--- a/app/assets/javascripts/vue_shared/ci_action_icons.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import cancelSVG from 'icons/_icon_action_cancel.svg';
-import retrySVG from 'icons/_icon_action_retry.svg';
-import playSVG from 'icons/_icon_action_play.svg';
-import stopSVG from 'icons/_icon_action_stop.svg';
-
-/**
- * For the provided action returns the respective SVG
- *
- * @param {String} action
- * @return {SVG|String}
- */
-export default function getActionIcon(action) {
- const icons = {
- icon_action_cancel: cancelSVG,
- icon_action_play: playSVG,
- icon_action_retry: retrySVG,
- icon_action_stop: stopSVG,
- };
-
- return icons[action] || '';
-}
diff --git a/app/assets/javascripts/vue_shared/ci_status_icons.js b/app/assets/javascripts/vue_shared/ci_status_icons.js
deleted file mode 100644
index d9d0cad38e4..00000000000
--- a/app/assets/javascripts/vue_shared/ci_status_icons.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import BORDERLESS_CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg';
-import BORDERLESS_CREATED_SVG from 'icons/_icon_status_created_borderless.svg';
-import BORDERLESS_FAILED_SVG from 'icons/_icon_status_failed_borderless.svg';
-import BORDERLESS_MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg';
-import BORDERLESS_PENDING_SVG from 'icons/_icon_status_pending_borderless.svg';
-import BORDERLESS_RUNNING_SVG from 'icons/_icon_status_running_borderless.svg';
-import BORDERLESS_SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg';
-import BORDERLESS_SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg';
-import BORDERLESS_WARNING_SVG from 'icons/_icon_status_warning_borderless.svg';
-
-import CANCELED_SVG from 'icons/_icon_status_canceled.svg';
-import CREATED_SVG from 'icons/_icon_status_created.svg';
-import FAILED_SVG from 'icons/_icon_status_failed.svg';
-import MANUAL_SVG from 'icons/_icon_status_manual.svg';
-import PENDING_SVG from 'icons/_icon_status_pending.svg';
-import RUNNING_SVG from 'icons/_icon_status_running.svg';
-import SKIPPED_SVG from 'icons/_icon_status_skipped.svg';
-import SUCCESS_SVG from 'icons/_icon_status_success.svg';
-import WARNING_SVG from 'icons/_icon_status_warning.svg';
-
-export const borderlessStatusIconEntityMap = {
- icon_status_canceled: BORDERLESS_CANCELED_SVG,
- icon_status_created: BORDERLESS_CREATED_SVG,
- icon_status_failed: BORDERLESS_FAILED_SVG,
- icon_status_manual: BORDERLESS_MANUAL_SVG,
- icon_status_pending: BORDERLESS_PENDING_SVG,
- icon_status_running: BORDERLESS_RUNNING_SVG,
- icon_status_skipped: BORDERLESS_SKIPPED_SVG,
- icon_status_success: BORDERLESS_SUCCESS_SVG,
- icon_status_warning: BORDERLESS_WARNING_SVG,
-};
-
-export const statusIconEntityMap = {
- icon_status_canceled: CANCELED_SVG,
- icon_status_created: CREATED_SVG,
- icon_status_failed: FAILED_SVG,
- icon_status_manual: MANUAL_SVG,
- icon_status_pending: PENDING_SVG,
- icon_status_running: RUNNING_SVG,
- icon_status_skipped: SKIPPED_SVG,
- icon_status_success: SUCCESS_SVG,
- icon_status_warning: WARNING_SVG,
-};
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index caa28bff6db..fc795936abf 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,52 +1,63 @@
<script>
-import ciIcon from './ci_icon.vue';
-/**
- * Renders CI Badge link with CI icon and status text based on
- * API response shared between all places where it is used.
- *
- * Receives status object containing:
- * status: {
- * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
- * label:"running" // used for potential tooltip
- * text:"running" // text rendered
- * }
- *
- * Used in:
- * - Pipelines table - first column
- * - Jobs table - first column
- * - Pipeline show view - header
- * - Job show view - header
- * - MR widget
- */
+ import ciIcon from './ci_icon.vue';
+ import tooltip from '../directives/tooltip';
+ /**
+ * Renders CI Badge link with CI icon and status text based on
+ * API response shared between all places where it is used.
+ *
+ * Receives status object containing:
+ * status: {
+ * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
+ * group:"running" // used for CSS class
+ * icon: "icon_status_running" // used to render the icon
+ * label:"running" // used for potential tooltip
+ * text:"running" // text rendered
+ * }
+ *
+ * Used in:
+ * - Pipelines table - first column
+ * - Jobs table - first column
+ * - Pipeline show view - header
+ * - Job show view - header
+ * - MR widget
+ */
-export default {
- props: {
- status: {
- type: Object,
- required: true,
+ export default {
+ props: {
+ status: {
+ type: Object,
+ required: true,
+ },
+ showText: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
- },
-
- components: {
- ciIcon,
- },
-
- computed: {
- cssClass() {
- const className = this.status.group;
-
- return className ? `ci-status ci-${this.status.group}` : 'ci-status';
+ components: {
+ ciIcon,
},
- },
-};
+ directives: {
+ tooltip,
+ },
+ computed: {
+ cssClass() {
+ const className = this.status.group;
+ return className ? `ci-status ci-${className}` : 'ci-status';
+ },
+ },
+ };
</script>
<template>
<a
:href="status.details_path"
- :class="cssClass">
+ :class="cssClass"
+ v-tooltip
+ :title="!showText ? status.text : ''">
<ci-icon :status="status" />
- {{status.text}}
+
+ <template v-if="showText">
+ {{status.text}}
+ </template>
</a>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index ec88119e16c..2a018f38366 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,5 +1,5 @@
<script>
- import { statusIconEntityMap } from '../ci_status_icons';
+ import icon from '../../vue_shared/components/icon.vue';
/**
* Renders CI icon based on API response shared between all places where it is used.
@@ -30,11 +30,11 @@
},
},
- computed: {
- statusIconSvg() {
- return statusIconEntityMap[this.status.icon];
- },
+ components: {
+ icon,
+ },
+ computed: {
cssClass() {
const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
@@ -44,7 +44,8 @@
</script>
<template>
<span
- :class="cssClass"
- v-html="statusIconSvg">
+ :class="cssClass">
+ <icon
+ :name="status.icon"/>
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 50d14282cad..52814de8b2d 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -63,14 +63,17 @@
required: false,
default: () => ({}),
},
+ showBranch: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
/**
* Used to verify if all the properties needed to render the commit
* ref section were provided.
*
- * TODO: Improve this! Use lodash _.has when we have it.
- *
* @returns {Boolean}
*/
hasCommitRef() {
@@ -80,8 +83,6 @@
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
- * TODO: Improve this! Use lodash _.has when we have it.
- *
* @returns {Boolean}
*/
hasAuthor() {
@@ -114,31 +115,30 @@
</script>
<template>
<div class="branch-commit">
- <div
- v-if="hasCommitRef"
- class="icon-container hidden-xs">
- <i
- v-if="tag"
- class="fa fa-tag"
- aria-hidden="true">
- </i>
- <i
- v-if="!tag"
- class="fa fa-code-fork"
- aria-hidden="true">
- </i>
- </div>
-
- <a
- v-if="hasCommitRef"
- class="ref-name hidden-xs"
- :href="commitRef.ref_url"
- v-tooltip
- data-container="body"
- :title="commitRef.name">
- {{commitRef.name}}
- </a>
+ <template v-if="hasCommitRef && showBranch">
+ <div
+ class="icon-container hidden-xs">
+ <i
+ v-if="tag"
+ class="fa fa-tag"
+ aria-hidden="true">
+ </i>
+ <i
+ v-if="!tag"
+ class="fa fa-code-fork"
+ aria-hidden="true">
+ </i>
+ </div>
+ <a
+ class="ref-name hidden-xs"
+ :href="commitRef.ref_url"
+ v-tooltip
+ data-container="body"
+ :title="commitRef.name">
+ {{commitRef.name}}
+ </a>
+ </template>
<div
v-html="commitIconSvg"
class="commit-icon js-commit-icon">
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
new file mode 100644
index 00000000000..4216660da8c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -0,0 +1,58 @@
+<script>
+
+/* This is a re-usable vue component for rendering a svg sprite
+ icon
+
+ Sample configuration:
+
+ <icon
+ name="retry"
+ :size="32"
+ css-classes="top"
+ />
+
+*/
+ // only allow classes in images.scss e.g. s12
+ const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
+
+ export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+
+ size: {
+ type: Number,
+ required: false,
+ default: 16,
+ validator(value) {
+ return validSizes.includes(value);
+ },
+ },
+
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ computed: {
+ spriteHref() {
+ return `${gon.sprite_icons}#${this.name}`;
+ },
+ iconSizeClass() {
+ return this.size ? `s${this.size}` : '';
+ },
+ },
+ };
+</script>
+
+<template>
+ <svg
+ :class="[iconSizeClass, cssClasses]">
+ <use
+ v-bind="{'xlink:href':spriteHref}"/>
+ </svg>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
index 16c0a8efcd2..564fc5029af 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
@@ -1,4 +1,6 @@
<script>
+ import Icon from '../../../vue_shared/components/icon.vue';
+
export default {
props: {
isLocked: {
@@ -14,12 +16,16 @@
},
},
+ components: {
+ Icon,
+ },
+
computed: {
- iconClass() {
- return {
- 'fa-eye-slash': this.isConfidential,
- 'fa-lock': this.isLocked,
- };
+ warningIcon() {
+ if (this.isConfidential) return 'eye-slash';
+ if (this.isLocked) return 'lock';
+
+ return '';
},
isLockedAndConfidential() {
@@ -30,12 +36,13 @@
</script>
<template>
<div class="issuable-note-warning">
- <i
- aria-hidden="true"
- class="fa icon"
- :class="iconClass"
- v-if="!isLockedAndConfidential"
- ></i>
+ <icon
+ :name="warningIcon"
+ :size="16"
+ class="icon inline"
+ aria-hidden="true"
+ v-if="!isLockedAndConfidential">
+ </icon>
<span v-if="isLockedAndConfidential">
{{ __('This issue is confidential and locked.') }}
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
new file mode 100644
index 00000000000..247943f83e6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -0,0 +1,81 @@
+<script>
+
+/* This is a re-usable vue component for rendering a button
+ that will probably be sending off ajax requests and need
+ to show the loading status by setting the `loading` option.
+ This can also be used for initial page load when you don't
+ know the action of the button yet by setting
+ `loading: true, label: undefined`.
+
+ Sample configuration:
+
+ <loading-button
+ :loading="true"
+ :label="Hello"
+ @click="..."
+ />
+
+*/
+
+import loadingIcon from './loading_icon.vue';
+
+export default {
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ label: {
+ type: String,
+ required: false,
+ },
+ containerClass: {
+ type: String,
+ required: false,
+ default: 'btn btn-align-content',
+ },
+ },
+ components: {
+ loadingIcon,
+ },
+ methods: {
+ onClick(e) {
+ this.$emit('click', e);
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ @click="onClick"
+ type="button"
+ :class="containerClass"
+ :disabled="loading || disabled"
+ >
+ <transition name="fade">
+ <loading-icon
+ v-if="loading"
+ :inline="true"
+ class="js-loading-button-icon"
+ :class="{
+ 'append-right-5': label
+ }"
+ />
+ </transition>
+ <transition name="fade">
+ <span
+ v-if="label"
+ class="js-loading-button-label"
+ >
+ {{ label }}
+ </span>
+ </transition>
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 759d30c9c7c..15e3d713448 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,7 +1,9 @@
<script>
- /* global Flash */
+ import Flash from '../../../flash';
+ import GLForm from '../../../gl_form';
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
+ import icon from '../icon.vue';
export default {
props: {
@@ -23,6 +25,16 @@
type: String,
required: false,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ enableAutocomplete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -36,6 +48,7 @@
components: {
markdownHeader,
markdownToolbar,
+ icon,
},
computed: {
shouldShowReferencedUsers() {
@@ -44,8 +57,10 @@
},
},
methods: {
- toggleMarkdownPreview() {
- this.previewMarkdown = !this.previewMarkdown;
+ showPreviewTab() {
+ if (this.previewMarkdown) return;
+
+ this.previewMarkdown = true;
/*
Can't use `$refs` as the component is technically in the parent component
@@ -53,20 +68,22 @@
*/
const text = this.$slots.textarea[0].elm.value;
- if (!this.previewMarkdown) {
- this.markdownPreview = '';
- } else if (text) {
+ if (text) {
this.markdownPreviewLoading = true;
this.$http.post(this.markdownPreviewPath, { text })
.then(resp => resp.json())
- .then((data) => {
- this.renderMarkdown(data);
- })
+ .then(data => this.renderMarkdown(data))
.catch(() => new Flash('Error loading markdown preview'));
} else {
this.renderMarkdown();
}
},
+
+ showWriteTab() {
+ this.markdownPreview = '';
+ this.previewMarkdown = false;
+ },
+
renderMarkdown(data = {}) {
this.markdownPreviewLoading = false;
this.markdownPreview = data.body || 'Nothing to preview.';
@@ -85,7 +102,7 @@
/*
GLForm class handles all the toolbar buttons
*/
- return new gl.GLForm($(this.$refs['gl-form']), true);
+ return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete);
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('gl-form');
@@ -103,7 +120,8 @@
ref="gl-form">
<markdown-header
:preview-markdown="previewMarkdown"
- @toggle-markdown="toggleMarkdownPreview" />
+ @preview-markdown="showPreviewTab"
+ @write-markdown="showWriteTab" />
<div
class="md-write-holder"
v-show="!previewMarkdown">
@@ -113,14 +131,15 @@
class="zen-control zen-control-leave js-zen-leave"
href="#"
aria-label="Enter zen mode">
- <i
- class="fa fa-compress"
- aria-hidden="true">
- </i>
+ <icon
+ name="screen-normal"
+ :size="32">
+ </icon>
</a>
<markdown-toolbar
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
+ :can-attach-file="canAttachFile"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 5bf2a90cc3b..6c575d8eb49 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,6 +1,7 @@
<script>
import tooltip from '../../directives/tooltip';
import toolbarButton from './toolbar_button.vue';
+ import icon from '../icon.vue';
export default {
props: {
@@ -14,25 +15,34 @@
},
components: {
toolbarButton,
+ icon,
},
methods: {
- toggleMarkdownPreview(e, form) {
- if (form && !form.find('.js-vue-markdown-field').length) {
- return;
- } else if (e.target.blur) {
- e.target.blur();
- }
+ isMarkdownForm(form) {
+ return form && !form.find('.js-vue-markdown-field').length;
+ },
+
+ previewMarkdownTab(event, form) {
+ if (event.target.blur) event.target.blur();
+ if (this.isMarkdownForm(form)) return;
+
+ this.$emit('preview-markdown');
+ },
+
+ writeMarkdownTab(event, form) {
+ if (event.target.blur) event.target.blur();
+ if (this.isMarkdownForm(form)) return;
- this.$emit('toggle-markdown');
+ this.$emit('write-markdown');
},
},
mounted() {
- $(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
- $(document).on('markdown-preview:hide.vue', this.toggleMarkdownPreview);
+ $(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
+ $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
},
beforeDestroy() {
- $(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
- $(document).off('markdown-preview:hide.vue', this.toggleMarkdownPreview);
+ $(document).off('markdown-preview:show.vue', this.previewMarkdownTab);
+ $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
},
};
</script>
@@ -40,73 +50,74 @@
<template>
<div class="md-header">
<ul class="nav-links clearfix">
- <li :class="{ active: !previewMarkdown }">
+ <li
+ class="md-header-tab"
+ :class="{ active: !previewMarkdown }">
<a
+ class="js-write-link"
href="#md-write-holder"
tabindex="-1"
- @click.prevent="toggleMarkdownPreview($event)">
+ @click.prevent="writeMarkdownTab($event)">
Write
</a>
</li>
- <li :class="{ active: previewMarkdown }">
+ <li
+ class="md-header-tab"
+ :class="{ active: previewMarkdown }">
<a
+ class="js-preview-link"
href="#md-preview-holder"
tabindex="-1"
- @click.prevent="toggleMarkdownPreview($event)">
+ @click.prevent="previewMarkdownTab($event)">
Preview
</a>
</li>
- <li class="pull-right">
- <div class="toolbar-group">
- <toolbar-button
- tag="**"
- button-title="Add bold text"
- icon="bold" />
- <toolbar-button
- tag="*"
- button-title="Add italic text"
- icon="italic" />
- <toolbar-button
- tag="> "
- :prepend="true"
- button-title="Insert a quote"
- icon="quote-right" />
- <toolbar-button
- tag="`"
- tag-block="```"
- button-title="Insert code"
- icon="code" />
- <toolbar-button
- tag="* "
- :prepend="true"
- button-title="Add a bullet list"
- icon="list-ul" />
- <toolbar-button
- tag="1. "
- :prepend="true"
- button-title="Add a numbered list"
- icon="list-ol" />
- <toolbar-button
- tag="* [ ] "
- :prepend="true"
- button-title="Add a task list"
- icon="check-square-o" />
- </div>
- <div class="toolbar-group">
- <button
- v-tooltip
- aria-label="Go full screen"
- class="toolbar-btn js-zen-enter"
- data-container="body"
- tabindex="-1"
- title="Go full screen"
- type="button">
- <i
- aria-hidden="true"
- class="fa fa-arrows-alt fa-fw">
- </i>
- </button>
- </div>
+ <li class="md-header-toolbar">
+ <toolbar-button
+ tag="**"
+ button-title="Add bold text"
+ icon="bold" />
+ <toolbar-button
+ tag="*"
+ button-title="Add italic text"
+ icon="italic" />
+ <toolbar-button
+ tag="> "
+ :prepend="true"
+ button-title="Insert a quote"
+ icon="quote" />
+ <toolbar-button
+ tag="`"
+ tag-block="```"
+ button-title="Insert code"
+ icon="code" />
+ <toolbar-button
+ tag="* "
+ :prepend="true"
+ button-title="Add a bullet list"
+ icon="list-bulleted" />
+ <toolbar-button
+ tag="1. "
+ :prepend="true"
+ button-title="Add a numbered list"
+ icon="list-numbered" />
+ <toolbar-button
+ tag="* [ ] "
+ :prepend="true"
+ button-title="Add a task list"
+ icon="task-done" />
+ <button
+ v-tooltip
+ aria-label="Go full screen"
+ class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
+ data-container="body"
+ tabindex="-1"
+ title="Go full screen"
+ type="button">
+ <icon
+ name="screen-full">
+ </icon>
+ </button>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 65fe7bbd94e..ea2509d2839 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -9,6 +9,11 @@
type: String,
required: false,
},
+ canAttachFile: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
};
</script>
@@ -41,7 +46,10 @@
are supported
</template>
</div>
- <span class="uploading-container">
+ <span
+ v-if="canAttachFile"
+ class="uploading-container"
+ >
<span class="uploading-progress-container hide">
<i
class="fa fa-file-image-o toolbar-button-icon"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index f7da7ebfcfe..e3e41f8f0ca 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -1,5 +1,6 @@
<script>
import tooltip from '../../directives/tooltip';
+ import icon from '../icon.vue';
export default {
props: {
@@ -26,14 +27,12 @@
default: false,
},
},
+ components: {
+ icon,
+ },
directives: {
tooltip,
},
- computed: {
- iconClass() {
- return `fa-${this.icon}`;
- },
- },
};
</script>
@@ -41,7 +40,7 @@
<button
v-tooltip
type="button"
- class="toolbar-btn js-md hidden-xs"
+ class="toolbar-btn js-md"
tabindex="-1"
data-container="body"
:data-md-tag="tag"
@@ -49,10 +48,8 @@
:data-md-prepend="prepend"
:title="buttonTitle"
:aria-label="buttonTitle">
- <i
- aria-hidden="true"
- class="fa fa-fw"
- :class="iconClass">
- </i>
+ <icon
+ :name="icon">
+ </icon>
</button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
new file mode 100644
index 00000000000..a2ddd565170
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -0,0 +1,76 @@
+<script>
+ /**
+ * Given an array of tabs, renders non linked bootstrap tabs.
+ * When a tab is clicked it will trigger an event and provide the clicked scope.
+ *
+ * This component is used in apps that handle the API call.
+ * If you only need to change the URL this component should not be used.
+ *
+ * @example
+ * <navigation-tabs
+ * :tabs="[
+ * {
+ * name: String,
+ * scope: String,
+ * count: Number || Undefined,
+ * isActive: Boolean,
+ * },
+ * ]"
+ * @onChangeTab="onChangeTab"
+ * />
+ */
+ export default {
+ name: 'NavigationTabs',
+ props: {
+ tabs: {
+ type: Array,
+ required: true,
+ },
+ scope: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ mounted() {
+ $(document).trigger('init.scrolling-tabs');
+ },
+ methods: {
+ shouldRenderBadge(count) {
+ // 0 is valid in a badge, but evaluates to false, we need to check for undefined
+ return count !== undefined;
+ },
+
+ onTabClick(tab) {
+ this.$emit('onChangeTab', tab.scope);
+ },
+ },
+};
+</script>
+<template>
+ <ul class="nav-links scrolling-tabs">
+ <li
+ v-for="(tab, i) in tabs"
+ :key="i"
+ :class="{
+ active: tab.isActive,
+ }"
+ >
+ <a
+ role="button"
+ @click="onTabClick(tab)"
+ :class="`js-${scope}-tab-${tab.scope}`"
+ >
+ {{ tab.name }}
+
+ <span
+ v-if="shouldRenderBadge(tab.count)"
+ class="badge"
+ >
+ {{tab.count}}
+ </span>
+
+ </a>
+ </li>
+ </ul>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
new file mode 100644
index 00000000000..e467ca56704
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -0,0 +1,70 @@
+<script>
+ /**
+ * Common component to render a placeholder note and user information.
+ *
+ * This component needs to be used with a vuex store.
+ * That vuex store needs to have a `getUserData` getter that contains
+ * {
+ * path: String,
+ * avatar_url: String,
+ * name: String,
+ * username: String,
+ * }
+ *
+ * @example
+ * <placeholder-note
+ * :note="{body: 'This is a note'}"
+ * />
+ */
+ import { mapGetters } from 'vuex';
+ import userAvatarLink from '../user_avatar/user_avatar_link.vue';
+
+ export default {
+ name: 'placeholderNote',
+ 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/vue_shared/components/notes/placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue
new file mode 100644
index 00000000000..d805fea8006
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue
@@ -0,0 +1,29 @@
+<script>
+ /**
+ * Common component to render a placeholder system note.
+ *
+ * @example
+ * <placeholder-system-note
+ * :note="{ body: 'Commands are being applied'}"
+ * />
+ */
+ 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/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
new file mode 100644
index 00000000000..98f8f32557d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -0,0 +1,73 @@
+<script>
+ /**
+ * Common component to render a system note, icon and user information.
+ *
+ * This component needs to be used with a vuex store.
+ * That vuex store needs to have a `targetNoteHash` getter
+ *
+ * @example
+ * <system-note
+ * :note="{
+ * id: String,
+ * author: Object,
+ * createdAt: String,
+ * note_html: String,
+ * system_note_icon_name: String
+ * }"
+ * />
+ */
+ import { mapGetters } from 'vuex';
+ import issueNoteHeader from '../../../notes/components/issue_note_header.vue';
+ import { spriteIcon } from '../../../lib/utils/common_utils';
+
+ 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;
+ },
+ iconHtml() {
+ return spriteIcon(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="iconHtml">
+ </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/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
new file mode 100644
index 00000000000..d8d974a2ff7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -0,0 +1,79 @@
+<script>
+ import Pikaday from 'pikaday';
+ import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix';
+
+ export default {
+ name: 'datePicker',
+ props: {
+ label: {
+ type: String,
+ required: false,
+ default: 'Date picker',
+ },
+ selectedDate: {
+ type: Date,
+ required: false,
+ },
+ minDate: {
+ type: Date,
+ required: false,
+ },
+ maxDate: {
+ type: Date,
+ required: false,
+ },
+ },
+ methods: {
+ selected(dateText) {
+ this.$emit('newDateSelected', this.calendar.toString(dateText));
+ },
+ toggled() {
+ this.$emit('hidePicker');
+ },
+ },
+ mounted() {
+ this.calendar = new Pikaday({
+ field: this.$el.querySelector('.dropdown-menu-toggle'),
+ theme: 'gitlab-theme animate-picker',
+ format: 'yyyy-mm-dd',
+ container: this.$el,
+ defaultDate: this.selectedDate,
+ setDefaultDate: !!this.selectedDate,
+ minDate: this.minDate,
+ maxDate: this.maxDate,
+ parse: dateString => parsePikadayDate(dateString),
+ toString: date => pikadayToString(date),
+ onSelect: this.selected.bind(this),
+ onClose: this.toggled.bind(this),
+ });
+
+ this.$el.append(this.calendar.el);
+ this.calendar.show();
+ },
+ beforeDestroy() {
+ this.calendar.destroy();
+ },
+ };
+</script>
+
+<template>
+ <div class="pikaday-container">
+ <div class="dropdown open">
+ <button
+ type="button"
+ class="dropdown-menu-toggle"
+ data-toggle="dropdown"
+ @click="toggled"
+ >
+ <span class="dropdown-toggle-text">
+ {{label}}
+ </span>
+ <i
+ class="fa fa-chevron-down"
+ aria-hidden="true"
+ >
+ </i>
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
index 9279b50cd55..47efee64c6e 100644
--- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue
+++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
@@ -5,17 +5,32 @@ export default {
props: {
title: {
type: String,
- required: true,
+ required: false,
},
text: {
type: String,
- required: true,
+ required: false,
+ },
+ hideFooter: {
+ type: Boolean,
+ required: false,
+ default: false,
},
kind: {
type: String,
required: false,
default: 'primary',
},
+ modalDialogClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ closeKind: {
+ type: String,
+ required: false,
+ default: 'default',
+ },
closeButtonLabel: {
type: String,
required: false,
@@ -25,6 +40,11 @@ export default {
type: String,
required: true,
},
+ submitDisabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
@@ -33,6 +53,11 @@ export default {
[`btn-${this.kind}`]: true,
};
},
+ btnCancelKindClass() {
+ return {
+ [`btn-${this.closeKind}`]: true,
+ };
+ },
},
methods: {
@@ -47,41 +72,58 @@ export default {
</script>
<template>
-<div
- class="modal popup-dialog"
- role="dialog"
- tabindex="-1">
- <div class="modal-dialog" role="document">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button"
- class="close"
- @click="close"
- aria-label="Close">
- <span aria-hidden="true">&times;</span>
- </button>
- <h4 class="modal-title">{{this.title}}</h4>
- </div>
- <div class="modal-body">
- <slot name="body" :text="text">
- <p>{{text}}</p>
- </slot>
- </div>
- <div class="modal-footer">
- <button
- type="button"
- class="btn btn-default"
- @click="emitSubmit(false)">
- {{closeButtonLabel}}
- </button>
- <button type="button"
- class="btn"
- :class="btnKindClass"
- @click="emitSubmit(true)">
- {{primaryButtonLabel}}
- </button>
+<div class="modal-open">
+ <div
+ class="modal popup-dialog"
+ role="dialog"
+ tabindex="-1"
+ >
+ <div
+ :class="modalDialogClass"
+ class="modal-dialog"
+ role="document"
+ >
+ <div class="modal-content">
+ <div class="modal-header">
+ <slot name="header">
+ <h4 class="modal-title pull-left">
+ {{this.title}}
+ </h4>
+ <button
+ type="button"
+ class="close pull-right"
+ @click="close"
+ aria-label="Close"
+ >
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </slot>
+ </div>
+ <div class="modal-body">
+ <slot name="body" :text="text">
+ <p>{{this.text}}</p>
+ </slot>
+ </div>
+ <div class="modal-footer" v-if="!hideFooter">
+ <button
+ type="button"
+ class="btn pull-left"
+ :class="btnCancelKindClass"
+ @click="close">
+ {{ closeButtonLabel }}
+ </button>
+ <button
+ type="button"
+ class="btn pull-right"
+ :disabled="submitDisabled"
+ :class="btnKindClass"
+ @click="emitSubmit(true)">
+ {{ primaryButtonLabel }}
+ </button>
+ </div>
</div>
</div>
</div>
+ <div class="modal-backdrop fade in" />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
new file mode 100644
index 00000000000..a88e1310131
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
@@ -0,0 +1,46 @@
+<script>
+ export default {
+ name: 'collapsedCalendarIcon',
+ props: {
+ containerClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ text: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showIcon: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ methods: {
+ click() {
+ this.$emit('click');
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ :class="containerClass"
+ @click="click"
+ >
+ <i
+ v-if="showIcon"
+ class="fa fa-calendar"
+ aria-hidden="true"
+ >
+ </i>
+ <slot>
+ <span>
+ {{ text }}
+ </span>
+ </slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
new file mode 100644
index 00000000000..9ede5553bc5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
@@ -0,0 +1,109 @@
+<script>
+ import { dateInWords } from '../../../lib/utils/datetime_utility';
+ import toggleSidebar from './toggle_sidebar.vue';
+ import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
+
+ export default {
+ name: 'sidebarCollapsedGroupedDatePicker',
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ showToggleSidebar: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ minDate: {
+ type: Date,
+ required: false,
+ },
+ maxDate: {
+ type: Date,
+ required: false,
+ },
+ disableClickableIcons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ components: {
+ toggleSidebar,
+ collapsedCalendarIcon,
+ },
+ computed: {
+ hasMinAndMaxDates() {
+ return this.minDate && this.maxDate;
+ },
+ hasNoMinAndMaxDates() {
+ return !this.minDate && !this.maxDate;
+ },
+ showMinDateBlock() {
+ return this.minDate || this.hasNoMinAndMaxDates;
+ },
+ showFromText() {
+ return !this.maxDate && this.minDate;
+ },
+ iconClass() {
+ const disabledClass = this.disableClickableIcons ? 'disabled' : '';
+ return `block sidebar-collapsed-icon calendar-icon ${disabledClass}`;
+ },
+ },
+ methods: {
+ toggleSidebar() {
+ this.$emit('toggleCollapse');
+ },
+ dateText(dateType = 'min') {
+ const date = this[`${dateType}Date`];
+ const dateWords = dateInWords(date, true);
+ const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
+
+ return date ? parsedDateWords : 'None';
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="block sidebar-grouped-item">
+ <div
+ v-if="showToggleSidebar"
+ class="issuable-sidebar-header"
+ >
+ <toggle-sidebar
+ :collapsed="collapsed"
+ @toggle="toggleSidebar"
+ />
+ </div>
+ <collapsed-calendar-icon
+ v-if="showMinDateBlock"
+ :container-class="iconClass"
+ @click="toggleSidebar"
+ >
+ <span class="sidebar-collapsed-value">
+ <span v-if="showFromText">From</span>
+ <span>{{ dateText('min') }}</span>
+ </span>
+ </collapsed-calendar-icon>
+ <div
+ v-if="hasMinAndMaxDates"
+ class="text-center sidebar-collapsed-divider"
+ >
+ -
+ </div>
+ <collapsed-calendar-icon
+ v-if="maxDate"
+ :container-class="iconClass"
+ :show-icon="!minDate"
+ @click="toggleSidebar"
+ >
+ <span class="sidebar-collapsed-value">
+ <span v-if="!minDate">Until</span>
+ <span>{{ dateText('max') }}</span>
+ </span>
+ </collapsed-calendar-icon>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
new file mode 100644
index 00000000000..9c3413377a3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -0,0 +1,163 @@
+<script>
+ import datePicker from '../pikaday.vue';
+ import loadingIcon from '../loading_icon.vue';
+ import toggleSidebar from './toggle_sidebar.vue';
+ import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
+ import { dateInWords } from '../../../lib/utils/datetime_utility';
+
+ export default {
+ name: 'sidebarDatePicker',
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ showToggleSidebar: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ editable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ label: {
+ type: String,
+ required: false,
+ default: 'Date picker',
+ },
+ selectedDate: {
+ type: Date,
+ required: false,
+ },
+ minDate: {
+ type: Date,
+ required: false,
+ },
+ maxDate: {
+ type: Date,
+ required: false,
+ },
+ },
+ data() {
+ return {
+ editing: false,
+ };
+ },
+ components: {
+ datePicker,
+ toggleSidebar,
+ loadingIcon,
+ collapsedCalendarIcon,
+ },
+ computed: {
+ selectedAndEditable() {
+ return this.selectedDate && this.editable;
+ },
+ selectedDateWords() {
+ return dateInWords(this.selectedDate, true);
+ },
+ collapsedText() {
+ return this.selectedDateWords ? this.selectedDateWords : 'None';
+ },
+ },
+ methods: {
+ stopEditing() {
+ this.editing = false;
+ },
+ toggleDatePicker() {
+ this.editing = !this.editing;
+ },
+ newDateSelected(date = null) {
+ this.date = date;
+ this.editing = false;
+ this.$emit('saveDate', date);
+ },
+ toggleSidebar() {
+ this.$emit('toggleCollapse');
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="block">
+ <div class="issuable-sidebar-header">
+ <toggle-sidebar
+ :collapsed="collapsed"
+ @toggle="toggleSidebar"
+ />
+ </div>
+ <collapsed-calendar-icon
+ class="sidebar-collapsed-icon"
+ :text="collapsedText"
+ />
+ <div class="title">
+ {{ label }}
+ <loading-icon
+ v-if="isLoading"
+ :inline="true"
+ />
+ <div class="pull-right">
+ <button
+ v-if="editable && !editing"
+ type="button"
+ class="btn-blank btn-link btn-primary-hover-link btn-sidebar-action"
+ @click="toggleDatePicker"
+ >
+ Edit
+ </button>
+ <toggle-sidebar
+ v-if="showToggleSidebar"
+ :collapsed="collapsed"
+ @toggle="toggleSidebar"
+ />
+ </div>
+ </div>
+ <div class="value">
+ <date-picker
+ v-if="editing"
+ :selected-date="selectedDate"
+ :min-date="minDate"
+ :max-date="maxDate"
+ :label="label"
+ @newDateSelected="newDateSelected"
+ @hidePicker="stopEditing"
+ />
+ <span
+ v-else
+ class="value-content"
+ >
+ <template v-if="selectedDate">
+ <strong>{{ selectedDateWords }}</strong>
+ <span
+ v-if="selectedAndEditable"
+ class="no-value"
+ >
+ -
+ <button
+ type="button"
+ class="btn-blank btn-link btn-secondary-hover-link"
+ @click="newDateSelected(null)"
+ >
+ remove
+ </button>
+ </span>
+ </template>
+ <span
+ v-else
+ class="no-value"
+ >
+ None
+ </span>
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
new file mode 100644
index 00000000000..5ae76adad71
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
@@ -0,0 +1,30 @@
+<script>
+ export default {
+ name: 'toggleSidebar',
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ methods: {
+ toggle() {
+ this.$emit('toggle');
+ },
+ },
+ };
+</script>
+
+<template>
+ <button
+ type="button"
+ class="btn btn-blank gutter-toggle btn-sidebar-action"
+ @click="toggle"
+ >
+ <i
+ aria-label="toggle collapse"
+ class="fa"
+ :class="{ 'fa-angle-double-right': !collapsed, 'fa-angle-double-left': collapsed }"
+ ></i>
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
new file mode 100644
index 00000000000..b06493e6c66
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
@@ -0,0 +1,37 @@
+<script>
+ export default {
+ props: {
+ small: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ lines: {
+ type: Number,
+ required: false,
+ default: 6,
+ },
+ },
+ computed: {
+ lineClasses() {
+ return new Array(this.lines).fill().map((_, i) => `skeleton-line-${i + 1}`);
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ class="animation-container"
+ :class="{
+ 'animation-container-small': small,
+ }"
+ >
+ <div
+ v-for="(css, index) in lineClasses"
+ :key="index"
+ :class="css"
+ >
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index dd9a2ebb184..1ac61a3c39b 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -7,6 +7,7 @@
Sample configuration:
<user-avatar-image
+ :lazy="true"
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
@@ -16,11 +17,17 @@
*/
import defaultAvatarUrl from 'images/no_avatar.png';
+import { placeholderImage } from '../../../lazy_loader';
import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarImage',
props: {
+ lazy: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
imgSrc: {
type: String,
required: false,
@@ -56,18 +63,21 @@ export default {
tooltip,
},
computed: {
+ // API response sends null when gravatar is disabled and
+ // we provide an empty string when we use it inside user avatar link.
+ // In both cases we should render the defaultAvatarUrl
+ sanitizedSource() {
+ return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
+ },
+ resultantSrcAttribute() {
+ return this.lazy ? placeholderImage : this.sanitizedSource;
+ },
tooltipContainer() {
return this.tooltipText ? 'body' : null;
},
avatarSizeClass() {
return `s${this.size}`;
},
- // API response sends null when gravatar is disabled and
- // we provide an empty string when we use it inside user avatar link.
- // In both cases we should render the defaultAvatarUrl
- imageSource() {
- return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
- },
},
};
</script>
@@ -76,11 +86,16 @@ export default {
<img
v-tooltip
class="avatar"
- :class="[avatarSizeClass, cssClasses]"
- :src="imageSource"
+ :class="{
+ lazy,
+ [avatarSizeClass]: true,
+ [cssClasses]: true
+ }"
+ :src="resultantSrcAttribute"
:width="size"
:height="size"
:alt="imgAlt"
+ :data-src="sanitizedSource"
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index 95898d54cf7..dc32e783258 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -12,12 +12,14 @@
:img-alt="tooltipText"
:img-size="20"
:tooltip-text="tooltipText"
- tooltip-placement="top"
+ :tooltip-placement="top"
+ :username="username"
/>
*/
import userAvatarImage from './user_avatar_image.vue';
+import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarLink',
@@ -60,6 +62,22 @@ export default {
required: false,
default: 'top',
},
+ username: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ shouldShowUsername() {
+ return this.username.length > 0;
+ },
+ avatarTooltipText() {
+ return this.shouldShowUsername ? '' : this.tooltipText;
+ },
+ },
+ directives: {
+ tooltip,
},
};
</script>
@@ -73,8 +91,13 @@ export default {
:img-alt="imgAlt"
:css-classes="imgCssClasses"
:size="imgSize"
- :tooltip-text="tooltipText"
+ :tooltip-text="avatarTooltipText"
+ :tooltip-placement="tooltipPlacement"
+ /><span
+ v-if="shouldShowUsername"
+ v-tooltip
+ :title="tooltipText"
:tooltip-placement="tooltipPlacement"
- />
+ >{{username}}</span>
</a>
</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
new file mode 100644
index 00000000000..f94cc670edf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
@@ -0,0 +1,42 @@
+/**
+ * API callbacks for pagination and tabs
+ * shared between Pipelines and Environments table.
+ *
+ * Components need to have `scope`, `page` and `requestData`
+ */
+import {
+ historyPushState,
+ buildUrlWithCurrentLocation,
+} from '../../lib/utils/common_utils';
+
+export default {
+ methods: {
+ onChangeTab(scope) {
+ this.updateContent({ scope, page: '1' });
+ },
+
+ onChangePage(page) {
+ /* URLS parameters are strings, we need to parse to match types */
+ this.updateContent({ scope: this.scope, page: Number(page).toString() });
+ },
+
+ updateInternalState(parameters) {
+ // stop polling
+ this.poll.stop();
+
+ const queryString = Object.keys(parameters).map((parameter) => {
+ const value = parameters[parameter];
+ // update internal state for UI
+ this[parameter] = value;
+ return `${parameter}=${encodeURIComponent(value)}`;
+ }).join('&');
+
+ // update polling parameters
+ this.requestData = parameters;
+
+ historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
+
+ this.isLoading = true;
+ },
+ },
+};
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
index a0025ddb598..7a865587444 100644
--- a/app/assets/javascripts/wikis.js
+++ b/app/assets/javascripts/wikis.js
@@ -1,4 +1,5 @@
import bp from './breakpoints';
+import { slugify } from './lib/utils/text_utility';
export default class Wikis {
constructor() {
@@ -23,7 +24,7 @@ export default class Wikis {
if (!this.newWikiForm) return;
const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
- const slug = gl.text.slugify(slugInput.value);
+ const slug = slugify(slugInput.value);
if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index 99c7644e4d9..06a86f3b94a 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -11,8 +11,6 @@ import Dropzone from 'dropzone';
import 'mousetrap';
import 'mousetrap/plugins/pause/mousetrap-pause';
-window.Dropzone = Dropzone;
-
//
// ### Events
//
@@ -73,7 +71,7 @@ export default class ZenMode {
this.active_textarea = this.active_backdrop.find('textarea');
// Prevent a user-resized textarea from persisting to fullscreen
this.active_textarea.removeAttr('style');
- return this.active_textarea.focus();
+ this.active_textarea.focus();
}
exit() {
@@ -83,7 +81,11 @@ export default class ZenMode {
this.scrollTo(this.active_textarea);
this.active_textarea = null;
this.active_backdrop = null;
- return Dropzone.forElement('.div-dropzone').enable();
+
+ const $dropzone = $('.div-dropzone');
+ if ($dropzone && !$dropzone.hasClass('js-invalid-dropzone')) {
+ Dropzone.forElement('.div-dropzone').enable();
+ }
}
}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index c7be94e2c8e..66212be1b8f 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -5,8 +5,10 @@
@import "framework/layout";
@import "framework/animations";
+@import "framework/vue_transitions";
@import "framework/avatar";
@import "framework/asciidoctor";
+@import "framework/banner";
@import "framework/blocks";
@import "framework/buttons";
@import "framework/badges";
@@ -32,12 +34,14 @@
@import "framework/modal";
@import "framework/pagination";
@import "framework/panels";
+@import "framework/popup";
@import "framework/secondary-navigation-elements";
@import "framework/selects";
@import "framework/sidebar";
-@import "framework/new-sidebar";
+@import "framework/contextual-sidebar";
@import "framework/tables";
@import "framework/notes";
+@import "framework/tabs";
@import "framework/timeline";
@import "framework/tooltips";
@import "framework/typography";
@@ -53,4 +57,4 @@
@import "framework/icons";
@import "framework/snippets";
@import "framework/memory_graph";
-@import "framework/responsive-tables";
+@import "framework/responsive_tables";
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index f0e6b23757f..728f9a27aca 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -125,7 +125,7 @@
@include transition(border-color);
}
-.note-action-button .link-highlight,
+.note-action-button,
.toolbar-btn,
.dropdown-toggle-caret {
@include transition(color);
@@ -198,6 +198,13 @@ a {
height: 12px;
}
+ &.animation-container-right {
+ .skeleton-line-2 {
+ left: 0;
+ right: 150px;
+ }
+ }
+
&::before {
animation-duration: 1s;
animation-fill-mode: forwards;
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index f1aedc227f3..26db2386879 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -42,8 +42,7 @@
&.avatar-inline {
float: none;
display: inline-block;
- margin-left: 4px;
- margin-bottom: 2px;
+ margin-left: 2px;
flex-shrink: 0;
-webkit-flex-shrink: 0;
@@ -59,7 +58,7 @@
&.avatar-tile {
border-radius: 0;
- border: none;
+ border: 0;
}
&:not([href]):hover {
@@ -96,7 +95,7 @@
.avatar {
border-radius: 0;
- border: none;
+ border: 0;
height: auto;
width: 100%;
margin: 0;
diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss
new file mode 100644
index 00000000000..6433b0c7855
--- /dev/null
+++ b/app/assets/stylesheets/framework/banner.scss
@@ -0,0 +1,25 @@
+.banner-callout {
+ display: flex;
+ position: relative;
+ flex-wrap: wrap;
+
+ .banner-close {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ opacity: 1;
+
+ .dismiss-icon {
+ color: $gl-text-color;
+ font-size: $gl-font-size;
+ }
+ }
+
+ .banner-graphic {
+ margin: 20px auto;
+ }
+
+ &.banner-non-empty-state {
+ border-bottom: 1px solid $border-color;
+ }
+}
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
index 6bb096fc5bd..9982a5779af 100644
--- a/app/assets/stylesheets/framework/blank.scss
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -7,29 +7,76 @@
width: 100%;
height: 100%;
padding-bottom: 25px;
- border: 1px solid $border-color;
border-radius: $border-radius-default;
}
}
-.blank-state {
- padding-top: 20px;
- padding-bottom: 20px;
+.blank-state-row {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-around;
+ height: 100%;
+}
+
+.blank-state-welcome {
text-align: center;
+ padding: 20px 0 40px;
+
+ .blank-state-welcome-title {
+ font-size: 24px;
+ }
+
+ .blank-state-text {
+ margin-bottom: 0;
+ }
+}
+
+.blank-state-link {
+ display: block;
+ color: $gl-text-color;
+ flex: 0 0 100%;
+ margin-bottom: 15px;
- &.blank-state-welcome {
- .blank-state-welcome-title {
- font-size: 24px;
+ @media (min-width: $screen-sm-min) {
+ flex: 0 0 49%;
+
+ &:nth-child(odd) {
+ margin-right: 5px;
}
- .blank-state-text {
- margin-bottom: 0;
+ &:nth-child(even) {
+ margin-left: 5px;
}
}
- .blank-state-icon {
- padding-bottom: 20px;
+ &:hover {
+ background-color: $gray-light;
+ text-decoration: none;
+ color: $gl-text-color;
+ }
+}
+.blank-state-center {
+ padding-top: 20px;
+ padding-bottom: 20px;
+ text-align: center;
+}
+
+.blank-state {
+ padding: 20px;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+
+ @media (min-width: $screen-sm-min) {
+ display: flex;
+ align-items: center;
+ padding: 50px 30px;
+ }
+}
+
+.blank-state,
+.blank-state-center {
+ .blank-state-icon {
svg {
display: block;
margin: auto;
@@ -38,13 +85,17 @@
.blank-state-title {
margin-top: 0;
- margin-bottom: 10px;
font-size: 18px;
}
- .blank-state-text {
- max-width: $container-text-max-width;
- margin: 0 auto $gl-padding;
- font-size: 14px;
+ .blank-state-body {
+ @media (max-width: $screen-xs-max) {
+ text-align: center;
+ margin-top: 20px;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ padding-left: 20px;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index dbd990f84c1..91976ca1f56 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -39,7 +39,11 @@
}
&.top-block {
- border-top: none;
+ border-top: 0;
+
+ .container-fluid {
+ background-color: inherit;
+ }
}
&.middle-block {
@@ -59,7 +63,7 @@
&.footer-block {
margin-top: 0;
- border-bottom: none;
+ border-bottom: 0;
margin-bottom: -$gl-padding;
}
@@ -96,11 +100,7 @@
&.build-content {
background-color: $white-light;
- border-top: none;
- }
-
- &.top-block .container-fluid {
- background-color: inherit;
+ border-top: 0;
}
}
@@ -209,7 +209,6 @@
padding: 24px 0 0;
.nav-links {
- justify-content: center;
width: 100%;
float: none;
@@ -217,6 +216,14 @@
float: none;
}
}
+
+ li:first-child {
+ margin-left: auto;
+ }
+
+ li:last-child {
+ margin-right: auto;
+ }
}
.group-info {
@@ -280,12 +287,12 @@
cursor: pointer;
color: $blue-300;
z-index: 1;
- border: none;
+ border: 0;
background-color: transparent;
&:hover,
&:focus {
- border: none;
+ border: 0;
color: $blue-400;
}
}
@@ -346,3 +353,7 @@
display: -webkit-flex;
display: flex;
}
+
+.flex-right {
+ margin-left: auto;
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index b131e2d57ee..cdc2aa196dd 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -88,17 +88,6 @@
border-color: $border-dark;
color: $color;
}
-
- svg {
-
- path {
- fill: $color;
- }
-
- use {
- stroke: $color;
- }
- }
}
@mixin btn-green {
@@ -142,6 +131,13 @@
}
}
+@mixin btn-svg {
+ height: $gl-padding;
+ width: $gl-padding;
+ top: 0;
+ vertical-align: text-top;
+}
+
.btn {
@include btn-default;
@include btn-white;
@@ -292,6 +288,12 @@
}
}
+.btn-align-content {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
.btn-group {
&.btn-grouped {
@include btn-with-margin;
@@ -299,7 +301,7 @@
}
.btn-clipboard {
- border: none;
+ border: 0;
padding: 0 5px;
}
@@ -402,6 +404,7 @@
padding: 0;
background: transparent;
border: 0;
+ border-radius: 0;
&:hover,
&:active,
@@ -411,3 +414,29 @@
box-shadow: none;
}
}
+
+.btn-link.btn-secondary-hover-link {
+ color: $gl-text-color-secondary;
+
+ &:hover,
+ &:active,
+ &:focus {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+}
+
+.btn-link.btn-primary-hover-link {
+ color: inherit;
+
+ &:hover,
+ &:active,
+ &:focus {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+}
+
+.btn-svg svg {
+ @include btn-svg;
+}
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index e0e46dd73af..1bd94c0acba 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -12,15 +12,15 @@
border-left: 3px solid $border-color;
color: $text-color;
background: $gray-light;
-}
-.bs-callout h4 {
- margin-top: 0;
- margin-bottom: 5px;
-}
+ h4 {
+ margin-top: 0;
+ margin-bottom: 5px;
+ }
-.bs-callout p:last-child {
- margin-bottom: 0;
+ p:last-child {
+ margin-bottom: 0;
+ }
}
/* Variations */
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 96f9dda26c4..cb1aad90a9c 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -5,31 +5,39 @@
.cgreen { color: $common-green; }
.cdark { color: $common-gray-dark; }
-/** COMMON CLASSES **/
-.prepend-top-0 { margin-top: 0; }
-.prepend-top-5 { margin-top: 5px; }
-.prepend-top-10 { margin-top: 10px; }
-.prepend-top-default { margin-top: $gl-padding !important; }
-.prepend-top-20 { margin-top: 20px; }
-.prepend-left-4 { margin-left: 4px; }
-.prepend-left-5 { margin-left: 5px; }
-.prepend-left-10 { margin-left: 10px; }
-.prepend-left-default { margin-left: $gl-padding; }
-.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; }
-.append-bottom-default { margin-bottom: $gl-padding; }
-.inline { display: inline-block; }
-.center { text-align: center; }
-.vertical-align-middle { vertical-align: middle; }
+.text-plain,
+.text-plain:hover {
+ color: $gl-text-color;
+}
+
+.text-secondary {
+ color: $gl-text-color-secondary;
+}
+
+.text-primary,
+.text-primary:hover {
+ color: $brand-primary;
+}
+
+.text-success,
+.text-success:hover {
+ color: $brand-success;
+}
+
+.text-danger,
+.text-danger:hover {
+ color: $brand-danger;
+}
+
+.text-warning,
+.text-warning:hover {
+ color: $brand-warning;
+}
+
+.text-info,
+.text-info:hover {
+ color: $brand-info;
+}
.underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: $hint-color; }
@@ -54,7 +62,7 @@
pre {
&.clean {
background: none;
- border: none;
+ border: 0;
margin: 0;
padding: 0;
}
@@ -79,6 +87,14 @@ hr {
.str-truncated {
@include str-truncated;
+
+ &-60 {
+ @include str-truncated(60%);
+ }
+
+ &-100 {
+ @include str-truncated(100%);
+ }
}
.block-truncated {
@@ -104,10 +120,17 @@ hr {
font-size: 14px;
}
-table a code {
- position: relative;
- top: -2px;
- margin-right: 3px;
+table {
+ a code {
+ position: relative;
+ top: -2px;
+ margin-right: 3px;
+ }
+
+ td.permission-x {
+ background: $table-permission-x-bg !important;
+ text-align: center;
+ }
}
.loading {
@@ -153,7 +176,7 @@ li.note {
img { max-width: 100%; }
.note-title {
li {
- border-bottom: none !important;
+ border-bottom: 0 !important;
}
}
}
@@ -198,7 +221,7 @@ li.note {
pre {
background: $white-light;
- border: none;
+ border: 0;
font-size: 12px;
}
}
@@ -292,13 +315,6 @@ img.emoji {
margin-bottom: 10px;
}
-table {
- td.permission-x {
- background: $table-permission-x-bg !important;
- text-align: center;
- }
-}
-
.btn-sign-in {
text-shadow: none;
@@ -364,10 +380,11 @@ table {
.dropzone .dz-preview .dz-progress {
border-color: $border-color !important;
-}
-.dropzone .dz-preview .dz-progress .dz-upload {
- background: $gl-success !important;
+ .dz-upload {
+ background: $gl-success !important;
+ }
+
}
.dz-message {
@@ -403,7 +420,7 @@ table {
}
.hide-bottom-border {
- border-bottom: none !important;
+ border-bottom: 0 !important;
}
.gl-accessibility {
@@ -428,16 +445,6 @@ table {
border-radius: $border-radius-default;
}
-.str-truncated {
- &-60 {
- @include str-truncated(60%);
- }
-
- &-100 {
- @include str-truncated(100%);
- }
-}
-
.tooltip {
.tooltip-inner {
word-wrap: break-word;
@@ -448,3 +455,31 @@ table {
pointer-events: none;
opacity: .5;
}
+
+/** COMMON CLASSES **/
+.prepend-top-0 { margin-top: 0; }
+.prepend-top-5 { margin-top: 5px; }
+.prepend-top-8 { margin-top: $grid-size; }
+.prepend-top-10 { margin-top: 10px; }
+.prepend-top-15 { margin-top: 15px; }
+.prepend-top-default { margin-top: $gl-padding !important; }
+.prepend-top-20 { margin-top: 20px; }
+.prepend-left-4 { margin-left: 4px; }
+.prepend-left-5 { margin-left: 5px; }
+.prepend-left-10 { margin-left: 10px; }
+.prepend-left-default { margin-left: $gl-padding; }
+.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; }
+.append-bottom-default { margin-bottom: $gl-padding; }
+.inline { display: inline-block; }
+.center { text-align: center; }
+.vertical-align-middle { vertical-align: middle; }
diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss
new file mode 100644
index 00000000000..b73932eb7e1
--- /dev/null
+++ b/app/assets/stylesheets/framework/contextual-sidebar.scss
@@ -0,0 +1,474 @@
+.page-with-contextual-sidebar {
+ @media (min-width: $screen-md-min) {
+ padding-left: $contextual-sidebar-collapsed-width;
+ }
+
+ @media (min-width: $screen-lg-min) {
+ padding-left: $contextual-sidebar-width;
+ }
+
+ // Override position: absolute
+ .right-sidebar {
+ position: fixed;
+ height: calc(100% - #{$header-height});
+ }
+
+ .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
+ padding: 10px 0 15px;
+ }
+}
+
+.page-with-icon-sidebar {
+ @media (min-width: $screen-sm-min) {
+ padding-left: $contextual-sidebar-collapsed-width;
+ }
+}
+
+.context-header {
+ position: relative;
+ margin-right: 2px;
+
+ a {
+ font-weight: $gl-font-weight-bold;
+ display: flex;
+ align-items: center;
+ padding: 10px 16px 10px 10px;
+ color: $gl-text-color;
+ }
+
+ &:hover,
+ a:hover {
+ background-color: $link-hover-background;
+ color: $gl-text-color;
+ }
+
+ .avatar-container {
+ flex: 0 0 40px;
+ background-color: $white-light;
+ }
+
+ .sidebar-context-title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
+
+.settings-avatar {
+ background-color: $white-light;
+
+ svg {
+ fill: $gl-text-color-secondary;
+ margin: auto;
+ }
+}
+
+.nav-sidebar {
+ position: fixed;
+ z-index: 400;
+ width: $contextual-sidebar-width;
+ transition: left $sidebar-transition-duration;
+ top: $header-height;
+ bottom: 0;
+ left: 0;
+ background-color: $gray-light;
+ box-shadow: inset -2px 0 0 $border-color;
+ transform: translate3d(0, 0, 0);
+
+ &:not(.sidebar-icons-only) {
+ @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
+ box-shadow: inset -2px 0 0 $border-color,
+ 2px 1px 3px $dropdown-shadow-color;
+ }
+ }
+
+ &.sidebar-icons-only {
+ width: auto;
+ min-width: $contextual-sidebar-collapsed-width;
+
+ .nav-sidebar-inner-scroll {
+ overflow-x: hidden;
+ }
+
+ .badge:not(.fly-out-badge),
+ .sidebar-context-title,
+ .nav-item-name {
+ display: none;
+ }
+
+ .sidebar-top-level-items > li > a {
+ min-height: 44px;
+ }
+
+ .fly-out-top-item {
+ display: block;
+ }
+
+ .avatar-container {
+ margin-right: 0;
+ }
+ }
+
+ &.nav-sidebar-expanded {
+ left: 0;
+ }
+
+ a {
+ transition: none;
+ text-decoration: none;
+ }
+
+ ul {
+ padding-left: 0;
+ list-style: none;
+ }
+
+ li {
+ white-space: nowrap;
+
+ a {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ color: $gl-text-color-secondary;
+ }
+
+ .nav-item-name {
+ flex: 1;
+ }
+
+ &.active {
+ > a {
+ font-weight: $gl-font-weight-bold;
+ }
+ }
+ }
+
+ @media (max-width: $screen-xs-max) {
+ left: (-$contextual-sidebar-width);
+ }
+
+ .nav-icon-container {
+ display: flex;
+ margin-right: 8px;
+ }
+
+ .fly-out-top-item {
+ display: none;
+ }
+
+ svg {
+ height: 16px;
+ width: 16px;
+ }
+}
+
+.nav-sidebar-inner-scroll {
+ height: 100%;
+ width: 100%;
+ overflow: auto;
+
+ @media (min-width: $screen-sm-min) {
+ overflow: hidden;
+ }
+}
+
+.with-performance-bar .nav-sidebar {
+ top: $header-height + $performance-bar-height;
+}
+
+.sidebar-sub-level-items {
+ display: none;
+ padding-bottom: 8px;
+
+ > li {
+ a {
+ padding: 8px 16px 8px 40px;
+
+ &:hover,
+ &:focus {
+ background: $link-active-background;
+ color: $gl-text-color;
+ }
+ }
+
+ &.active {
+ a {
+ &,
+ &:hover,
+ &:focus {
+ background: $link-active-background;
+ }
+ }
+ }
+ }
+}
+
+.sidebar-top-level-items {
+ margin-bottom: 60px;
+
+ > li {
+ > a {
+ @media (min-width: $screen-sm-min) {
+ margin-right: 2px;
+ }
+
+ &:hover {
+ color: $gl-text-color;
+ }
+ }
+
+ &.is-showing-fly-out {
+ > a {
+ margin-right: 2px;
+ }
+
+ .sidebar-sub-level-items {
+ @media (min-width: $screen-sm-min) {
+ position: fixed;
+ top: 0;
+ left: 0;
+ min-width: 150px;
+ margin-top: -1px;
+ padding: 4px 1px;
+ background-color: $white-light;
+ box-shadow: 2px 1px 3px $dropdown-shadow-color;
+ border: 1px solid $gray-darker;
+ border-left: 0;
+ border-radius: 0 3px 3px 0;
+
+ &::before {
+ content: "";
+ position: absolute;
+ top: -30px;
+ bottom: -30px;
+ left: -10px;
+ right: -30px;
+ z-index: -1;
+ }
+
+ &.is-above {
+ margin-top: 1px;
+ }
+
+ .divider {
+ height: 1px;
+ margin: 4px -1px;
+ padding: 0;
+ background-color: $dropdown-divider-color;
+ }
+
+ > .active {
+ box-shadow: none;
+
+ > a {
+ background-color: transparent;
+ }
+ }
+
+ a {
+ padding: 8px 16px;
+ color: $gl-text-color;
+
+ &:hover,
+ &:focus {
+ background-color: $gray-darker;
+ }
+ }
+ }
+ }
+ }
+
+ .badge {
+ background-color: $inactive-badge-background;
+ color: $gl-text-color-secondary;
+ }
+
+ &.active {
+ background: $link-active-background;
+
+ > a {
+ margin-left: 4px;
+ padding-left: 12px;
+ }
+
+ .badge {
+ font-weight: $gl-font-weight-bold;
+ }
+
+ .sidebar-sub-level-items:not(.is-fly-out-only) {
+ display: block;
+ }
+ }
+
+ &.active > a:hover,
+ &.is-over > a {
+ background-color: $link-hover-background;
+ }
+ }
+}
+
+
+// Collapsed nav
+
+.toggle-sidebar-button,
+.close-nav-button {
+ width: $contextual-sidebar-width - 2px;
+ position: fixed;
+ bottom: 0;
+ padding: 16px;
+ background-color: $gray-light;
+ border: 0;
+ border-top: 2px solid $border-color;
+ color: $gl-text-color-secondary;
+ display: flex;
+ align-items: center;
+
+ svg {
+ margin-right: 8px;
+ }
+
+ .icon-angle-double-right {
+ display: none;
+ }
+
+ &:hover {
+ background-color: $border-color;
+ color: $gl-text-color;
+ }
+}
+
+.toggle-sidebar-button {
+ @media (max-width: $screen-xs-max) {
+ display: none;
+ }
+}
+
+
+.sidebar-icons-only {
+ .context-header {
+ height: 61px;
+
+ a {
+ padding: 10px 4px;
+ }
+ }
+
+ li a {
+ padding: 12px 15px;
+ }
+
+ .sidebar-top-level-items > li {
+ &.active a {
+ padding-left: 12px;
+ }
+
+ .sidebar-sub-level-items {
+ &:not(.flyout-list) {
+ display: none;
+ }
+ }
+ }
+
+ .nav-icon-container {
+ margin-right: 0;
+ }
+
+ .toggle-sidebar-button {
+ width: $contextual-sidebar-collapsed-width - 2px;
+ padding: 16px;
+
+ .collapse-text,
+ .icon-angle-double-left {
+ display: none;
+ }
+
+ .icon-angle-double-right {
+ display: block;
+ margin: 0;
+ }
+ }
+}
+
+.fly-out-top-item {
+ > a {
+ display: flex;
+ }
+
+ .fly-out-badge {
+ margin-left: 8px;
+ }
+}
+
+.fly-out-top-item-name {
+ flex: 1;
+}
+
+// Mobile nav
+
+.close-nav-button {
+ display: none;
+}
+
+.toggle-mobile-nav {
+ display: none;
+ background-color: transparent;
+ border: 0;
+ padding: 6px 16px;
+ margin: 0 0 0 -15px;
+ height: 46px;
+
+ i {
+ font-size: 20px;
+ color: $gl-text-color-secondary;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ display: flex;
+ align-items: center;
+
+ i {
+ font-size: 18px;
+ }
+ }
+
+ @media (max-width: $screen-xs-max) {
+ + .breadcrumbs-links {
+ padding-left: $gl-padding;
+ border-left: 1px solid $gl-text-color-quaternary;
+ }
+ }
+}
+
+@media (max-width: $screen-xs-max) {
+ .close-nav-button {
+ display: flex;
+ }
+}
+
+.mobile-overlay {
+ display: none;
+
+ &.mobile-nav-open {
+ display: block;
+ position: fixed;
+ background-color: $black-transparent;
+ height: 100%;
+ width: 100%;
+ z-index: 300;
+ }
+}
+
+
+// Make issue boards full-height now that sub-nav is gone
+
+.boards-list {
+ height: calc(100vh - #{$header-height});
+
+ @media (min-width: $screen-sm-min) {
+ height: calc(100vh - 180px);
+ }
+}
+
+.with-performance-bar .boards-list {
+ height: calc(100vh - #{$header-height} - #{$performance-bar-height});
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 5b950ae0ba0..30d5d7a653b 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -37,6 +37,7 @@
.dropdown-menu-nav {
@include set-visible;
display: block;
+ min-height: 40px;
@media (max-width: $screen-xs-max) {
width: 100%;
@@ -727,11 +728,11 @@
.pika-single.animate-picker.is-bound {
@include set-visible;
-}
-.pika-single.animate-picker.is-bound.is-hidden {
- @include set-invisible;
- overflow: hidden;
+ &.is-hidden {
+ @include set-invisible;
+ overflow: hidden;
+ }
}
@mixin dropdown-item-hover {
@@ -749,7 +750,7 @@
margin-bottom: $dropdown-vertical-offset;
}
- li:not(.dropdown-bold-header) {
+ li {
display: block;
padding: 0 1px;
@@ -776,12 +777,15 @@
a,
button,
.menu-item {
+ margin-bottom: 0;
border-radius: 0;
box-shadow: none;
padding: 8px 16px;
text-align: left;
white-space: normal;
width: 100%;
+ font-weight: $gl-font-weight-normal;
+ line-height: normal;
&.dropdown-menu-user-link {
white-space: nowrap;
@@ -838,6 +842,7 @@
a {
padding: 8px 40px;
+ &.is-indeterminate::before,
&.is-active::before {
left: 16px;
}
@@ -934,9 +939,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
border-right: 0;
}
}
-}
-.projects-dropdown-container {
.projects-list-frequent-container,
.projects-list-search-container, {
padding: 8px 0;
@@ -947,11 +950,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
.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;
}
@@ -1004,6 +1002,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
+ white-space: nowrap;
}
&:hover {
diff --git a/app/assets/stylesheets/framework/emoji-sprites.scss b/app/assets/stylesheets/framework/emoji-sprites.scss
index 925415f84b1..0174e17b660 100644
--- a/app/assets/stylesheets/framework/emoji-sprites.scss
+++ b/app/assets/stylesheets/framework/emoji-sprites.scss
@@ -765,1031 +765,1033 @@
.emoji-full_moon { background-position: -160px -540px; }
.emoji-full_moon_with_face { background-position: -180px -540px; }
.emoji-game_die { background-position: -200px -540px; }
-.emoji-gear { background-position: -220px -540px; }
-.emoji-gem { background-position: -240px -540px; }
-.emoji-gemini { background-position: -260px -540px; }
-.emoji-ghost { background-position: -280px -540px; }
-.emoji-gift { background-position: -300px -540px; }
-.emoji-gift_heart { background-position: -320px -540px; }
-.emoji-girl { background-position: -340px -540px; }
-.emoji-girl_tone1 { background-position: -360px -540px; }
-.emoji-girl_tone2 { background-position: -380px -540px; }
-.emoji-girl_tone3 { background-position: -400px -540px; }
-.emoji-girl_tone4 { background-position: -420px -540px; }
-.emoji-girl_tone5 { background-position: -440px -540px; }
-.emoji-globe_with_meridians { background-position: -460px -540px; }
-.emoji-goal { background-position: -480px -540px; }
-.emoji-goat { background-position: -500px -540px; }
-.emoji-golf { background-position: -520px -540px; }
-.emoji-golfer { background-position: -540px -540px; }
-.emoji-gorilla { background-position: -560px 0; }
-.emoji-grapes { background-position: -560px -20px; }
-.emoji-green_apple { background-position: -560px -40px; }
-.emoji-green_book { background-position: -560px -60px; }
-.emoji-green_heart { background-position: -560px -80px; }
-.emoji-grey_exclamation { background-position: -560px -100px; }
-.emoji-grey_question { background-position: -560px -120px; }
-.emoji-grimacing { background-position: -560px -140px; }
-.emoji-grin { background-position: -560px -160px; }
-.emoji-grinning { background-position: -560px -180px; }
-.emoji-guardsman { background-position: -560px -200px; }
-.emoji-guardsman_tone1 { background-position: -560px -220px; }
-.emoji-guardsman_tone2 { background-position: -560px -240px; }
-.emoji-guardsman_tone3 { background-position: -560px -260px; }
-.emoji-guardsman_tone4 { background-position: -560px -280px; }
-.emoji-guardsman_tone5 { background-position: -560px -300px; }
-.emoji-guitar { background-position: -560px -320px; }
-.emoji-gun { background-position: -560px -340px; }
-.emoji-haircut { background-position: -560px -360px; }
-.emoji-haircut_tone1 { background-position: -560px -380px; }
-.emoji-haircut_tone2 { background-position: -560px -400px; }
-.emoji-haircut_tone3 { background-position: -560px -420px; }
-.emoji-haircut_tone4 { background-position: -560px -440px; }
-.emoji-haircut_tone5 { background-position: -560px -460px; }
-.emoji-hamburger { background-position: -560px -480px; }
-.emoji-hammer { background-position: -560px -500px; }
-.emoji-hammer_pick { background-position: -560px -520px; }
-.emoji-hamster { background-position: -560px -540px; }
-.emoji-hand_splayed { background-position: 0 -560px; }
-.emoji-hand_splayed_tone1 { background-position: -20px -560px; }
-.emoji-hand_splayed_tone2 { background-position: -40px -560px; }
-.emoji-hand_splayed_tone3 { background-position: -60px -560px; }
-.emoji-hand_splayed_tone4 { background-position: -80px -560px; }
-.emoji-hand_splayed_tone5 { background-position: -100px -560px; }
-.emoji-handbag { background-position: -120px -560px; }
-.emoji-handball { background-position: -140px -560px; }
-.emoji-handball_tone1 { background-position: -160px -560px; }
-.emoji-handball_tone2 { background-position: -180px -560px; }
-.emoji-handball_tone3 { background-position: -200px -560px; }
-.emoji-handball_tone4 { background-position: -220px -560px; }
-.emoji-handball_tone5 { background-position: -240px -560px; }
-.emoji-handshake { background-position: -260px -560px; }
-.emoji-handshake_tone1 { background-position: -280px -560px; }
-.emoji-handshake_tone2 { background-position: -300px -560px; }
-.emoji-handshake_tone3 { background-position: -320px -560px; }
-.emoji-handshake_tone4 { background-position: -340px -560px; }
-.emoji-handshake_tone5 { background-position: -360px -560px; }
-.emoji-hash { background-position: -380px -560px; }
-.emoji-hatched_chick { background-position: -400px -560px; }
-.emoji-hatching_chick { background-position: -420px -560px; }
-.emoji-head_bandage { background-position: -440px -560px; }
-.emoji-headphones { background-position: -460px -560px; }
-.emoji-hear_no_evil { background-position: -480px -560px; }
-.emoji-heart { background-position: -500px -560px; }
-.emoji-heart_decoration { background-position: -520px -560px; }
-.emoji-heart_exclamation { background-position: -540px -560px; }
-.emoji-heart_eyes { background-position: -560px -560px; }
-.emoji-heart_eyes_cat { background-position: -580px 0; }
-.emoji-heartbeat { background-position: -580px -20px; }
-.emoji-heartpulse { background-position: -580px -40px; }
-.emoji-hearts { background-position: -580px -60px; }
-.emoji-heavy_check_mark { background-position: -580px -80px; }
-.emoji-heavy_division_sign { background-position: -580px -100px; }
-.emoji-heavy_dollar_sign { background-position: -580px -120px; }
-.emoji-heavy_minus_sign { background-position: -580px -140px; }
-.emoji-heavy_multiplication_x { background-position: -580px -160px; }
-.emoji-heavy_plus_sign { background-position: -580px -180px; }
-.emoji-helicopter { background-position: -580px -200px; }
-.emoji-helmet_with_cross { background-position: -580px -220px; }
-.emoji-herb { background-position: -580px -240px; }
-.emoji-hibiscus { background-position: -580px -260px; }
-.emoji-high_brightness { background-position: -580px -280px; }
-.emoji-high_heel { background-position: -580px -300px; }
-.emoji-hockey { background-position: -580px -320px; }
-.emoji-hole { background-position: -580px -340px; }
-.emoji-homes { background-position: -580px -360px; }
-.emoji-honey_pot { background-position: -580px -380px; }
-.emoji-horse { background-position: -580px -400px; }
-.emoji-horse_racing { background-position: -580px -420px; }
-.emoji-horse_racing_tone1 { background-position: -580px -440px; }
-.emoji-horse_racing_tone2 { background-position: -580px -460px; }
-.emoji-horse_racing_tone3 { background-position: -580px -480px; }
-.emoji-horse_racing_tone4 { background-position: -580px -500px; }
-.emoji-horse_racing_tone5 { background-position: -580px -520px; }
-.emoji-hospital { background-position: -580px -540px; }
-.emoji-hot_pepper { background-position: -580px -560px; }
-.emoji-hotdog { background-position: 0 -580px; }
-.emoji-hotel { background-position: -20px -580px; }
-.emoji-hotsprings { background-position: -40px -580px; }
-.emoji-hourglass { background-position: -60px -580px; }
-.emoji-hourglass_flowing_sand { background-position: -80px -580px; }
-.emoji-house { background-position: -100px -580px; }
-.emoji-house_abandoned { background-position: -120px -580px; }
-.emoji-house_with_garden { background-position: -140px -580px; }
-.emoji-hugging { background-position: -160px -580px; }
-.emoji-hushed { background-position: -180px -580px; }
-.emoji-ice_cream { background-position: -200px -580px; }
-.emoji-ice_skate { background-position: -220px -580px; }
-.emoji-icecream { background-position: -240px -580px; }
-.emoji-id { background-position: -260px -580px; }
-.emoji-ideograph_advantage { background-position: -280px -580px; }
-.emoji-imp { background-position: -300px -580px; }
-.emoji-inbox_tray { background-position: -320px -580px; }
-.emoji-incoming_envelope { background-position: -340px -580px; }
-.emoji-information_desk_person { background-position: -360px -580px; }
-.emoji-information_desk_person_tone1 { background-position: -380px -580px; }
-.emoji-information_desk_person_tone2 { background-position: -400px -580px; }
-.emoji-information_desk_person_tone3 { background-position: -420px -580px; }
-.emoji-information_desk_person_tone4 { background-position: -440px -580px; }
-.emoji-information_desk_person_tone5 { background-position: -460px -580px; }
-.emoji-information_source { background-position: -480px -580px; }
-.emoji-innocent { background-position: -500px -580px; }
-.emoji-interrobang { background-position: -520px -580px; }
-.emoji-iphone { background-position: -540px -580px; }
-.emoji-island { background-position: -560px -580px; }
-.emoji-izakaya_lantern { background-position: -580px -580px; }
-.emoji-jack_o_lantern { background-position: -600px 0; }
-.emoji-japan { background-position: -600px -20px; }
-.emoji-japanese_castle { background-position: -600px -40px; }
-.emoji-japanese_goblin { background-position: -600px -60px; }
-.emoji-japanese_ogre { background-position: -600px -80px; }
-.emoji-jeans { background-position: -600px -100px; }
-.emoji-joy { background-position: -600px -120px; }
-.emoji-joy_cat { background-position: -600px -140px; }
-.emoji-joystick { background-position: -600px -160px; }
-.emoji-juggling { background-position: -600px -180px; }
-.emoji-juggling_tone1 { background-position: -600px -200px; }
-.emoji-juggling_tone2 { background-position: -600px -220px; }
-.emoji-juggling_tone3 { background-position: -600px -240px; }
-.emoji-juggling_tone4 { background-position: -600px -260px; }
-.emoji-juggling_tone5 { background-position: -600px -280px; }
-.emoji-kaaba { background-position: -600px -300px; }
-.emoji-key { background-position: -600px -320px; }
-.emoji-key2 { background-position: -600px -340px; }
-.emoji-keyboard { background-position: -600px -360px; }
-.emoji-kimono { background-position: -600px -380px; }
-.emoji-kiss { background-position: -600px -400px; }
-.emoji-kiss_mm { background-position: -600px -420px; }
-.emoji-kiss_ww { background-position: -600px -440px; }
-.emoji-kissing { background-position: -600px -460px; }
-.emoji-kissing_cat { background-position: -600px -480px; }
-.emoji-kissing_closed_eyes { background-position: -600px -500px; }
-.emoji-kissing_heart { background-position: -600px -520px; }
-.emoji-kissing_smiling_eyes { background-position: -600px -540px; }
-.emoji-kiwi { background-position: -600px -560px; }
-.emoji-knife { background-position: -600px -580px; }
-.emoji-koala { background-position: 0 -600px; }
-.emoji-koko { background-position: -20px -600px; }
-.emoji-label { background-position: -40px -600px; }
-.emoji-large_blue_circle { background-position: -60px -600px; }
-.emoji-large_blue_diamond { background-position: -80px -600px; }
-.emoji-large_orange_diamond { background-position: -100px -600px; }
-.emoji-last_quarter_moon { background-position: -120px -600px; }
-.emoji-last_quarter_moon_with_face { background-position: -140px -600px; }
-.emoji-laughing { background-position: -160px -600px; }
-.emoji-leaves { background-position: -180px -600px; }
-.emoji-ledger { background-position: -200px -600px; }
-.emoji-left_facing_fist { background-position: -220px -600px; }
-.emoji-left_facing_fist_tone1 { background-position: -240px -600px; }
-.emoji-left_facing_fist_tone2 { background-position: -260px -600px; }
-.emoji-left_facing_fist_tone3 { background-position: -280px -600px; }
-.emoji-left_facing_fist_tone4 { background-position: -300px -600px; }
-.emoji-left_facing_fist_tone5 { background-position: -320px -600px; }
-.emoji-left_luggage { background-position: -340px -600px; }
-.emoji-left_right_arrow { background-position: -360px -600px; }
-.emoji-leftwards_arrow_with_hook { background-position: -380px -600px; }
-.emoji-lemon { background-position: -400px -600px; }
-.emoji-leo { background-position: -420px -600px; }
-.emoji-leopard { background-position: -440px -600px; }
-.emoji-level_slider { background-position: -460px -600px; }
-.emoji-levitate { background-position: -480px -600px; }
-.emoji-libra { background-position: -500px -600px; }
-.emoji-lifter { background-position: -520px -600px; }
-.emoji-lifter_tone1 { background-position: -540px -600px; }
-.emoji-lifter_tone2 { background-position: -560px -600px; }
-.emoji-lifter_tone3 { background-position: -580px -600px; }
-.emoji-lifter_tone4 { background-position: -600px -600px; }
-.emoji-lifter_tone5 { background-position: -620px 0; }
-.emoji-light_rail { background-position: -620px -20px; }
-.emoji-link { background-position: -620px -40px; }
-.emoji-lion_face { background-position: -620px -60px; }
-.emoji-lips { background-position: -620px -80px; }
-.emoji-lipstick { background-position: -620px -100px; }
-.emoji-lizard { background-position: -620px -120px; }
-.emoji-lock { background-position: -620px -140px; }
-.emoji-lock_with_ink_pen { background-position: -620px -160px; }
-.emoji-lollipop { background-position: -620px -180px; }
-.emoji-loop { background-position: -620px -200px; }
-.emoji-loud_sound { background-position: -620px -220px; }
-.emoji-loudspeaker { background-position: -620px -240px; }
-.emoji-love_hotel { background-position: -620px -260px; }
-.emoji-love_letter { background-position: -620px -280px; }
-.emoji-low_brightness { background-position: -620px -300px; }
-.emoji-lying_face { background-position: -620px -320px; }
-.emoji-m { background-position: -620px -340px; }
-.emoji-mag { background-position: -620px -360px; }
-.emoji-mag_right { background-position: -620px -380px; }
-.emoji-mahjong { background-position: -620px -400px; }
-.emoji-mailbox { background-position: -620px -420px; }
-.emoji-mailbox_closed { background-position: -620px -440px; }
-.emoji-mailbox_with_mail { background-position: -620px -460px; }
-.emoji-mailbox_with_no_mail { background-position: -620px -480px; }
-.emoji-man { background-position: -620px -500px; }
-.emoji-man_dancing { background-position: -620px -520px; }
-.emoji-man_dancing_tone1 { background-position: -620px -540px; }
-.emoji-man_dancing_tone2 { background-position: -620px -560px; }
-.emoji-man_dancing_tone3 { background-position: -620px -580px; }
-.emoji-man_dancing_tone4 { background-position: -620px -600px; }
-.emoji-man_dancing_tone5 { background-position: 0 -620px; }
-.emoji-man_in_tuxedo { background-position: -20px -620px; }
-.emoji-man_in_tuxedo_tone1 { background-position: -40px -620px; }
-.emoji-man_in_tuxedo_tone2 { background-position: -60px -620px; }
-.emoji-man_in_tuxedo_tone3 { background-position: -80px -620px; }
-.emoji-man_in_tuxedo_tone4 { background-position: -100px -620px; }
-.emoji-man_in_tuxedo_tone5 { background-position: -120px -620px; }
-.emoji-man_tone1 { background-position: -140px -620px; }
-.emoji-man_tone2 { background-position: -160px -620px; }
-.emoji-man_tone3 { background-position: -180px -620px; }
-.emoji-man_tone4 { background-position: -200px -620px; }
-.emoji-man_tone5 { background-position: -220px -620px; }
-.emoji-man_with_gua_pi_mao { background-position: -240px -620px; }
-.emoji-man_with_gua_pi_mao_tone1 { background-position: -260px -620px; }
-.emoji-man_with_gua_pi_mao_tone2 { background-position: -280px -620px; }
-.emoji-man_with_gua_pi_mao_tone3 { background-position: -300px -620px; }
-.emoji-man_with_gua_pi_mao_tone4 { background-position: -320px -620px; }
-.emoji-man_with_gua_pi_mao_tone5 { background-position: -340px -620px; }
-.emoji-man_with_turban { background-position: -360px -620px; }
-.emoji-man_with_turban_tone1 { background-position: -380px -620px; }
-.emoji-man_with_turban_tone2 { background-position: -400px -620px; }
-.emoji-man_with_turban_tone3 { background-position: -420px -620px; }
-.emoji-man_with_turban_tone4 { background-position: -440px -620px; }
-.emoji-man_with_turban_tone5 { background-position: -460px -620px; }
-.emoji-mans_shoe { background-position: -480px -620px; }
-.emoji-map { background-position: -500px -620px; }
-.emoji-maple_leaf { background-position: -520px -620px; }
-.emoji-martial_arts_uniform { background-position: -540px -620px; }
-.emoji-mask { background-position: -560px -620px; }
-.emoji-massage { background-position: -580px -620px; }
-.emoji-massage_tone1 { background-position: -600px -620px; }
-.emoji-massage_tone2 { background-position: -620px -620px; }
-.emoji-massage_tone3 { background-position: -640px 0; }
-.emoji-massage_tone4 { background-position: -640px -20px; }
-.emoji-massage_tone5 { background-position: -640px -40px; }
-.emoji-meat_on_bone { background-position: -640px -60px; }
-.emoji-medal { background-position: -640px -80px; }
-.emoji-mega { background-position: -640px -100px; }
-.emoji-melon { background-position: -640px -120px; }
-.emoji-menorah { background-position: -640px -140px; }
-.emoji-mens { background-position: -640px -160px; }
-.emoji-metal { background-position: -640px -180px; }
-.emoji-metal_tone1 { background-position: -640px -200px; }
-.emoji-metal_tone2 { background-position: -640px -220px; }
-.emoji-metal_tone3 { background-position: -640px -240px; }
-.emoji-metal_tone4 { background-position: -640px -260px; }
-.emoji-metal_tone5 { background-position: -640px -280px; }
-.emoji-metro { background-position: -640px -300px; }
-.emoji-microphone { background-position: -640px -320px; }
-.emoji-microphone2 { background-position: -640px -340px; }
-.emoji-microscope { background-position: -640px -360px; }
-.emoji-middle_finger { background-position: -640px -380px; }
-.emoji-middle_finger_tone1 { background-position: -640px -400px; }
-.emoji-middle_finger_tone2 { background-position: -640px -420px; }
-.emoji-middle_finger_tone3 { background-position: -640px -440px; }
-.emoji-middle_finger_tone4 { background-position: -640px -460px; }
-.emoji-middle_finger_tone5 { background-position: -640px -480px; }
-.emoji-military_medal { background-position: -640px -500px; }
-.emoji-milk { background-position: -640px -520px; }
-.emoji-milky_way { background-position: -640px -540px; }
-.emoji-minibus { background-position: -640px -560px; }
-.emoji-minidisc { background-position: -640px -580px; }
-.emoji-mobile_phone_off { background-position: -640px -600px; }
-.emoji-money_mouth { background-position: -640px -620px; }
-.emoji-money_with_wings { background-position: 0 -640px; }
-.emoji-moneybag { background-position: -20px -640px; }
-.emoji-monkey { background-position: -40px -640px; }
-.emoji-monkey_face { background-position: -60px -640px; }
-.emoji-monorail { background-position: -80px -640px; }
-.emoji-mortar_board { background-position: -100px -640px; }
-.emoji-mosque { background-position: -120px -640px; }
-.emoji-motor_scooter { background-position: -140px -640px; }
-.emoji-motorboat { background-position: -160px -640px; }
-.emoji-motorcycle { background-position: -180px -640px; }
-.emoji-motorway { background-position: -200px -640px; }
-.emoji-mount_fuji { background-position: -220px -640px; }
-.emoji-mountain { background-position: -240px -640px; }
-.emoji-mountain_bicyclist { background-position: -260px -640px; }
-.emoji-mountain_bicyclist_tone1 { background-position: -280px -640px; }
-.emoji-mountain_bicyclist_tone2 { background-position: -300px -640px; }
-.emoji-mountain_bicyclist_tone3 { background-position: -320px -640px; }
-.emoji-mountain_bicyclist_tone4 { background-position: -340px -640px; }
-.emoji-mountain_bicyclist_tone5 { background-position: -360px -640px; }
-.emoji-mountain_cableway { background-position: -380px -640px; }
-.emoji-mountain_railway { background-position: -400px -640px; }
-.emoji-mountain_snow { background-position: -420px -640px; }
-.emoji-mouse { background-position: -440px -640px; }
-.emoji-mouse2 { background-position: -460px -640px; }
-.emoji-mouse_three_button { background-position: -480px -640px; }
-.emoji-movie_camera { background-position: -500px -640px; }
-.emoji-moyai { background-position: -520px -640px; }
-.emoji-mrs_claus { background-position: -540px -640px; }
-.emoji-mrs_claus_tone1 { background-position: -560px -640px; }
-.emoji-mrs_claus_tone2 { background-position: -580px -640px; }
-.emoji-mrs_claus_tone3 { background-position: -600px -640px; }
-.emoji-mrs_claus_tone4 { background-position: -620px -640px; }
-.emoji-mrs_claus_tone5 { background-position: -640px -640px; }
-.emoji-muscle { background-position: -660px 0; }
-.emoji-muscle_tone1 { background-position: -660px -20px; }
-.emoji-muscle_tone2 { background-position: -660px -40px; }
-.emoji-muscle_tone3 { background-position: -660px -60px; }
-.emoji-muscle_tone4 { background-position: -660px -80px; }
-.emoji-muscle_tone5 { background-position: -660px -100px; }
-.emoji-mushroom { background-position: -660px -120px; }
-.emoji-musical_keyboard { background-position: -660px -140px; }
-.emoji-musical_note { background-position: -660px -160px; }
-.emoji-musical_score { background-position: -660px -180px; }
-.emoji-mute { background-position: -660px -200px; }
-.emoji-nail_care { background-position: -660px -220px; }
-.emoji-nail_care_tone1 { background-position: -660px -240px; }
-.emoji-nail_care_tone2 { background-position: -660px -260px; }
-.emoji-nail_care_tone3 { background-position: -660px -280px; }
-.emoji-nail_care_tone4 { background-position: -660px -300px; }
-.emoji-nail_care_tone5 { background-position: -660px -320px; }
-.emoji-name_badge { background-position: -660px -340px; }
-.emoji-nauseated_face { background-position: -660px -360px; }
-.emoji-necktie { background-position: -660px -380px; }
-.emoji-negative_squared_cross_mark { background-position: -660px -400px; }
-.emoji-nerd { background-position: -660px -420px; }
-.emoji-neutral_face { background-position: -660px -440px; }
-.emoji-new { background-position: -660px -460px; }
-.emoji-new_moon { background-position: -660px -480px; }
-.emoji-new_moon_with_face { background-position: -660px -500px; }
-.emoji-newspaper { background-position: -660px -520px; }
-.emoji-newspaper2 { background-position: -660px -540px; }
-.emoji-ng { background-position: -660px -560px; }
-.emoji-night_with_stars { background-position: -660px -580px; }
-.emoji-nine { background-position: -660px -600px; }
-.emoji-no_bell { background-position: -660px -620px; }
-.emoji-no_bicycles { background-position: -660px -640px; }
-.emoji-no_entry { background-position: 0 -660px; }
-.emoji-no_entry_sign { background-position: -20px -660px; }
-.emoji-no_good { background-position: -40px -660px; }
-.emoji-no_good_tone1 { background-position: -60px -660px; }
-.emoji-no_good_tone2 { background-position: -80px -660px; }
-.emoji-no_good_tone3 { background-position: -100px -660px; }
-.emoji-no_good_tone4 { background-position: -120px -660px; }
-.emoji-no_good_tone5 { background-position: -140px -660px; }
-.emoji-no_mobile_phones { background-position: -160px -660px; }
-.emoji-no_mouth { background-position: -180px -660px; }
-.emoji-no_pedestrians { background-position: -200px -660px; }
-.emoji-no_smoking { background-position: -220px -660px; }
-.emoji-non-potable_water { background-position: -240px -660px; }
-.emoji-nose { background-position: -260px -660px; }
-.emoji-nose_tone1 { background-position: -280px -660px; }
-.emoji-nose_tone2 { background-position: -300px -660px; }
-.emoji-nose_tone3 { background-position: -320px -660px; }
-.emoji-nose_tone4 { background-position: -340px -660px; }
-.emoji-nose_tone5 { background-position: -360px -660px; }
-.emoji-notebook { background-position: -380px -660px; }
-.emoji-notebook_with_decorative_cover { background-position: -400px -660px; }
-.emoji-notepad_spiral { background-position: -420px -660px; }
-.emoji-notes { background-position: -440px -660px; }
-.emoji-nut_and_bolt { background-position: -460px -660px; }
-.emoji-o { background-position: -480px -660px; }
-.emoji-o2 { background-position: -500px -660px; }
-.emoji-ocean { background-position: -520px -660px; }
-.emoji-octagonal_sign { background-position: -540px -660px; }
-.emoji-octopus { background-position: -560px -660px; }
-.emoji-oden { background-position: -580px -660px; }
-.emoji-office { background-position: -600px -660px; }
-.emoji-oil { background-position: -620px -660px; }
-.emoji-ok { background-position: -640px -660px; }
-.emoji-ok_hand { background-position: -660px -660px; }
-.emoji-ok_hand_tone1 { background-position: -680px 0; }
-.emoji-ok_hand_tone2 { background-position: -680px -20px; }
-.emoji-ok_hand_tone3 { background-position: -680px -40px; }
-.emoji-ok_hand_tone4 { background-position: -680px -60px; }
-.emoji-ok_hand_tone5 { background-position: -680px -80px; }
-.emoji-ok_woman { background-position: -680px -100px; }
-.emoji-ok_woman_tone1 { background-position: -680px -120px; }
-.emoji-ok_woman_tone2 { background-position: -680px -140px; }
-.emoji-ok_woman_tone3 { background-position: -680px -160px; }
-.emoji-ok_woman_tone4 { background-position: -680px -180px; }
-.emoji-ok_woman_tone5 { background-position: -680px -200px; }
-.emoji-older_man { background-position: -680px -220px; }
-.emoji-older_man_tone1 { background-position: -680px -240px; }
-.emoji-older_man_tone2 { background-position: -680px -260px; }
-.emoji-older_man_tone3 { background-position: -680px -280px; }
-.emoji-older_man_tone4 { background-position: -680px -300px; }
-.emoji-older_man_tone5 { background-position: -680px -320px; }
-.emoji-older_woman { background-position: -680px -340px; }
-.emoji-older_woman_tone1 { background-position: -680px -360px; }
-.emoji-older_woman_tone2 { background-position: -680px -380px; }
-.emoji-older_woman_tone3 { background-position: -680px -400px; }
-.emoji-older_woman_tone4 { background-position: -680px -420px; }
-.emoji-older_woman_tone5 { background-position: -680px -440px; }
-.emoji-om_symbol { background-position: -680px -460px; }
-.emoji-on { background-position: -680px -480px; }
-.emoji-oncoming_automobile { background-position: -680px -500px; }
-.emoji-oncoming_bus { background-position: -680px -520px; }
-.emoji-oncoming_police_car { background-position: -680px -540px; }
-.emoji-oncoming_taxi { background-position: -680px -560px; }
-.emoji-one { background-position: -680px -580px; }
-.emoji-open_file_folder { background-position: -680px -600px; }
-.emoji-open_hands { background-position: -680px -620px; }
-.emoji-open_hands_tone1 { background-position: -680px -640px; }
-.emoji-open_hands_tone2 { background-position: -680px -660px; }
-.emoji-open_hands_tone3 { background-position: 0 -680px; }
-.emoji-open_hands_tone4 { background-position: -20px -680px; }
-.emoji-open_hands_tone5 { background-position: -40px -680px; }
-.emoji-open_mouth { background-position: -60px -680px; }
-.emoji-ophiuchus { background-position: -80px -680px; }
-.emoji-orange_book { background-position: -100px -680px; }
-.emoji-orthodox_cross { background-position: -120px -680px; }
-.emoji-outbox_tray { background-position: -140px -680px; }
-.emoji-owl { background-position: -160px -680px; }
-.emoji-ox { background-position: -180px -680px; }
-.emoji-package { background-position: -200px -680px; }
-.emoji-page_facing_up { background-position: -220px -680px; }
-.emoji-page_with_curl { background-position: -240px -680px; }
-.emoji-pager { background-position: -260px -680px; }
-.emoji-paintbrush { background-position: -280px -680px; }
-.emoji-palm_tree { background-position: -300px -680px; }
-.emoji-pancakes { background-position: -320px -680px; }
-.emoji-panda_face { background-position: -340px -680px; }
-.emoji-paperclip { background-position: -360px -680px; }
-.emoji-paperclips { background-position: -380px -680px; }
-.emoji-park { background-position: -400px -680px; }
-.emoji-parking { background-position: -420px -680px; }
-.emoji-part_alternation_mark { background-position: -440px -680px; }
-.emoji-partly_sunny { background-position: -460px -680px; }
-.emoji-passport_control { background-position: -480px -680px; }
-.emoji-pause_button { background-position: -500px -680px; }
-.emoji-peace { background-position: -520px -680px; }
-.emoji-peach { background-position: -540px -680px; }
-.emoji-peanuts { background-position: -560px -680px; }
-.emoji-pear { background-position: -580px -680px; }
-.emoji-pen_ballpoint { background-position: -600px -680px; }
-.emoji-pen_fountain { background-position: -620px -680px; }
-.emoji-pencil { background-position: -640px -680px; }
-.emoji-pencil2 { background-position: -660px -680px; }
-.emoji-penguin { background-position: -680px -680px; }
-.emoji-pensive { background-position: -700px 0; }
-.emoji-performing_arts { background-position: -700px -20px; }
-.emoji-persevere { background-position: -700px -40px; }
-.emoji-person_frowning { background-position: -700px -60px; }
-.emoji-person_frowning_tone1 { background-position: -700px -80px; }
-.emoji-person_frowning_tone2 { background-position: -700px -100px; }
-.emoji-person_frowning_tone3 { background-position: -700px -120px; }
-.emoji-person_frowning_tone4 { background-position: -700px -140px; }
-.emoji-person_frowning_tone5 { background-position: -700px -160px; }
-.emoji-person_with_blond_hair { background-position: -700px -180px; }
-.emoji-person_with_blond_hair_tone1 { background-position: -700px -200px; }
-.emoji-person_with_blond_hair_tone2 { background-position: -700px -220px; }
-.emoji-person_with_blond_hair_tone3 { background-position: -700px -240px; }
-.emoji-person_with_blond_hair_tone4 { background-position: -700px -260px; }
-.emoji-person_with_blond_hair_tone5 { background-position: -700px -280px; }
-.emoji-person_with_pouting_face { background-position: -700px -300px; }
-.emoji-person_with_pouting_face_tone1 { background-position: -700px -320px; }
-.emoji-person_with_pouting_face_tone2 { background-position: -700px -340px; }
-.emoji-person_with_pouting_face_tone3 { background-position: -700px -360px; }
-.emoji-person_with_pouting_face_tone4 { background-position: -700px -380px; }
-.emoji-person_with_pouting_face_tone5 { background-position: -700px -400px; }
-.emoji-pick { background-position: -700px -420px; }
-.emoji-pig { background-position: -700px -440px; }
-.emoji-pig2 { background-position: -700px -460px; }
-.emoji-pig_nose { background-position: -700px -480px; }
-.emoji-pill { background-position: -700px -500px; }
-.emoji-pineapple { background-position: -700px -520px; }
-.emoji-ping_pong { background-position: -700px -540px; }
-.emoji-pisces { background-position: -700px -560px; }
-.emoji-pizza { background-position: -700px -580px; }
-.emoji-place_of_worship { background-position: -700px -600px; }
-.emoji-play_pause { background-position: -700px -620px; }
-.emoji-point_down { background-position: -700px -640px; }
-.emoji-point_down_tone1 { background-position: -700px -660px; }
-.emoji-point_down_tone2 { background-position: -700px -680px; }
-.emoji-point_down_tone3 { background-position: 0 -700px; }
-.emoji-point_down_tone4 { background-position: -20px -700px; }
-.emoji-point_down_tone5 { background-position: -40px -700px; }
-.emoji-point_left { background-position: -60px -700px; }
-.emoji-point_left_tone1 { background-position: -80px -700px; }
-.emoji-point_left_tone2 { background-position: -100px -700px; }
-.emoji-point_left_tone3 { background-position: -120px -700px; }
-.emoji-point_left_tone4 { background-position: -140px -700px; }
-.emoji-point_left_tone5 { background-position: -160px -700px; }
-.emoji-point_right { background-position: -180px -700px; }
-.emoji-point_right_tone1 { background-position: -200px -700px; }
-.emoji-point_right_tone2 { background-position: -220px -700px; }
-.emoji-point_right_tone3 { background-position: -240px -700px; }
-.emoji-point_right_tone4 { background-position: -260px -700px; }
-.emoji-point_right_tone5 { background-position: -280px -700px; }
-.emoji-point_up { background-position: -300px -700px; }
-.emoji-point_up_2 { background-position: -320px -700px; }
-.emoji-point_up_2_tone1 { background-position: -340px -700px; }
-.emoji-point_up_2_tone2 { background-position: -360px -700px; }
-.emoji-point_up_2_tone3 { background-position: -380px -700px; }
-.emoji-point_up_2_tone4 { background-position: -400px -700px; }
-.emoji-point_up_2_tone5 { background-position: -420px -700px; }
-.emoji-point_up_tone1 { background-position: -440px -700px; }
-.emoji-point_up_tone2 { background-position: -460px -700px; }
-.emoji-point_up_tone3 { background-position: -480px -700px; }
-.emoji-point_up_tone4 { background-position: -500px -700px; }
-.emoji-point_up_tone5 { background-position: -520px -700px; }
-.emoji-police_car { background-position: -540px -700px; }
-.emoji-poodle { background-position: -560px -700px; }
-.emoji-poop { background-position: -580px -700px; }
-.emoji-popcorn { background-position: -600px -700px; }
-.emoji-post_office { background-position: -620px -700px; }
-.emoji-postal_horn { background-position: -640px -700px; }
-.emoji-postbox { background-position: -660px -700px; }
-.emoji-potable_water { background-position: -680px -700px; }
-.emoji-potato { background-position: -700px -700px; }
-.emoji-pouch { background-position: -720px 0; }
-.emoji-poultry_leg { background-position: -720px -20px; }
-.emoji-pound { background-position: -720px -40px; }
-.emoji-pouting_cat { background-position: -720px -60px; }
-.emoji-pray { background-position: -720px -80px; }
-.emoji-pray_tone1 { background-position: -720px -100px; }
-.emoji-pray_tone2 { background-position: -720px -120px; }
-.emoji-pray_tone3 { background-position: -720px -140px; }
-.emoji-pray_tone4 { background-position: -720px -160px; }
-.emoji-pray_tone5 { background-position: -720px -180px; }
-.emoji-prayer_beads { background-position: -720px -200px; }
-.emoji-pregnant_woman { background-position: -720px -220px; }
-.emoji-pregnant_woman_tone1 { background-position: -720px -240px; }
-.emoji-pregnant_woman_tone2 { background-position: -720px -260px; }
-.emoji-pregnant_woman_tone3 { background-position: -720px -280px; }
-.emoji-pregnant_woman_tone4 { background-position: -720px -300px; }
-.emoji-pregnant_woman_tone5 { background-position: -720px -320px; }
-.emoji-prince { background-position: -720px -340px; }
-.emoji-prince_tone1 { background-position: -720px -360px; }
-.emoji-prince_tone2 { background-position: -720px -380px; }
-.emoji-prince_tone3 { background-position: -720px -400px; }
-.emoji-prince_tone4 { background-position: -720px -420px; }
-.emoji-prince_tone5 { background-position: -720px -440px; }
-.emoji-princess { background-position: -720px -460px; }
-.emoji-princess_tone1 { background-position: -720px -480px; }
-.emoji-princess_tone2 { background-position: -720px -500px; }
-.emoji-princess_tone3 { background-position: -720px -520px; }
-.emoji-princess_tone4 { background-position: -720px -540px; }
-.emoji-princess_tone5 { background-position: -720px -560px; }
-.emoji-printer { background-position: -720px -580px; }
-.emoji-projector { background-position: -720px -600px; }
-.emoji-punch { background-position: -720px -620px; }
-.emoji-punch_tone1 { background-position: -720px -640px; }
-.emoji-punch_tone2 { background-position: -720px -660px; }
-.emoji-punch_tone3 { background-position: -720px -680px; }
-.emoji-punch_tone4 { background-position: -720px -700px; }
-.emoji-punch_tone5 { background-position: 0 -720px; }
-.emoji-purple_heart { background-position: -20px -720px; }
-.emoji-purse { background-position: -40px -720px; }
-.emoji-pushpin { background-position: -60px -720px; }
-.emoji-put_litter_in_its_place { background-position: -80px -720px; }
-.emoji-question { background-position: -100px -720px; }
-.emoji-rabbit { background-position: -120px -720px; }
-.emoji-rabbit2 { background-position: -140px -720px; }
-.emoji-race_car { background-position: -160px -720px; }
-.emoji-racehorse { background-position: -180px -720px; }
-.emoji-radio { background-position: -200px -720px; }
-.emoji-radio_button { background-position: -220px -720px; }
-.emoji-radioactive { background-position: -240px -720px; }
-.emoji-rage { background-position: -260px -720px; }
-.emoji-railway_car { background-position: -280px -720px; }
-.emoji-railway_track { background-position: -300px -720px; }
-.emoji-rainbow { background-position: -320px -720px; }
-.emoji-raised_back_of_hand { background-position: -340px -720px; }
-.emoji-raised_back_of_hand_tone1 { background-position: -360px -720px; }
-.emoji-raised_back_of_hand_tone2 { background-position: -380px -720px; }
-.emoji-raised_back_of_hand_tone3 { background-position: -400px -720px; }
-.emoji-raised_back_of_hand_tone4 { background-position: -420px -720px; }
-.emoji-raised_back_of_hand_tone5 { background-position: -440px -720px; }
-.emoji-raised_hand { background-position: -460px -720px; }
-.emoji-raised_hand_tone1 { background-position: -480px -720px; }
-.emoji-raised_hand_tone2 { background-position: -500px -720px; }
-.emoji-raised_hand_tone3 { background-position: -520px -720px; }
-.emoji-raised_hand_tone4 { background-position: -540px -720px; }
-.emoji-raised_hand_tone5 { background-position: -560px -720px; }
-.emoji-raised_hands { background-position: -580px -720px; }
-.emoji-raised_hands_tone1 { background-position: -600px -720px; }
-.emoji-raised_hands_tone2 { background-position: -620px -720px; }
-.emoji-raised_hands_tone3 { background-position: -640px -720px; }
-.emoji-raised_hands_tone4 { background-position: -660px -720px; }
-.emoji-raised_hands_tone5 { background-position: -680px -720px; }
-.emoji-raising_hand { background-position: -700px -720px; }
-.emoji-raising_hand_tone1 { background-position: -720px -720px; }
-.emoji-raising_hand_tone2 { background-position: -740px 0; }
-.emoji-raising_hand_tone3 { background-position: -740px -20px; }
-.emoji-raising_hand_tone4 { background-position: -740px -40px; }
-.emoji-raising_hand_tone5 { background-position: -740px -60px; }
-.emoji-ram { background-position: -740px -80px; }
-.emoji-ramen { background-position: -740px -100px; }
-.emoji-rat { background-position: -740px -120px; }
-.emoji-record_button { background-position: -740px -140px; }
-.emoji-recycle { background-position: -740px -160px; }
-.emoji-red_car { background-position: -740px -180px; }
-.emoji-red_circle { background-position: -740px -200px; }
-.emoji-registered { background-position: -740px -220px; }
-.emoji-relaxed { background-position: -740px -240px; }
-.emoji-relieved { background-position: -740px -260px; }
-.emoji-reminder_ribbon { background-position: -740px -280px; }
-.emoji-repeat { background-position: -740px -300px; }
-.emoji-repeat_one { background-position: -740px -320px; }
-.emoji-restroom { background-position: -740px -340px; }
-.emoji-revolving_hearts { background-position: -740px -360px; }
-.emoji-rewind { background-position: -740px -380px; }
-.emoji-rhino { background-position: -740px -400px; }
-.emoji-ribbon { background-position: -740px -420px; }
-.emoji-rice { background-position: -740px -440px; }
-.emoji-rice_ball { background-position: -740px -460px; }
-.emoji-rice_cracker { background-position: -740px -480px; }
-.emoji-rice_scene { background-position: -740px -500px; }
-.emoji-right_facing_fist { background-position: -740px -520px; }
-.emoji-right_facing_fist_tone1 { background-position: -740px -540px; }
-.emoji-right_facing_fist_tone2 { background-position: -740px -560px; }
-.emoji-right_facing_fist_tone3 { background-position: -740px -580px; }
-.emoji-right_facing_fist_tone4 { background-position: -740px -600px; }
-.emoji-right_facing_fist_tone5 { background-position: -740px -620px; }
-.emoji-ring { background-position: -740px -640px; }
-.emoji-robot { background-position: -740px -660px; }
-.emoji-rocket { background-position: -740px -680px; }
-.emoji-rofl { background-position: -740px -700px; }
-.emoji-roller_coaster { background-position: -740px -720px; }
-.emoji-rolling_eyes { background-position: 0 -740px; }
-.emoji-rooster { background-position: -20px -740px; }
-.emoji-rose { background-position: -40px -740px; }
-.emoji-rosette { background-position: -60px -740px; }
-.emoji-rotating_light { background-position: -80px -740px; }
-.emoji-round_pushpin { background-position: -100px -740px; }
-.emoji-rowboat { background-position: -120px -740px; }
-.emoji-rowboat_tone1 { background-position: -140px -740px; }
-.emoji-rowboat_tone2 { background-position: -160px -740px; }
-.emoji-rowboat_tone3 { background-position: -180px -740px; }
-.emoji-rowboat_tone4 { background-position: -200px -740px; }
-.emoji-rowboat_tone5 { background-position: -220px -740px; }
-.emoji-rugby_football { background-position: -240px -740px; }
-.emoji-runner { background-position: -260px -740px; }
-.emoji-runner_tone1 { background-position: -280px -740px; }
-.emoji-runner_tone2 { background-position: -300px -740px; }
-.emoji-runner_tone3 { background-position: -320px -740px; }
-.emoji-runner_tone4 { background-position: -340px -740px; }
-.emoji-runner_tone5 { background-position: -360px -740px; }
-.emoji-running_shirt_with_sash { background-position: -380px -740px; }
-.emoji-sa { background-position: -400px -740px; }
-.emoji-sagittarius { background-position: -420px -740px; }
-.emoji-sailboat { background-position: -440px -740px; }
-.emoji-sake { background-position: -460px -740px; }
-.emoji-salad { background-position: -480px -740px; }
-.emoji-sandal { background-position: -500px -740px; }
-.emoji-santa { background-position: -520px -740px; }
-.emoji-santa_tone1 { background-position: -540px -740px; }
-.emoji-santa_tone2 { background-position: -560px -740px; }
-.emoji-santa_tone3 { background-position: -580px -740px; }
-.emoji-santa_tone4 { background-position: -600px -740px; }
-.emoji-santa_tone5 { background-position: -620px -740px; }
-.emoji-satellite { background-position: -640px -740px; }
-.emoji-satellite_orbital { background-position: -660px -740px; }
-.emoji-saxophone { background-position: -680px -740px; }
-.emoji-scales { background-position: -700px -740px; }
-.emoji-school { background-position: -720px -740px; }
-.emoji-school_satchel { background-position: -740px -740px; }
-.emoji-scissors { background-position: -760px 0; }
-.emoji-scooter { background-position: -760px -20px; }
-.emoji-scorpion { background-position: -760px -40px; }
-.emoji-scorpius { background-position: -760px -60px; }
-.emoji-scream { background-position: -760px -80px; }
-.emoji-scream_cat { background-position: -760px -100px; }
-.emoji-scroll { background-position: -760px -120px; }
-.emoji-seat { background-position: -760px -140px; }
-.emoji-second_place { background-position: -760px -160px; }
-.emoji-secret { background-position: -760px -180px; }
-.emoji-see_no_evil { background-position: -760px -200px; }
-.emoji-seedling { background-position: -760px -220px; }
-.emoji-selfie { background-position: -760px -240px; }
-.emoji-selfie_tone1 { background-position: -760px -260px; }
-.emoji-selfie_tone2 { background-position: -760px -280px; }
-.emoji-selfie_tone3 { background-position: -760px -300px; }
-.emoji-selfie_tone4 { background-position: -760px -320px; }
-.emoji-selfie_tone5 { background-position: -760px -340px; }
-.emoji-seven { background-position: -760px -360px; }
-.emoji-shallow_pan_of_food { background-position: -760px -380px; }
-.emoji-shamrock { background-position: -760px -400px; }
-.emoji-shark { background-position: -760px -420px; }
-.emoji-shaved_ice { background-position: -760px -440px; }
-.emoji-sheep { background-position: -760px -460px; }
-.emoji-shell { background-position: -760px -480px; }
-.emoji-shield { background-position: -760px -500px; }
-.emoji-shinto_shrine { background-position: -760px -520px; }
-.emoji-ship { background-position: -760px -540px; }
-.emoji-shirt { background-position: -760px -560px; }
-.emoji-shopping_bags { background-position: -760px -580px; }
-.emoji-shopping_cart { background-position: -760px -600px; }
-.emoji-shower { background-position: -760px -620px; }
-.emoji-shrimp { background-position: -760px -640px; }
-.emoji-shrug { background-position: -760px -660px; }
-.emoji-shrug_tone1 { background-position: -760px -680px; }
-.emoji-shrug_tone2 { background-position: -760px -700px; }
-.emoji-shrug_tone3 { background-position: -760px -720px; }
-.emoji-shrug_tone4 { background-position: -760px -740px; }
-.emoji-shrug_tone5 { background-position: 0 -760px; }
-.emoji-signal_strength { background-position: -20px -760px; }
-.emoji-six { background-position: -40px -760px; }
-.emoji-six_pointed_star { background-position: -60px -760px; }
-.emoji-ski { background-position: -80px -760px; }
-.emoji-skier { background-position: -100px -760px; }
-.emoji-skull { background-position: -120px -760px; }
-.emoji-skull_crossbones { background-position: -140px -760px; }
-.emoji-sleeping { background-position: -160px -760px; }
-.emoji-sleeping_accommodation { background-position: -180px -760px; }
-.emoji-sleepy { background-position: -200px -760px; }
-.emoji-slight_frown { background-position: -220px -760px; }
-.emoji-slight_smile { background-position: -240px -760px; }
-.emoji-slot_machine { background-position: -260px -760px; }
-.emoji-small_blue_diamond { background-position: -280px -760px; }
-.emoji-small_orange_diamond { background-position: -300px -760px; }
-.emoji-small_red_triangle { background-position: -320px -760px; }
-.emoji-small_red_triangle_down { background-position: -340px -760px; }
-.emoji-smile { background-position: -360px -760px; }
-.emoji-smile_cat { background-position: -380px -760px; }
-.emoji-smiley { background-position: -400px -760px; }
-.emoji-smiley_cat { background-position: -420px -760px; }
-.emoji-smiling_imp { background-position: -440px -760px; }
-.emoji-smirk { background-position: -460px -760px; }
-.emoji-smirk_cat { background-position: -480px -760px; }
-.emoji-smoking { background-position: -500px -760px; }
-.emoji-snail { background-position: -520px -760px; }
-.emoji-snake { background-position: -540px -760px; }
-.emoji-sneezing_face { background-position: -560px -760px; }
-.emoji-snowboarder { background-position: -580px -760px; }
-.emoji-snowflake { background-position: -600px -760px; }
-.emoji-snowman { background-position: -620px -760px; }
-.emoji-snowman2 { background-position: -640px -760px; }
-.emoji-sob { background-position: -660px -760px; }
-.emoji-soccer { background-position: -680px -760px; }
-.emoji-soon { background-position: -700px -760px; }
-.emoji-sos { background-position: -720px -760px; }
-.emoji-sound { background-position: -740px -760px; }
-.emoji-space_invader { background-position: -760px -760px; }
-.emoji-spades { background-position: -780px 0; }
-.emoji-spaghetti { background-position: -780px -20px; }
-.emoji-sparkle { background-position: -780px -40px; }
-.emoji-sparkler { background-position: -780px -60px; }
-.emoji-sparkles { background-position: -780px -80px; }
-.emoji-sparkling_heart { background-position: -780px -100px; }
-.emoji-speak_no_evil { background-position: -780px -120px; }
-.emoji-speaker { background-position: -780px -140px; }
-.emoji-speaking_head { background-position: -780px -160px; }
-.emoji-speech_balloon { background-position: -780px -180px; }
-.emoji-speedboat { background-position: -780px -200px; }
-.emoji-spider { background-position: -780px -220px; }
-.emoji-spider_web { background-position: -780px -240px; }
-.emoji-spoon { background-position: -780px -260px; }
-.emoji-spy { background-position: -780px -280px; }
-.emoji-spy_tone1 { background-position: -780px -300px; }
-.emoji-spy_tone2 { background-position: -780px -320px; }
-.emoji-spy_tone3 { background-position: -780px -340px; }
-.emoji-spy_tone4 { background-position: -780px -360px; }
-.emoji-spy_tone5 { background-position: -780px -380px; }
-.emoji-squid { background-position: -780px -400px; }
-.emoji-stadium { background-position: -780px -420px; }
-.emoji-star { background-position: -780px -440px; }
-.emoji-star2 { background-position: -780px -460px; }
-.emoji-star_and_crescent { background-position: -780px -480px; }
-.emoji-star_of_david { background-position: -780px -500px; }
-.emoji-stars { background-position: -780px -520px; }
-.emoji-station { background-position: -780px -540px; }
-.emoji-statue_of_liberty { background-position: -780px -560px; }
-.emoji-steam_locomotive { background-position: -780px -580px; }
-.emoji-stew { background-position: -780px -600px; }
-.emoji-stop_button { background-position: -780px -620px; }
-.emoji-stopwatch { background-position: -780px -640px; }
-.emoji-straight_ruler { background-position: -780px -660px; }
-.emoji-strawberry { background-position: -780px -680px; }
-.emoji-stuck_out_tongue { background-position: -780px -700px; }
-.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -720px; }
-.emoji-stuck_out_tongue_winking_eye { background-position: -780px -740px; }
-.emoji-stuffed_flatbread { background-position: -780px -760px; }
-.emoji-sun_with_face { background-position: 0 -780px; }
-.emoji-sunflower { background-position: -20px -780px; }
-.emoji-sunglasses { background-position: -40px -780px; }
-.emoji-sunny { background-position: -60px -780px; }
-.emoji-sunrise { background-position: -80px -780px; }
-.emoji-sunrise_over_mountains { background-position: -100px -780px; }
-.emoji-surfer { background-position: -120px -780px; }
-.emoji-surfer_tone1 { background-position: -140px -780px; }
-.emoji-surfer_tone2 { background-position: -160px -780px; }
-.emoji-surfer_tone3 { background-position: -180px -780px; }
-.emoji-surfer_tone4 { background-position: -200px -780px; }
-.emoji-surfer_tone5 { background-position: -220px -780px; }
-.emoji-sushi { background-position: -240px -780px; }
-.emoji-suspension_railway { background-position: -260px -780px; }
-.emoji-sweat { background-position: -280px -780px; }
-.emoji-sweat_drops { background-position: -300px -780px; }
-.emoji-sweat_smile { background-position: -320px -780px; }
-.emoji-sweet_potato { background-position: -340px -780px; }
-.emoji-swimmer { background-position: -360px -780px; }
-.emoji-swimmer_tone1 { background-position: -380px -780px; }
-.emoji-swimmer_tone2 { background-position: -400px -780px; }
-.emoji-swimmer_tone3 { background-position: -420px -780px; }
-.emoji-swimmer_tone4 { background-position: -440px -780px; }
-.emoji-swimmer_tone5 { background-position: -460px -780px; }
-.emoji-symbols { background-position: -480px -780px; }
-.emoji-synagogue { background-position: -500px -780px; }
-.emoji-syringe { background-position: -520px -780px; }
-.emoji-taco { background-position: -540px -780px; }
-.emoji-tada { background-position: -560px -780px; }
-.emoji-tanabata_tree { background-position: -580px -780px; }
-.emoji-tangerine { background-position: -600px -780px; }
-.emoji-taurus { background-position: -620px -780px; }
-.emoji-taxi { background-position: -640px -780px; }
-.emoji-tea { background-position: -660px -780px; }
-.emoji-telephone { background-position: -680px -780px; }
-.emoji-telephone_receiver { background-position: -700px -780px; }
-.emoji-telescope { background-position: -720px -780px; }
-.emoji-ten { background-position: -740px -780px; }
-.emoji-tennis { background-position: -760px -780px; }
-.emoji-tent { background-position: -780px -780px; }
-.emoji-thermometer { background-position: -800px 0; }
-.emoji-thermometer_face { background-position: -800px -20px; }
-.emoji-thinking { background-position: -800px -40px; }
-.emoji-third_place { background-position: -800px -60px; }
-.emoji-thought_balloon { background-position: -800px -80px; }
-.emoji-three { background-position: -800px -100px; }
-.emoji-thumbsdown { background-position: -800px -120px; }
-.emoji-thumbsdown_tone1 { background-position: -800px -140px; }
-.emoji-thumbsdown_tone2 { background-position: -800px -160px; }
-.emoji-thumbsdown_tone3 { background-position: -800px -180px; }
-.emoji-thumbsdown_tone4 { background-position: -800px -200px; }
-.emoji-thumbsdown_tone5 { background-position: -800px -220px; }
-.emoji-thumbsup { background-position: -800px -240px; }
-.emoji-thumbsup_tone1 { background-position: -800px -260px; }
-.emoji-thumbsup_tone2 { background-position: -800px -280px; }
-.emoji-thumbsup_tone3 { background-position: -800px -300px; }
-.emoji-thumbsup_tone4 { background-position: -800px -320px; }
-.emoji-thumbsup_tone5 { background-position: -800px -340px; }
-.emoji-thunder_cloud_rain { background-position: -800px -360px; }
-.emoji-ticket { background-position: -800px -380px; }
-.emoji-tickets { background-position: -800px -400px; }
-.emoji-tiger { background-position: -800px -420px; }
-.emoji-tiger2 { background-position: -800px -440px; }
-.emoji-timer { background-position: -800px -460px; }
-.emoji-tired_face { background-position: -800px -480px; }
-.emoji-tm { background-position: -800px -500px; }
-.emoji-toilet { background-position: -800px -520px; }
-.emoji-tokyo_tower { background-position: -800px -540px; }
-.emoji-tomato { background-position: -800px -560px; }
-.emoji-tone1 { background-position: -800px -580px; }
-.emoji-tone2 { background-position: -800px -600px; }
-.emoji-tone3 { background-position: -800px -620px; }
-.emoji-tone4 { background-position: -800px -640px; }
-.emoji-tone5 { background-position: -800px -660px; }
-.emoji-tongue { background-position: -800px -680px; }
-.emoji-tools { background-position: -800px -700px; }
-.emoji-top { background-position: -800px -720px; }
-.emoji-tophat { background-position: -800px -740px; }
-.emoji-track_next { background-position: -800px -760px; }
-.emoji-track_previous { background-position: -800px -780px; }
-.emoji-trackball { background-position: 0 -800px; }
-.emoji-tractor { background-position: -20px -800px; }
-.emoji-traffic_light { background-position: -40px -800px; }
-.emoji-train { background-position: -60px -800px; }
-.emoji-train2 { background-position: -80px -800px; }
-.emoji-tram { background-position: -100px -800px; }
-.emoji-triangular_flag_on_post { background-position: -120px -800px; }
-.emoji-triangular_ruler { background-position: -140px -800px; }
-.emoji-trident { background-position: -160px -800px; }
-.emoji-triumph { background-position: -180px -800px; }
-.emoji-trolleybus { background-position: -200px -800px; }
-.emoji-trophy { background-position: -220px -800px; }
-.emoji-tropical_drink { background-position: -240px -800px; }
-.emoji-tropical_fish { background-position: -260px -800px; }
-.emoji-truck { background-position: -280px -800px; }
-.emoji-trumpet { background-position: -300px -800px; }
-.emoji-tulip { background-position: -320px -800px; }
-.emoji-tumbler_glass { background-position: -340px -800px; }
-.emoji-turkey { background-position: -360px -800px; }
-.emoji-turtle { background-position: -380px -800px; }
-.emoji-tv { background-position: -400px -800px; }
-.emoji-twisted_rightwards_arrows { background-position: -420px -800px; }
-.emoji-two { background-position: -440px -800px; }
-.emoji-two_hearts { background-position: -460px -800px; }
-.emoji-two_men_holding_hands { background-position: -480px -800px; }
-.emoji-two_women_holding_hands { background-position: -500px -800px; }
-.emoji-u5272 { background-position: -520px -800px; }
-.emoji-u5408 { background-position: -540px -800px; }
-.emoji-u55b6 { background-position: -560px -800px; }
-.emoji-u6307 { background-position: -580px -800px; }
-.emoji-u6708 { background-position: -600px -800px; }
-.emoji-u6709 { background-position: -620px -800px; }
-.emoji-u6e80 { background-position: -640px -800px; }
-.emoji-u7121 { background-position: -660px -800px; }
-.emoji-u7533 { background-position: -680px -800px; }
-.emoji-u7981 { background-position: -700px -800px; }
-.emoji-u7a7a { background-position: -720px -800px; }
-.emoji-umbrella { background-position: -740px -800px; }
-.emoji-umbrella2 { background-position: -760px -800px; }
-.emoji-unamused { background-position: -780px -800px; }
-.emoji-underage { background-position: -800px -800px; }
-.emoji-unicorn { background-position: -820px 0; }
-.emoji-unlock { background-position: -820px -20px; }
-.emoji-up { background-position: -820px -40px; }
-.emoji-upside_down { background-position: -820px -60px; }
-.emoji-urn { background-position: -820px -80px; }
-.emoji-v { background-position: -820px -100px; }
-.emoji-v_tone1 { background-position: -820px -120px; }
-.emoji-v_tone2 { background-position: -820px -140px; }
-.emoji-v_tone3 { background-position: -820px -160px; }
-.emoji-v_tone4 { background-position: -820px -180px; }
-.emoji-v_tone5 { background-position: -820px -200px; }
-.emoji-vertical_traffic_light { background-position: -820px -220px; }
-.emoji-vhs { background-position: -820px -240px; }
-.emoji-vibration_mode { background-position: -820px -260px; }
-.emoji-video_camera { background-position: -820px -280px; }
-.emoji-video_game { background-position: -820px -300px; }
-.emoji-violin { background-position: -820px -320px; }
-.emoji-virgo { background-position: -820px -340px; }
-.emoji-volcano { background-position: -820px -360px; }
-.emoji-volleyball { background-position: -820px -380px; }
-.emoji-vs { background-position: -820px -400px; }
-.emoji-vulcan { background-position: -820px -420px; }
-.emoji-vulcan_tone1 { background-position: -820px -440px; }
-.emoji-vulcan_tone2 { background-position: -820px -460px; }
-.emoji-vulcan_tone3 { background-position: -820px -480px; }
-.emoji-vulcan_tone4 { background-position: -820px -500px; }
-.emoji-vulcan_tone5 { background-position: -820px -520px; }
-.emoji-walking { background-position: -820px -540px; }
-.emoji-walking_tone1 { background-position: -820px -560px; }
-.emoji-walking_tone2 { background-position: -820px -580px; }
-.emoji-walking_tone3 { background-position: -820px -600px; }
-.emoji-walking_tone4 { background-position: -820px -620px; }
-.emoji-walking_tone5 { background-position: -820px -640px; }
-.emoji-waning_crescent_moon { background-position: -820px -660px; }
-.emoji-waning_gibbous_moon { background-position: -820px -680px; }
-.emoji-warning { background-position: -820px -700px; }
-.emoji-wastebasket { background-position: -820px -720px; }
-.emoji-watch { background-position: -820px -740px; }
-.emoji-water_buffalo { background-position: -820px -760px; }
-.emoji-water_polo { background-position: -820px -780px; }
-.emoji-water_polo_tone1 { background-position: -820px -800px; }
-.emoji-water_polo_tone2 { background-position: 0 -820px; }
-.emoji-water_polo_tone3 { background-position: -20px -820px; }
-.emoji-water_polo_tone4 { background-position: -40px -820px; }
-.emoji-water_polo_tone5 { background-position: -60px -820px; }
-.emoji-watermelon { background-position: -80px -820px; }
-.emoji-wave { background-position: -100px -820px; }
-.emoji-wave_tone1 { background-position: -120px -820px; }
-.emoji-wave_tone2 { background-position: -140px -820px; }
-.emoji-wave_tone3 { background-position: -160px -820px; }
-.emoji-wave_tone4 { background-position: -180px -820px; }
-.emoji-wave_tone5 { background-position: -200px -820px; }
-.emoji-wavy_dash { background-position: -220px -820px; }
-.emoji-waxing_crescent_moon { background-position: -240px -820px; }
-.emoji-waxing_gibbous_moon { background-position: -260px -820px; }
-.emoji-wc { background-position: -280px -820px; }
-.emoji-weary { background-position: -300px -820px; }
-.emoji-wedding { background-position: -320px -820px; }
-.emoji-whale { background-position: -340px -820px; }
-.emoji-whale2 { background-position: -360px -820px; }
-.emoji-wheel_of_dharma { background-position: -380px -820px; }
-.emoji-wheelchair { background-position: -400px -820px; }
-.emoji-white_check_mark { background-position: -420px -820px; }
-.emoji-white_circle { background-position: -440px -820px; }
-.emoji-white_flower { background-position: -460px -820px; }
-.emoji-white_large_square { background-position: -480px -820px; }
-.emoji-white_medium_small_square { background-position: -500px -820px; }
-.emoji-white_medium_square { background-position: -520px -820px; }
-.emoji-white_small_square { background-position: -540px -820px; }
-.emoji-white_square_button { background-position: -560px -820px; }
-.emoji-white_sun_cloud { background-position: -580px -820px; }
-.emoji-white_sun_rain_cloud { background-position: -600px -820px; }
-.emoji-white_sun_small_cloud { background-position: -620px -820px; }
-.emoji-wilted_rose { background-position: -640px -820px; }
-.emoji-wind_blowing_face { background-position: -660px -820px; }
-.emoji-wind_chime { background-position: -680px -820px; }
-.emoji-wine_glass { background-position: -700px -820px; }
-.emoji-wink { background-position: -720px -820px; }
-.emoji-wolf { background-position: -740px -820px; }
-.emoji-woman { background-position: -760px -820px; }
-.emoji-woman_tone1 { background-position: -780px -820px; }
-.emoji-woman_tone2 { background-position: -800px -820px; }
-.emoji-woman_tone3 { background-position: -820px -820px; }
-.emoji-woman_tone4 { background-position: -840px 0; }
-.emoji-woman_tone5 { background-position: -840px -20px; }
-.emoji-womans_clothes { background-position: -840px -40px; }
-.emoji-womans_hat { background-position: -840px -60px; }
-.emoji-womens { background-position: -840px -80px; }
-.emoji-worried { background-position: -840px -100px; }
-.emoji-wrench { background-position: -840px -120px; }
-.emoji-wrestlers { background-position: -840px -140px; }
-.emoji-wrestlers_tone1 { background-position: -840px -160px; }
-.emoji-wrestlers_tone2 { background-position: -840px -180px; }
-.emoji-wrestlers_tone3 { background-position: -840px -200px; }
-.emoji-wrestlers_tone4 { background-position: -840px -220px; }
-.emoji-wrestlers_tone5 { background-position: -840px -240px; }
-.emoji-writing_hand { background-position: -840px -260px; }
-.emoji-writing_hand_tone1 { background-position: -840px -280px; }
-.emoji-writing_hand_tone2 { background-position: -840px -300px; }
-.emoji-writing_hand_tone3 { background-position: -840px -320px; }
-.emoji-writing_hand_tone4 { background-position: -840px -340px; }
-.emoji-writing_hand_tone5 { background-position: -840px -360px; }
-.emoji-x { background-position: -840px -380px; }
-.emoji-yellow_heart { background-position: -840px -400px; }
-.emoji-yen { background-position: -840px -420px; }
-.emoji-yin_yang { background-position: -840px -440px; }
-.emoji-yum { background-position: -840px -460px; }
-.emoji-zap { background-position: -840px -480px; }
-.emoji-zero { background-position: -840px -500px; }
-.emoji-zipper_mouth { background-position: -840px -520px; }
-.emoji-100 { background-position: -840px -540px; }
+.emoji-gay_pride_flag { background-position: -220px -540px; }
+.emoji-gear { background-position: -240px -540px; }
+.emoji-gem { background-position: -260px -540px; }
+.emoji-gemini { background-position: -280px -540px; }
+.emoji-ghost { background-position: -300px -540px; }
+.emoji-gift { background-position: -320px -540px; }
+.emoji-gift_heart { background-position: -340px -540px; }
+.emoji-girl { background-position: -360px -540px; }
+.emoji-girl_tone1 { background-position: -380px -540px; }
+.emoji-girl_tone2 { background-position: -400px -540px; }
+.emoji-girl_tone3 { background-position: -420px -540px; }
+.emoji-girl_tone4 { background-position: -440px -540px; }
+.emoji-girl_tone5 { background-position: -460px -540px; }
+.emoji-globe_with_meridians { background-position: -480px -540px; }
+.emoji-goal { background-position: -500px -540px; }
+.emoji-goat { background-position: -520px -540px; }
+.emoji-golf { background-position: -540px -540px; }
+.emoji-golfer { background-position: -560px 0; }
+.emoji-gorilla { background-position: -560px -20px; }
+.emoji-grapes { background-position: -560px -40px; }
+.emoji-green_apple { background-position: -560px -60px; }
+.emoji-green_book { background-position: -560px -80px; }
+.emoji-green_heart { background-position: -560px -100px; }
+.emoji-grey_exclamation { background-position: -560px -120px; }
+.emoji-grey_question { background-position: -560px -140px; }
+.emoji-grimacing { background-position: -560px -160px; }
+.emoji-grin { background-position: -560px -180px; }
+.emoji-grinning { background-position: -560px -200px; }
+.emoji-guardsman { background-position: -560px -220px; }
+.emoji-guardsman_tone1 { background-position: -560px -240px; }
+.emoji-guardsman_tone2 { background-position: -560px -260px; }
+.emoji-guardsman_tone3 { background-position: -560px -280px; }
+.emoji-guardsman_tone4 { background-position: -560px -300px; }
+.emoji-guardsman_tone5 { background-position: -560px -320px; }
+.emoji-guitar { background-position: -560px -340px; }
+.emoji-gun { background-position: -560px -360px; }
+.emoji-haircut { background-position: -560px -380px; }
+.emoji-haircut_tone1 { background-position: -560px -400px; }
+.emoji-haircut_tone2 { background-position: -560px -420px; }
+.emoji-haircut_tone3 { background-position: -560px -440px; }
+.emoji-haircut_tone4 { background-position: -560px -460px; }
+.emoji-haircut_tone5 { background-position: -560px -480px; }
+.emoji-hamburger { background-position: -560px -500px; }
+.emoji-hammer { background-position: -560px -520px; }
+.emoji-hammer_pick { background-position: -560px -540px; }
+.emoji-hamster { background-position: 0 -560px; }
+.emoji-hand_splayed { background-position: -20px -560px; }
+.emoji-hand_splayed_tone1 { background-position: -40px -560px; }
+.emoji-hand_splayed_tone2 { background-position: -60px -560px; }
+.emoji-hand_splayed_tone3 { background-position: -80px -560px; }
+.emoji-hand_splayed_tone4 { background-position: -100px -560px; }
+.emoji-hand_splayed_tone5 { background-position: -120px -560px; }
+.emoji-handbag { background-position: -140px -560px; }
+.emoji-handball { background-position: -160px -560px; }
+.emoji-handball_tone1 { background-position: -180px -560px; }
+.emoji-handball_tone2 { background-position: -200px -560px; }
+.emoji-handball_tone3 { background-position: -220px -560px; }
+.emoji-handball_tone4 { background-position: -240px -560px; }
+.emoji-handball_tone5 { background-position: -260px -560px; }
+.emoji-handshake { background-position: -280px -560px; }
+.emoji-handshake_tone1 { background-position: -300px -560px; }
+.emoji-handshake_tone2 { background-position: -320px -560px; }
+.emoji-handshake_tone3 { background-position: -340px -560px; }
+.emoji-handshake_tone4 { background-position: -360px -560px; }
+.emoji-handshake_tone5 { background-position: -380px -560px; }
+.emoji-hash { background-position: -400px -560px; }
+.emoji-hatched_chick { background-position: -420px -560px; }
+.emoji-hatching_chick { background-position: -440px -560px; }
+.emoji-head_bandage { background-position: -460px -560px; }
+.emoji-headphones { background-position: -480px -560px; }
+.emoji-hear_no_evil { background-position: -500px -560px; }
+.emoji-heart { background-position: -520px -560px; }
+.emoji-heart_decoration { background-position: -540px -560px; }
+.emoji-heart_exclamation { background-position: -560px -560px; }
+.emoji-heart_eyes { background-position: -580px 0; }
+.emoji-heart_eyes_cat { background-position: -580px -20px; }
+.emoji-heartbeat { background-position: -580px -40px; }
+.emoji-heartpulse { background-position: -580px -60px; }
+.emoji-hearts { background-position: -580px -80px; }
+.emoji-heavy_check_mark { background-position: -580px -100px; }
+.emoji-heavy_division_sign { background-position: -580px -120px; }
+.emoji-heavy_dollar_sign { background-position: -580px -140px; }
+.emoji-heavy_minus_sign { background-position: -580px -160px; }
+.emoji-heavy_multiplication_x { background-position: -580px -180px; }
+.emoji-heavy_plus_sign { background-position: -580px -200px; }
+.emoji-helicopter { background-position: -580px -220px; }
+.emoji-helmet_with_cross { background-position: -580px -240px; }
+.emoji-herb { background-position: -580px -260px; }
+.emoji-hibiscus { background-position: -580px -280px; }
+.emoji-high_brightness { background-position: -580px -300px; }
+.emoji-high_heel { background-position: -580px -320px; }
+.emoji-hockey { background-position: -580px -340px; }
+.emoji-hole { background-position: -580px -360px; }
+.emoji-homes { background-position: -580px -380px; }
+.emoji-honey_pot { background-position: -580px -400px; }
+.emoji-horse { background-position: -580px -420px; }
+.emoji-horse_racing { background-position: -580px -440px; }
+.emoji-horse_racing_tone1 { background-position: -580px -460px; }
+.emoji-horse_racing_tone2 { background-position: -580px -480px; }
+.emoji-horse_racing_tone3 { background-position: -580px -500px; }
+.emoji-horse_racing_tone4 { background-position: -580px -520px; }
+.emoji-horse_racing_tone5 { background-position: -580px -540px; }
+.emoji-hospital { background-position: -580px -560px; }
+.emoji-hot_pepper { background-position: 0 -580px; }
+.emoji-hotdog { background-position: -20px -580px; }
+.emoji-hotel { background-position: -40px -580px; }
+.emoji-hotsprings { background-position: -60px -580px; }
+.emoji-hourglass { background-position: -80px -580px; }
+.emoji-hourglass_flowing_sand { background-position: -100px -580px; }
+.emoji-house { background-position: -120px -580px; }
+.emoji-house_abandoned { background-position: -140px -580px; }
+.emoji-house_with_garden { background-position: -160px -580px; }
+.emoji-hugging { background-position: -180px -580px; }
+.emoji-hushed { background-position: -200px -580px; }
+.emoji-ice_cream { background-position: -220px -580px; }
+.emoji-ice_skate { background-position: -240px -580px; }
+.emoji-icecream { background-position: -260px -580px; }
+.emoji-id { background-position: -280px -580px; }
+.emoji-ideograph_advantage { background-position: -300px -580px; }
+.emoji-imp { background-position: -320px -580px; }
+.emoji-inbox_tray { background-position: -340px -580px; }
+.emoji-incoming_envelope { background-position: -360px -580px; }
+.emoji-information_desk_person { background-position: -380px -580px; }
+.emoji-information_desk_person_tone1 { background-position: -400px -580px; }
+.emoji-information_desk_person_tone2 { background-position: -420px -580px; }
+.emoji-information_desk_person_tone3 { background-position: -440px -580px; }
+.emoji-information_desk_person_tone4 { background-position: -460px -580px; }
+.emoji-information_desk_person_tone5 { background-position: -480px -580px; }
+.emoji-information_source { background-position: -500px -580px; }
+.emoji-innocent { background-position: -520px -580px; }
+.emoji-interrobang { background-position: -540px -580px; }
+.emoji-iphone { background-position: -560px -580px; }
+.emoji-island { background-position: -580px -580px; }
+.emoji-izakaya_lantern { background-position: -600px 0; }
+.emoji-jack_o_lantern { background-position: -600px -20px; }
+.emoji-japan { background-position: -600px -40px; }
+.emoji-japanese_castle { background-position: -600px -60px; }
+.emoji-japanese_goblin { background-position: -600px -80px; }
+.emoji-japanese_ogre { background-position: -600px -100px; }
+.emoji-jeans { background-position: -600px -120px; }
+.emoji-joy { background-position: -600px -140px; }
+.emoji-joy_cat { background-position: -600px -160px; }
+.emoji-joystick { background-position: -600px -180px; }
+.emoji-juggling { background-position: -600px -200px; }
+.emoji-juggling_tone1 { background-position: -600px -220px; }
+.emoji-juggling_tone2 { background-position: -600px -240px; }
+.emoji-juggling_tone3 { background-position: -600px -260px; }
+.emoji-juggling_tone4 { background-position: -600px -280px; }
+.emoji-juggling_tone5 { background-position: -600px -300px; }
+.emoji-kaaba { background-position: -600px -320px; }
+.emoji-key { background-position: -600px -340px; }
+.emoji-key2 { background-position: -600px -360px; }
+.emoji-keyboard { background-position: -600px -380px; }
+.emoji-kimono { background-position: -600px -400px; }
+.emoji-kiss { background-position: -600px -420px; }
+.emoji-kiss_mm { background-position: -600px -440px; }
+.emoji-kiss_ww { background-position: -600px -460px; }
+.emoji-kissing { background-position: -600px -480px; }
+.emoji-kissing_cat { background-position: -600px -500px; }
+.emoji-kissing_closed_eyes { background-position: -600px -520px; }
+.emoji-kissing_heart { background-position: -600px -540px; }
+.emoji-kissing_smiling_eyes { background-position: -600px -560px; }
+.emoji-kiwi { background-position: -600px -580px; }
+.emoji-knife { background-position: 0 -600px; }
+.emoji-koala { background-position: -20px -600px; }
+.emoji-koko { background-position: -40px -600px; }
+.emoji-label { background-position: -60px -600px; }
+.emoji-large_blue_circle { background-position: -80px -600px; }
+.emoji-large_blue_diamond { background-position: -100px -600px; }
+.emoji-large_orange_diamond { background-position: -120px -600px; }
+.emoji-last_quarter_moon { background-position: -140px -600px; }
+.emoji-last_quarter_moon_with_face { background-position: -160px -600px; }
+.emoji-laughing { background-position: -180px -600px; }
+.emoji-leaves { background-position: -200px -600px; }
+.emoji-ledger { background-position: -220px -600px; }
+.emoji-left_facing_fist { background-position: -240px -600px; }
+.emoji-left_facing_fist_tone1 { background-position: -260px -600px; }
+.emoji-left_facing_fist_tone2 { background-position: -280px -600px; }
+.emoji-left_facing_fist_tone3 { background-position: -300px -600px; }
+.emoji-left_facing_fist_tone4 { background-position: -320px -600px; }
+.emoji-left_facing_fist_tone5 { background-position: -340px -600px; }
+.emoji-left_luggage { background-position: -360px -600px; }
+.emoji-left_right_arrow { background-position: -380px -600px; }
+.emoji-leftwards_arrow_with_hook { background-position: -400px -600px; }
+.emoji-lemon { background-position: -420px -600px; }
+.emoji-leo { background-position: -440px -600px; }
+.emoji-leopard { background-position: -460px -600px; }
+.emoji-level_slider { background-position: -480px -600px; }
+.emoji-levitate { background-position: -500px -600px; }
+.emoji-libra { background-position: -520px -600px; }
+.emoji-lifter { background-position: -540px -600px; }
+.emoji-lifter_tone1 { background-position: -560px -600px; }
+.emoji-lifter_tone2 { background-position: -580px -600px; }
+.emoji-lifter_tone3 { background-position: -600px -600px; }
+.emoji-lifter_tone4 { background-position: -620px 0; }
+.emoji-lifter_tone5 { background-position: -620px -20px; }
+.emoji-light_rail { background-position: -620px -40px; }
+.emoji-link { background-position: -620px -60px; }
+.emoji-lion_face { background-position: -620px -80px; }
+.emoji-lips { background-position: -620px -100px; }
+.emoji-lipstick { background-position: -620px -120px; }
+.emoji-lizard { background-position: -620px -140px; }
+.emoji-lock { background-position: -620px -160px; }
+.emoji-lock_with_ink_pen { background-position: -620px -180px; }
+.emoji-lollipop { background-position: -620px -200px; }
+.emoji-loop { background-position: -620px -220px; }
+.emoji-loud_sound { background-position: -620px -240px; }
+.emoji-loudspeaker { background-position: -620px -260px; }
+.emoji-love_hotel { background-position: -620px -280px; }
+.emoji-love_letter { background-position: -620px -300px; }
+.emoji-low_brightness { background-position: -620px -320px; }
+.emoji-lying_face { background-position: -620px -340px; }
+.emoji-m { background-position: -620px -360px; }
+.emoji-mag { background-position: -620px -380px; }
+.emoji-mag_right { background-position: -620px -400px; }
+.emoji-mahjong { background-position: -620px -420px; }
+.emoji-mailbox { background-position: -620px -440px; }
+.emoji-mailbox_closed { background-position: -620px -460px; }
+.emoji-mailbox_with_mail { background-position: -620px -480px; }
+.emoji-mailbox_with_no_mail { background-position: -620px -500px; }
+.emoji-man { background-position: -620px -520px; }
+.emoji-man_dancing { background-position: -620px -540px; }
+.emoji-man_dancing_tone1 { background-position: -620px -560px; }
+.emoji-man_dancing_tone2 { background-position: -620px -580px; }
+.emoji-man_dancing_tone3 { background-position: -620px -600px; }
+.emoji-man_dancing_tone4 { background-position: 0 -620px; }
+.emoji-man_dancing_tone5 { background-position: -20px -620px; }
+.emoji-man_in_tuxedo { background-position: -40px -620px; }
+.emoji-man_in_tuxedo_tone1 { background-position: -60px -620px; }
+.emoji-man_in_tuxedo_tone2 { background-position: -80px -620px; }
+.emoji-man_in_tuxedo_tone3 { background-position: -100px -620px; }
+.emoji-man_in_tuxedo_tone4 { background-position: -120px -620px; }
+.emoji-man_in_tuxedo_tone5 { background-position: -140px -620px; }
+.emoji-man_tone1 { background-position: -160px -620px; }
+.emoji-man_tone2 { background-position: -180px -620px; }
+.emoji-man_tone3 { background-position: -200px -620px; }
+.emoji-man_tone4 { background-position: -220px -620px; }
+.emoji-man_tone5 { background-position: -240px -620px; }
+.emoji-man_with_gua_pi_mao { background-position: -260px -620px; }
+.emoji-man_with_gua_pi_mao_tone1 { background-position: -280px -620px; }
+.emoji-man_with_gua_pi_mao_tone2 { background-position: -300px -620px; }
+.emoji-man_with_gua_pi_mao_tone3 { background-position: -320px -620px; }
+.emoji-man_with_gua_pi_mao_tone4 { background-position: -340px -620px; }
+.emoji-man_with_gua_pi_mao_tone5 { background-position: -360px -620px; }
+.emoji-man_with_turban { background-position: -380px -620px; }
+.emoji-man_with_turban_tone1 { background-position: -400px -620px; }
+.emoji-man_with_turban_tone2 { background-position: -420px -620px; }
+.emoji-man_with_turban_tone3 { background-position: -440px -620px; }
+.emoji-man_with_turban_tone4 { background-position: -460px -620px; }
+.emoji-man_with_turban_tone5 { background-position: -480px -620px; }
+.emoji-mans_shoe { background-position: -500px -620px; }
+.emoji-map { background-position: -520px -620px; }
+.emoji-maple_leaf { background-position: -540px -620px; }
+.emoji-martial_arts_uniform { background-position: -560px -620px; }
+.emoji-mask { background-position: -580px -620px; }
+.emoji-massage { background-position: -600px -620px; }
+.emoji-massage_tone1 { background-position: -620px -620px; }
+.emoji-massage_tone2 { background-position: -640px 0; }
+.emoji-massage_tone3 { background-position: -640px -20px; }
+.emoji-massage_tone4 { background-position: -640px -40px; }
+.emoji-massage_tone5 { background-position: -640px -60px; }
+.emoji-meat_on_bone { background-position: -640px -80px; }
+.emoji-medal { background-position: -640px -100px; }
+.emoji-mega { background-position: -640px -120px; }
+.emoji-melon { background-position: -640px -140px; }
+.emoji-menorah { background-position: -640px -160px; }
+.emoji-mens { background-position: -640px -180px; }
+.emoji-metal { background-position: -640px -200px; }
+.emoji-metal_tone1 { background-position: -640px -220px; }
+.emoji-metal_tone2 { background-position: -640px -240px; }
+.emoji-metal_tone3 { background-position: -640px -260px; }
+.emoji-metal_tone4 { background-position: -640px -280px; }
+.emoji-metal_tone5 { background-position: -640px -300px; }
+.emoji-metro { background-position: -640px -320px; }
+.emoji-microphone { background-position: -640px -340px; }
+.emoji-microphone2 { background-position: -640px -360px; }
+.emoji-microscope { background-position: -640px -380px; }
+.emoji-middle_finger { background-position: -640px -400px; }
+.emoji-middle_finger_tone1 { background-position: -640px -420px; }
+.emoji-middle_finger_tone2 { background-position: -640px -440px; }
+.emoji-middle_finger_tone3 { background-position: -640px -460px; }
+.emoji-middle_finger_tone4 { background-position: -640px -480px; }
+.emoji-middle_finger_tone5 { background-position: -640px -500px; }
+.emoji-military_medal { background-position: -640px -520px; }
+.emoji-milk { background-position: -640px -540px; }
+.emoji-milky_way { background-position: -640px -560px; }
+.emoji-minibus { background-position: -640px -580px; }
+.emoji-minidisc { background-position: -640px -600px; }
+.emoji-mobile_phone_off { background-position: -640px -620px; }
+.emoji-money_mouth { background-position: 0 -640px; }
+.emoji-money_with_wings { background-position: -20px -640px; }
+.emoji-moneybag { background-position: -40px -640px; }
+.emoji-monkey { background-position: -60px -640px; }
+.emoji-monkey_face { background-position: -80px -640px; }
+.emoji-monorail { background-position: -100px -640px; }
+.emoji-mortar_board { background-position: -120px -640px; }
+.emoji-mosque { background-position: -140px -640px; }
+.emoji-motor_scooter { background-position: -160px -640px; }
+.emoji-motorboat { background-position: -180px -640px; }
+.emoji-motorcycle { background-position: -200px -640px; }
+.emoji-motorway { background-position: -220px -640px; }
+.emoji-mount_fuji { background-position: -240px -640px; }
+.emoji-mountain { background-position: -260px -640px; }
+.emoji-mountain_bicyclist { background-position: -280px -640px; }
+.emoji-mountain_bicyclist_tone1 { background-position: -300px -640px; }
+.emoji-mountain_bicyclist_tone2 { background-position: -320px -640px; }
+.emoji-mountain_bicyclist_tone3 { background-position: -340px -640px; }
+.emoji-mountain_bicyclist_tone4 { background-position: -360px -640px; }
+.emoji-mountain_bicyclist_tone5 { background-position: -380px -640px; }
+.emoji-mountain_cableway { background-position: -400px -640px; }
+.emoji-mountain_railway { background-position: -420px -640px; }
+.emoji-mountain_snow { background-position: -440px -640px; }
+.emoji-mouse { background-position: -460px -640px; }
+.emoji-mouse2 { background-position: -480px -640px; }
+.emoji-mouse_three_button { background-position: -500px -640px; }
+.emoji-movie_camera { background-position: -520px -640px; }
+.emoji-moyai { background-position: -540px -640px; }
+.emoji-mrs_claus { background-position: -560px -640px; }
+.emoji-mrs_claus_tone1 { background-position: -580px -640px; }
+.emoji-mrs_claus_tone2 { background-position: -600px -640px; }
+.emoji-mrs_claus_tone3 { background-position: -620px -640px; }
+.emoji-mrs_claus_tone4 { background-position: -640px -640px; }
+.emoji-mrs_claus_tone5 { background-position: -660px 0; }
+.emoji-muscle { background-position: -660px -20px; }
+.emoji-muscle_tone1 { background-position: -660px -40px; }
+.emoji-muscle_tone2 { background-position: -660px -60px; }
+.emoji-muscle_tone3 { background-position: -660px -80px; }
+.emoji-muscle_tone4 { background-position: -660px -100px; }
+.emoji-muscle_tone5 { background-position: -660px -120px; }
+.emoji-mushroom { background-position: -660px -140px; }
+.emoji-musical_keyboard { background-position: -660px -160px; }
+.emoji-musical_note { background-position: -660px -180px; }
+.emoji-musical_score { background-position: -660px -200px; }
+.emoji-mute { background-position: -660px -220px; }
+.emoji-nail_care { background-position: -660px -240px; }
+.emoji-nail_care_tone1 { background-position: -660px -260px; }
+.emoji-nail_care_tone2 { background-position: -660px -280px; }
+.emoji-nail_care_tone3 { background-position: -660px -300px; }
+.emoji-nail_care_tone4 { background-position: -660px -320px; }
+.emoji-nail_care_tone5 { background-position: -660px -340px; }
+.emoji-name_badge { background-position: -660px -360px; }
+.emoji-nauseated_face { background-position: -660px -380px; }
+.emoji-necktie { background-position: -660px -400px; }
+.emoji-negative_squared_cross_mark { background-position: -660px -420px; }
+.emoji-nerd { background-position: -660px -440px; }
+.emoji-neutral_face { background-position: -660px -460px; }
+.emoji-new { background-position: -660px -480px; }
+.emoji-new_moon { background-position: -660px -500px; }
+.emoji-new_moon_with_face { background-position: -660px -520px; }
+.emoji-newspaper { background-position: -660px -540px; }
+.emoji-newspaper2 { background-position: -660px -560px; }
+.emoji-ng { background-position: -660px -580px; }
+.emoji-night_with_stars { background-position: -660px -600px; }
+.emoji-nine { background-position: -660px -620px; }
+.emoji-no_bell { background-position: -660px -640px; }
+.emoji-no_bicycles { background-position: 0 -660px; }
+.emoji-no_entry { background-position: -20px -660px; }
+.emoji-no_entry_sign { background-position: -40px -660px; }
+.emoji-no_good { background-position: -60px -660px; }
+.emoji-no_good_tone1 { background-position: -80px -660px; }
+.emoji-no_good_tone2 { background-position: -100px -660px; }
+.emoji-no_good_tone3 { background-position: -120px -660px; }
+.emoji-no_good_tone4 { background-position: -140px -660px; }
+.emoji-no_good_tone5 { background-position: -160px -660px; }
+.emoji-no_mobile_phones { background-position: -180px -660px; }
+.emoji-no_mouth { background-position: -200px -660px; }
+.emoji-no_pedestrians { background-position: -220px -660px; }
+.emoji-no_smoking { background-position: -240px -660px; }
+.emoji-non-potable_water { background-position: -260px -660px; }
+.emoji-nose { background-position: -280px -660px; }
+.emoji-nose_tone1 { background-position: -300px -660px; }
+.emoji-nose_tone2 { background-position: -320px -660px; }
+.emoji-nose_tone3 { background-position: -340px -660px; }
+.emoji-nose_tone4 { background-position: -360px -660px; }
+.emoji-nose_tone5 { background-position: -380px -660px; }
+.emoji-notebook { background-position: -400px -660px; }
+.emoji-notebook_with_decorative_cover { background-position: -420px -660px; }
+.emoji-notepad_spiral { background-position: -440px -660px; }
+.emoji-notes { background-position: -460px -660px; }
+.emoji-nut_and_bolt { background-position: -480px -660px; }
+.emoji-o { background-position: -500px -660px; }
+.emoji-o2 { background-position: -520px -660px; }
+.emoji-ocean { background-position: -540px -660px; }
+.emoji-octagonal_sign { background-position: -560px -660px; }
+.emoji-octopus { background-position: -580px -660px; }
+.emoji-oden { background-position: -600px -660px; }
+.emoji-office { background-position: -620px -660px; }
+.emoji-oil { background-position: -640px -660px; }
+.emoji-ok { background-position: -660px -660px; }
+.emoji-ok_hand { background-position: -680px 0; }
+.emoji-ok_hand_tone1 { background-position: -680px -20px; }
+.emoji-ok_hand_tone2 { background-position: -680px -40px; }
+.emoji-ok_hand_tone3 { background-position: -680px -60px; }
+.emoji-ok_hand_tone4 { background-position: -680px -80px; }
+.emoji-ok_hand_tone5 { background-position: -680px -100px; }
+.emoji-ok_woman { background-position: -680px -120px; }
+.emoji-ok_woman_tone1 { background-position: -680px -140px; }
+.emoji-ok_woman_tone2 { background-position: -680px -160px; }
+.emoji-ok_woman_tone3 { background-position: -680px -180px; }
+.emoji-ok_woman_tone4 { background-position: -680px -200px; }
+.emoji-ok_woman_tone5 { background-position: -680px -220px; }
+.emoji-older_man { background-position: -680px -240px; }
+.emoji-older_man_tone1 { background-position: -680px -260px; }
+.emoji-older_man_tone2 { background-position: -680px -280px; }
+.emoji-older_man_tone3 { background-position: -680px -300px; }
+.emoji-older_man_tone4 { background-position: -680px -320px; }
+.emoji-older_man_tone5 { background-position: -680px -340px; }
+.emoji-older_woman { background-position: -680px -360px; }
+.emoji-older_woman_tone1 { background-position: -680px -380px; }
+.emoji-older_woman_tone2 { background-position: -680px -400px; }
+.emoji-older_woman_tone3 { background-position: -680px -420px; }
+.emoji-older_woman_tone4 { background-position: -680px -440px; }
+.emoji-older_woman_tone5 { background-position: -680px -460px; }
+.emoji-om_symbol { background-position: -680px -480px; }
+.emoji-on { background-position: -680px -500px; }
+.emoji-oncoming_automobile { background-position: -680px -520px; }
+.emoji-oncoming_bus { background-position: -680px -540px; }
+.emoji-oncoming_police_car { background-position: -680px -560px; }
+.emoji-oncoming_taxi { background-position: -680px -580px; }
+.emoji-one { background-position: -680px -600px; }
+.emoji-open_file_folder { background-position: -680px -620px; }
+.emoji-open_hands { background-position: -680px -640px; }
+.emoji-open_hands_tone1 { background-position: -680px -660px; }
+.emoji-open_hands_tone2 { background-position: 0 -680px; }
+.emoji-open_hands_tone3 { background-position: -20px -680px; }
+.emoji-open_hands_tone4 { background-position: -40px -680px; }
+.emoji-open_hands_tone5 { background-position: -60px -680px; }
+.emoji-open_mouth { background-position: -80px -680px; }
+.emoji-ophiuchus { background-position: -100px -680px; }
+.emoji-orange_book { background-position: -120px -680px; }
+.emoji-orthodox_cross { background-position: -140px -680px; }
+.emoji-outbox_tray { background-position: -160px -680px; }
+.emoji-owl { background-position: -180px -680px; }
+.emoji-ox { background-position: -200px -680px; }
+.emoji-package { background-position: -220px -680px; }
+.emoji-page_facing_up { background-position: -240px -680px; }
+.emoji-page_with_curl { background-position: -260px -680px; }
+.emoji-pager { background-position: -280px -680px; }
+.emoji-paintbrush { background-position: -300px -680px; }
+.emoji-palm_tree { background-position: -320px -680px; }
+.emoji-pancakes { background-position: -340px -680px; }
+.emoji-panda_face { background-position: -360px -680px; }
+.emoji-paperclip { background-position: -380px -680px; }
+.emoji-paperclips { background-position: -400px -680px; }
+.emoji-park { background-position: -420px -680px; }
+.emoji-parking { background-position: -440px -680px; }
+.emoji-part_alternation_mark { background-position: -460px -680px; }
+.emoji-partly_sunny { background-position: -480px -680px; }
+.emoji-passport_control { background-position: -500px -680px; }
+.emoji-pause_button { background-position: -520px -680px; }
+.emoji-peace { background-position: -540px -680px; }
+.emoji-peach { background-position: -560px -680px; }
+.emoji-peanuts { background-position: -580px -680px; }
+.emoji-pear { background-position: -600px -680px; }
+.emoji-pen_ballpoint { background-position: -620px -680px; }
+.emoji-pen_fountain { background-position: -640px -680px; }
+.emoji-pencil { background-position: -660px -680px; }
+.emoji-pencil2 { background-position: -680px -680px; }
+.emoji-penguin { background-position: -700px 0; }
+.emoji-pensive { background-position: -700px -20px; }
+.emoji-performing_arts { background-position: -700px -40px; }
+.emoji-persevere { background-position: -700px -60px; }
+.emoji-person_frowning { background-position: -700px -80px; }
+.emoji-person_frowning_tone1 { background-position: -700px -100px; }
+.emoji-person_frowning_tone2 { background-position: -700px -120px; }
+.emoji-person_frowning_tone3 { background-position: -700px -140px; }
+.emoji-person_frowning_tone4 { background-position: -700px -160px; }
+.emoji-person_frowning_tone5 { background-position: -700px -180px; }
+.emoji-person_with_blond_hair { background-position: -700px -200px; }
+.emoji-person_with_blond_hair_tone1 { background-position: -700px -220px; }
+.emoji-person_with_blond_hair_tone2 { background-position: -700px -240px; }
+.emoji-person_with_blond_hair_tone3 { background-position: -700px -260px; }
+.emoji-person_with_blond_hair_tone4 { background-position: -700px -280px; }
+.emoji-person_with_blond_hair_tone5 { background-position: -700px -300px; }
+.emoji-person_with_pouting_face { background-position: -700px -320px; }
+.emoji-person_with_pouting_face_tone1 { background-position: -700px -340px; }
+.emoji-person_with_pouting_face_tone2 { background-position: -700px -360px; }
+.emoji-person_with_pouting_face_tone3 { background-position: -700px -380px; }
+.emoji-person_with_pouting_face_tone4 { background-position: -700px -400px; }
+.emoji-person_with_pouting_face_tone5 { background-position: -700px -420px; }
+.emoji-pick { background-position: -700px -440px; }
+.emoji-pig { background-position: -700px -460px; }
+.emoji-pig2 { background-position: -700px -480px; }
+.emoji-pig_nose { background-position: -700px -500px; }
+.emoji-pill { background-position: -700px -520px; }
+.emoji-pineapple { background-position: -700px -540px; }
+.emoji-ping_pong { background-position: -700px -560px; }
+.emoji-pisces { background-position: -700px -580px; }
+.emoji-pizza { background-position: -700px -600px; }
+.emoji-place_of_worship { background-position: -700px -620px; }
+.emoji-play_pause { background-position: -700px -640px; }
+.emoji-point_down { background-position: -700px -660px; }
+.emoji-point_down_tone1 { background-position: -700px -680px; }
+.emoji-point_down_tone2 { background-position: 0 -700px; }
+.emoji-point_down_tone3 { background-position: -20px -700px; }
+.emoji-point_down_tone4 { background-position: -40px -700px; }
+.emoji-point_down_tone5 { background-position: -60px -700px; }
+.emoji-point_left { background-position: -80px -700px; }
+.emoji-point_left_tone1 { background-position: -100px -700px; }
+.emoji-point_left_tone2 { background-position: -120px -700px; }
+.emoji-point_left_tone3 { background-position: -140px -700px; }
+.emoji-point_left_tone4 { background-position: -160px -700px; }
+.emoji-point_left_tone5 { background-position: -180px -700px; }
+.emoji-point_right { background-position: -200px -700px; }
+.emoji-point_right_tone1 { background-position: -220px -700px; }
+.emoji-point_right_tone2 { background-position: -240px -700px; }
+.emoji-point_right_tone3 { background-position: -260px -700px; }
+.emoji-point_right_tone4 { background-position: -280px -700px; }
+.emoji-point_right_tone5 { background-position: -300px -700px; }
+.emoji-point_up { background-position: -320px -700px; }
+.emoji-point_up_2 { background-position: -340px -700px; }
+.emoji-point_up_2_tone1 { background-position: -360px -700px; }
+.emoji-point_up_2_tone2 { background-position: -380px -700px; }
+.emoji-point_up_2_tone3 { background-position: -400px -700px; }
+.emoji-point_up_2_tone4 { background-position: -420px -700px; }
+.emoji-point_up_2_tone5 { background-position: -440px -700px; }
+.emoji-point_up_tone1 { background-position: -460px -700px; }
+.emoji-point_up_tone2 { background-position: -480px -700px; }
+.emoji-point_up_tone3 { background-position: -500px -700px; }
+.emoji-point_up_tone4 { background-position: -520px -700px; }
+.emoji-point_up_tone5 { background-position: -540px -700px; }
+.emoji-police_car { background-position: -560px -700px; }
+.emoji-poodle { background-position: -580px -700px; }
+.emoji-poop { background-position: -600px -700px; }
+.emoji-popcorn { background-position: -620px -700px; }
+.emoji-post_office { background-position: -640px -700px; }
+.emoji-postal_horn { background-position: -660px -700px; }
+.emoji-postbox { background-position: -680px -700px; }
+.emoji-potable_water { background-position: -700px -700px; }
+.emoji-potato { background-position: -720px 0; }
+.emoji-pouch { background-position: -720px -20px; }
+.emoji-poultry_leg { background-position: -720px -40px; }
+.emoji-pound { background-position: -720px -60px; }
+.emoji-pouting_cat { background-position: -720px -80px; }
+.emoji-pray { background-position: -720px -100px; }
+.emoji-pray_tone1 { background-position: -720px -120px; }
+.emoji-pray_tone2 { background-position: -720px -140px; }
+.emoji-pray_tone3 { background-position: -720px -160px; }
+.emoji-pray_tone4 { background-position: -720px -180px; }
+.emoji-pray_tone5 { background-position: -720px -200px; }
+.emoji-prayer_beads { background-position: -720px -220px; }
+.emoji-pregnant_woman { background-position: -720px -240px; }
+.emoji-pregnant_woman_tone1 { background-position: -720px -260px; }
+.emoji-pregnant_woman_tone2 { background-position: -720px -280px; }
+.emoji-pregnant_woman_tone3 { background-position: -720px -300px; }
+.emoji-pregnant_woman_tone4 { background-position: -720px -320px; }
+.emoji-pregnant_woman_tone5 { background-position: -720px -340px; }
+.emoji-prince { background-position: -720px -360px; }
+.emoji-prince_tone1 { background-position: -720px -380px; }
+.emoji-prince_tone2 { background-position: -720px -400px; }
+.emoji-prince_tone3 { background-position: -720px -420px; }
+.emoji-prince_tone4 { background-position: -720px -440px; }
+.emoji-prince_tone5 { background-position: -720px -460px; }
+.emoji-princess { background-position: -720px -480px; }
+.emoji-princess_tone1 { background-position: -720px -500px; }
+.emoji-princess_tone2 { background-position: -720px -520px; }
+.emoji-princess_tone3 { background-position: -720px -540px; }
+.emoji-princess_tone4 { background-position: -720px -560px; }
+.emoji-princess_tone5 { background-position: -720px -580px; }
+.emoji-printer { background-position: -720px -600px; }
+.emoji-projector { background-position: -720px -620px; }
+.emoji-punch { background-position: -720px -640px; }
+.emoji-punch_tone1 { background-position: -720px -660px; }
+.emoji-punch_tone2 { background-position: -720px -680px; }
+.emoji-punch_tone3 { background-position: -720px -700px; }
+.emoji-punch_tone4 { background-position: 0 -720px; }
+.emoji-punch_tone5 { background-position: -20px -720px; }
+.emoji-purple_heart { background-position: -40px -720px; }
+.emoji-purse { background-position: -60px -720px; }
+.emoji-pushpin { background-position: -80px -720px; }
+.emoji-put_litter_in_its_place { background-position: -100px -720px; }
+.emoji-question { background-position: -120px -720px; }
+.emoji-rabbit { background-position: -140px -720px; }
+.emoji-rabbit2 { background-position: -160px -720px; }
+.emoji-race_car { background-position: -180px -720px; }
+.emoji-racehorse { background-position: -200px -720px; }
+.emoji-radio { background-position: -220px -720px; }
+.emoji-radio_button { background-position: -240px -720px; }
+.emoji-radioactive { background-position: -260px -720px; }
+.emoji-rage { background-position: -280px -720px; }
+.emoji-railway_car { background-position: -300px -720px; }
+.emoji-railway_track { background-position: -320px -720px; }
+.emoji-rainbow { background-position: -340px -720px; }
+.emoji-raised_back_of_hand { background-position: -360px -720px; }
+.emoji-raised_back_of_hand_tone1 { background-position: -380px -720px; }
+.emoji-raised_back_of_hand_tone2 { background-position: -400px -720px; }
+.emoji-raised_back_of_hand_tone3 { background-position: -420px -720px; }
+.emoji-raised_back_of_hand_tone4 { background-position: -440px -720px; }
+.emoji-raised_back_of_hand_tone5 { background-position: -460px -720px; }
+.emoji-raised_hand { background-position: -480px -720px; }
+.emoji-raised_hand_tone1 { background-position: -500px -720px; }
+.emoji-raised_hand_tone2 { background-position: -520px -720px; }
+.emoji-raised_hand_tone3 { background-position: -540px -720px; }
+.emoji-raised_hand_tone4 { background-position: -560px -720px; }
+.emoji-raised_hand_tone5 { background-position: -580px -720px; }
+.emoji-raised_hands { background-position: -600px -720px; }
+.emoji-raised_hands_tone1 { background-position: -620px -720px; }
+.emoji-raised_hands_tone2 { background-position: -640px -720px; }
+.emoji-raised_hands_tone3 { background-position: -660px -720px; }
+.emoji-raised_hands_tone4 { background-position: -680px -720px; }
+.emoji-raised_hands_tone5 { background-position: -700px -720px; }
+.emoji-raising_hand { background-position: -720px -720px; }
+.emoji-raising_hand_tone1 { background-position: -740px 0; }
+.emoji-raising_hand_tone2 { background-position: -740px -20px; }
+.emoji-raising_hand_tone3 { background-position: -740px -40px; }
+.emoji-raising_hand_tone4 { background-position: -740px -60px; }
+.emoji-raising_hand_tone5 { background-position: -740px -80px; }
+.emoji-ram { background-position: -740px -100px; }
+.emoji-ramen { background-position: -740px -120px; }
+.emoji-rat { background-position: -740px -140px; }
+.emoji-record_button { background-position: -740px -160px; }
+.emoji-recycle { background-position: -740px -180px; }
+.emoji-red_car { background-position: -740px -200px; }
+.emoji-red_circle { background-position: -740px -220px; }
+.emoji-registered { background-position: -740px -240px; }
+.emoji-relaxed { background-position: -740px -260px; }
+.emoji-relieved { background-position: -740px -280px; }
+.emoji-reminder_ribbon { background-position: -740px -300px; }
+.emoji-repeat { background-position: -740px -320px; }
+.emoji-repeat_one { background-position: -740px -340px; }
+.emoji-restroom { background-position: -740px -360px; }
+.emoji-revolving_hearts { background-position: -740px -380px; }
+.emoji-rewind { background-position: -740px -400px; }
+.emoji-rhino { background-position: -740px -420px; }
+.emoji-ribbon { background-position: -740px -440px; }
+.emoji-rice { background-position: -740px -460px; }
+.emoji-rice_ball { background-position: -740px -480px; }
+.emoji-rice_cracker { background-position: -740px -500px; }
+.emoji-rice_scene { background-position: -740px -520px; }
+.emoji-right_facing_fist { background-position: -740px -540px; }
+.emoji-right_facing_fist_tone1 { background-position: -740px -560px; }
+.emoji-right_facing_fist_tone2 { background-position: -740px -580px; }
+.emoji-right_facing_fist_tone3 { background-position: -740px -600px; }
+.emoji-right_facing_fist_tone4 { background-position: -740px -620px; }
+.emoji-right_facing_fist_tone5 { background-position: -740px -640px; }
+.emoji-ring { background-position: -740px -660px; }
+.emoji-robot { background-position: -740px -680px; }
+.emoji-rocket { background-position: -740px -700px; }
+.emoji-rofl { background-position: -740px -720px; }
+.emoji-roller_coaster { background-position: 0 -740px; }
+.emoji-rolling_eyes { background-position: -20px -740px; }
+.emoji-rooster { background-position: -40px -740px; }
+.emoji-rose { background-position: -60px -740px; }
+.emoji-rosette { background-position: -80px -740px; }
+.emoji-rotating_light { background-position: -100px -740px; }
+.emoji-round_pushpin { background-position: -120px -740px; }
+.emoji-rowboat { background-position: -140px -740px; }
+.emoji-rowboat_tone1 { background-position: -160px -740px; }
+.emoji-rowboat_tone2 { background-position: -180px -740px; }
+.emoji-rowboat_tone3 { background-position: -200px -740px; }
+.emoji-rowboat_tone4 { background-position: -220px -740px; }
+.emoji-rowboat_tone5 { background-position: -240px -740px; }
+.emoji-rugby_football { background-position: -260px -740px; }
+.emoji-runner { background-position: -280px -740px; }
+.emoji-runner_tone1 { background-position: -300px -740px; }
+.emoji-runner_tone2 { background-position: -320px -740px; }
+.emoji-runner_tone3 { background-position: -340px -740px; }
+.emoji-runner_tone4 { background-position: -360px -740px; }
+.emoji-runner_tone5 { background-position: -380px -740px; }
+.emoji-running_shirt_with_sash { background-position: -400px -740px; }
+.emoji-sa { background-position: -420px -740px; }
+.emoji-sagittarius { background-position: -440px -740px; }
+.emoji-sailboat { background-position: -460px -740px; }
+.emoji-sake { background-position: -480px -740px; }
+.emoji-salad { background-position: -500px -740px; }
+.emoji-sandal { background-position: -520px -740px; }
+.emoji-santa { background-position: -540px -740px; }
+.emoji-santa_tone1 { background-position: -560px -740px; }
+.emoji-santa_tone2 { background-position: -580px -740px; }
+.emoji-santa_tone3 { background-position: -600px -740px; }
+.emoji-santa_tone4 { background-position: -620px -740px; }
+.emoji-santa_tone5 { background-position: -640px -740px; }
+.emoji-satellite { background-position: -660px -740px; }
+.emoji-satellite_orbital { background-position: -680px -740px; }
+.emoji-saxophone { background-position: -700px -740px; }
+.emoji-scales { background-position: -720px -740px; }
+.emoji-school { background-position: -740px -740px; }
+.emoji-school_satchel { background-position: -760px 0; }
+.emoji-scissors { background-position: -760px -20px; }
+.emoji-scooter { background-position: -760px -40px; }
+.emoji-scorpion { background-position: -760px -60px; }
+.emoji-scorpius { background-position: -760px -80px; }
+.emoji-scream { background-position: -760px -100px; }
+.emoji-scream_cat { background-position: -760px -120px; }
+.emoji-scroll { background-position: -760px -140px; }
+.emoji-seat { background-position: -760px -160px; }
+.emoji-second_place { background-position: -760px -180px; }
+.emoji-secret { background-position: -760px -200px; }
+.emoji-see_no_evil { background-position: -760px -220px; }
+.emoji-seedling { background-position: -760px -240px; }
+.emoji-selfie { background-position: -760px -260px; }
+.emoji-selfie_tone1 { background-position: -760px -280px; }
+.emoji-selfie_tone2 { background-position: -760px -300px; }
+.emoji-selfie_tone3 { background-position: -760px -320px; }
+.emoji-selfie_tone4 { background-position: -760px -340px; }
+.emoji-selfie_tone5 { background-position: -760px -360px; }
+.emoji-seven { background-position: -760px -380px; }
+.emoji-shallow_pan_of_food { background-position: -760px -400px; }
+.emoji-shamrock { background-position: -760px -420px; }
+.emoji-shark { background-position: -760px -440px; }
+.emoji-shaved_ice { background-position: -760px -460px; }
+.emoji-sheep { background-position: -760px -480px; }
+.emoji-shell { background-position: -760px -500px; }
+.emoji-shield { background-position: -760px -520px; }
+.emoji-shinto_shrine { background-position: -760px -540px; }
+.emoji-ship { background-position: -760px -560px; }
+.emoji-shirt { background-position: -760px -580px; }
+.emoji-shopping_bags { background-position: -760px -600px; }
+.emoji-shopping_cart { background-position: -760px -620px; }
+.emoji-shower { background-position: -760px -640px; }
+.emoji-shrimp { background-position: -760px -660px; }
+.emoji-shrug { background-position: -760px -680px; }
+.emoji-shrug_tone1 { background-position: -760px -700px; }
+.emoji-shrug_tone2 { background-position: -760px -720px; }
+.emoji-shrug_tone3 { background-position: -760px -740px; }
+.emoji-shrug_tone4 { background-position: 0 -760px; }
+.emoji-shrug_tone5 { background-position: -20px -760px; }
+.emoji-signal_strength { background-position: -40px -760px; }
+.emoji-six { background-position: -60px -760px; }
+.emoji-six_pointed_star { background-position: -80px -760px; }
+.emoji-ski { background-position: -100px -760px; }
+.emoji-skier { background-position: -120px -760px; }
+.emoji-skull { background-position: -140px -760px; }
+.emoji-skull_crossbones { background-position: -160px -760px; }
+.emoji-sleeping { background-position: -180px -760px; }
+.emoji-sleeping_accommodation { background-position: -200px -760px; }
+.emoji-sleepy { background-position: -220px -760px; }
+.emoji-slight_frown { background-position: -240px -760px; }
+.emoji-slight_smile { background-position: -260px -760px; }
+.emoji-slot_machine { background-position: -280px -760px; }
+.emoji-small_blue_diamond { background-position: -300px -760px; }
+.emoji-small_orange_diamond { background-position: -320px -760px; }
+.emoji-small_red_triangle { background-position: -340px -760px; }
+.emoji-small_red_triangle_down { background-position: -360px -760px; }
+.emoji-smile { background-position: -380px -760px; }
+.emoji-smile_cat { background-position: -400px -760px; }
+.emoji-smiley { background-position: -420px -760px; }
+.emoji-smiley_cat { background-position: -440px -760px; }
+.emoji-smiling_imp { background-position: -460px -760px; }
+.emoji-smirk { background-position: -480px -760px; }
+.emoji-smirk_cat { background-position: -500px -760px; }
+.emoji-smoking { background-position: -520px -760px; }
+.emoji-snail { background-position: -540px -760px; }
+.emoji-snake { background-position: -560px -760px; }
+.emoji-sneezing_face { background-position: -580px -760px; }
+.emoji-snowboarder { background-position: -600px -760px; }
+.emoji-snowflake { background-position: -620px -760px; }
+.emoji-snowman { background-position: -640px -760px; }
+.emoji-snowman2 { background-position: -660px -760px; }
+.emoji-sob { background-position: -680px -760px; }
+.emoji-soccer { background-position: -700px -760px; }
+.emoji-soon { background-position: -720px -760px; }
+.emoji-sos { background-position: -740px -760px; }
+.emoji-sound { background-position: -760px -760px; }
+.emoji-space_invader { background-position: -780px 0; }
+.emoji-spades { background-position: -780px -20px; }
+.emoji-spaghetti { background-position: -780px -40px; }
+.emoji-sparkle { background-position: -780px -60px; }
+.emoji-sparkler { background-position: -780px -80px; }
+.emoji-sparkles { background-position: -780px -100px; }
+.emoji-sparkling_heart { background-position: -780px -120px; }
+.emoji-speak_no_evil { background-position: -780px -140px; }
+.emoji-speaker { background-position: -780px -160px; }
+.emoji-speaking_head { background-position: -780px -180px; }
+.emoji-speech_balloon { background-position: -780px -200px; }
+.emoji-speech_left { background-position: -780px -220px; }
+.emoji-speedboat { background-position: -780px -240px; }
+.emoji-spider { background-position: -780px -260px; }
+.emoji-spider_web { background-position: -780px -280px; }
+.emoji-spoon { background-position: -780px -300px; }
+.emoji-spy { background-position: -780px -320px; }
+.emoji-spy_tone1 { background-position: -780px -340px; }
+.emoji-spy_tone2 { background-position: -780px -360px; }
+.emoji-spy_tone3 { background-position: -780px -380px; }
+.emoji-spy_tone4 { background-position: -780px -400px; }
+.emoji-spy_tone5 { background-position: -780px -420px; }
+.emoji-squid { background-position: -780px -440px; }
+.emoji-stadium { background-position: -780px -460px; }
+.emoji-star { background-position: -780px -480px; }
+.emoji-star2 { background-position: -780px -500px; }
+.emoji-star_and_crescent { background-position: -780px -520px; }
+.emoji-star_of_david { background-position: -780px -540px; }
+.emoji-stars { background-position: -780px -560px; }
+.emoji-station { background-position: -780px -580px; }
+.emoji-statue_of_liberty { background-position: -780px -600px; }
+.emoji-steam_locomotive { background-position: -780px -620px; }
+.emoji-stew { background-position: -780px -640px; }
+.emoji-stop_button { background-position: -780px -660px; }
+.emoji-stopwatch { background-position: -780px -680px; }
+.emoji-straight_ruler { background-position: -780px -700px; }
+.emoji-strawberry { background-position: -780px -720px; }
+.emoji-stuck_out_tongue { background-position: -780px -740px; }
+.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -760px; }
+.emoji-stuck_out_tongue_winking_eye { background-position: 0 -780px; }
+.emoji-stuffed_flatbread { background-position: -20px -780px; }
+.emoji-sun_with_face { background-position: -40px -780px; }
+.emoji-sunflower { background-position: -60px -780px; }
+.emoji-sunglasses { background-position: -80px -780px; }
+.emoji-sunny { background-position: -100px -780px; }
+.emoji-sunrise { background-position: -120px -780px; }
+.emoji-sunrise_over_mountains { background-position: -140px -780px; }
+.emoji-surfer { background-position: -160px -780px; }
+.emoji-surfer_tone1 { background-position: -180px -780px; }
+.emoji-surfer_tone2 { background-position: -200px -780px; }
+.emoji-surfer_tone3 { background-position: -220px -780px; }
+.emoji-surfer_tone4 { background-position: -240px -780px; }
+.emoji-surfer_tone5 { background-position: -260px -780px; }
+.emoji-sushi { background-position: -280px -780px; }
+.emoji-suspension_railway { background-position: -300px -780px; }
+.emoji-sweat { background-position: -320px -780px; }
+.emoji-sweat_drops { background-position: -340px -780px; }
+.emoji-sweat_smile { background-position: -360px -780px; }
+.emoji-sweet_potato { background-position: -380px -780px; }
+.emoji-swimmer { background-position: -400px -780px; }
+.emoji-swimmer_tone1 { background-position: -420px -780px; }
+.emoji-swimmer_tone2 { background-position: -440px -780px; }
+.emoji-swimmer_tone3 { background-position: -460px -780px; }
+.emoji-swimmer_tone4 { background-position: -480px -780px; }
+.emoji-swimmer_tone5 { background-position: -500px -780px; }
+.emoji-symbols { background-position: -520px -780px; }
+.emoji-synagogue { background-position: -540px -780px; }
+.emoji-syringe { background-position: -560px -780px; }
+.emoji-taco { background-position: -580px -780px; }
+.emoji-tada { background-position: -600px -780px; }
+.emoji-tanabata_tree { background-position: -620px -780px; }
+.emoji-tangerine { background-position: -640px -780px; }
+.emoji-taurus { background-position: -660px -780px; }
+.emoji-taxi { background-position: -680px -780px; }
+.emoji-tea { background-position: -700px -780px; }
+.emoji-telephone { background-position: -720px -780px; }
+.emoji-telephone_receiver { background-position: -740px -780px; }
+.emoji-telescope { background-position: -760px -780px; }
+.emoji-ten { background-position: -780px -780px; }
+.emoji-tennis { background-position: -800px 0; }
+.emoji-tent { background-position: -800px -20px; }
+.emoji-thermometer { background-position: -800px -40px; }
+.emoji-thermometer_face { background-position: -800px -60px; }
+.emoji-thinking { background-position: -800px -80px; }
+.emoji-third_place { background-position: -800px -100px; }
+.emoji-thought_balloon { background-position: -800px -120px; }
+.emoji-three { background-position: -800px -140px; }
+.emoji-thumbsdown { background-position: -800px -160px; }
+.emoji-thumbsdown_tone1 { background-position: -800px -180px; }
+.emoji-thumbsdown_tone2 { background-position: -800px -200px; }
+.emoji-thumbsdown_tone3 { background-position: -800px -220px; }
+.emoji-thumbsdown_tone4 { background-position: -800px -240px; }
+.emoji-thumbsdown_tone5 { background-position: -800px -260px; }
+.emoji-thumbsup { background-position: -800px -280px; }
+.emoji-thumbsup_tone1 { background-position: -800px -300px; }
+.emoji-thumbsup_tone2 { background-position: -800px -320px; }
+.emoji-thumbsup_tone3 { background-position: -800px -340px; }
+.emoji-thumbsup_tone4 { background-position: -800px -360px; }
+.emoji-thumbsup_tone5 { background-position: -800px -380px; }
+.emoji-thunder_cloud_rain { background-position: -800px -400px; }
+.emoji-ticket { background-position: -800px -420px; }
+.emoji-tickets { background-position: -800px -440px; }
+.emoji-tiger { background-position: -800px -460px; }
+.emoji-tiger2 { background-position: -800px -480px; }
+.emoji-timer { background-position: -800px -500px; }
+.emoji-tired_face { background-position: -800px -520px; }
+.emoji-tm { background-position: -800px -540px; }
+.emoji-toilet { background-position: -800px -560px; }
+.emoji-tokyo_tower { background-position: -800px -580px; }
+.emoji-tomato { background-position: -800px -600px; }
+.emoji-tone1 { background-position: -800px -620px; }
+.emoji-tone2 { background-position: -800px -640px; }
+.emoji-tone3 { background-position: -800px -660px; }
+.emoji-tone4 { background-position: -800px -680px; }
+.emoji-tone5 { background-position: -800px -700px; }
+.emoji-tongue { background-position: -800px -720px; }
+.emoji-tools { background-position: -800px -740px; }
+.emoji-top { background-position: -800px -760px; }
+.emoji-tophat { background-position: -800px -780px; }
+.emoji-track_next { background-position: 0 -800px; }
+.emoji-track_previous { background-position: -20px -800px; }
+.emoji-trackball { background-position: -40px -800px; }
+.emoji-tractor { background-position: -60px -800px; }
+.emoji-traffic_light { background-position: -80px -800px; }
+.emoji-train { background-position: -100px -800px; }
+.emoji-train2 { background-position: -120px -800px; }
+.emoji-tram { background-position: -140px -800px; }
+.emoji-triangular_flag_on_post { background-position: -160px -800px; }
+.emoji-triangular_ruler { background-position: -180px -800px; }
+.emoji-trident { background-position: -200px -800px; }
+.emoji-triumph { background-position: -220px -800px; }
+.emoji-trolleybus { background-position: -240px -800px; }
+.emoji-trophy { background-position: -260px -800px; }
+.emoji-tropical_drink { background-position: -280px -800px; }
+.emoji-tropical_fish { background-position: -300px -800px; }
+.emoji-truck { background-position: -320px -800px; }
+.emoji-trumpet { background-position: -340px -800px; }
+.emoji-tulip { background-position: -360px -800px; }
+.emoji-tumbler_glass { background-position: -380px -800px; }
+.emoji-turkey { background-position: -400px -800px; }
+.emoji-turtle { background-position: -420px -800px; }
+.emoji-tv { background-position: -440px -800px; }
+.emoji-twisted_rightwards_arrows { background-position: -460px -800px; }
+.emoji-two { background-position: -480px -800px; }
+.emoji-two_hearts { background-position: -500px -800px; }
+.emoji-two_men_holding_hands { background-position: -520px -800px; }
+.emoji-two_women_holding_hands { background-position: -540px -800px; }
+.emoji-u5272 { background-position: -560px -800px; }
+.emoji-u5408 { background-position: -580px -800px; }
+.emoji-u55b6 { background-position: -600px -800px; }
+.emoji-u6307 { background-position: -620px -800px; }
+.emoji-u6708 { background-position: -640px -800px; }
+.emoji-u6709 { background-position: -660px -800px; }
+.emoji-u6e80 { background-position: -680px -800px; }
+.emoji-u7121 { background-position: -700px -800px; }
+.emoji-u7533 { background-position: -720px -800px; }
+.emoji-u7981 { background-position: -740px -800px; }
+.emoji-u7a7a { background-position: -760px -800px; }
+.emoji-umbrella { background-position: -780px -800px; }
+.emoji-umbrella2 { background-position: -800px -800px; }
+.emoji-unamused { background-position: -820px 0; }
+.emoji-underage { background-position: -820px -20px; }
+.emoji-unicorn { background-position: -820px -40px; }
+.emoji-unlock { background-position: -820px -60px; }
+.emoji-up { background-position: -820px -80px; }
+.emoji-upside_down { background-position: -820px -100px; }
+.emoji-urn { background-position: -820px -120px; }
+.emoji-v { background-position: -820px -140px; }
+.emoji-v_tone1 { background-position: -820px -160px; }
+.emoji-v_tone2 { background-position: -820px -180px; }
+.emoji-v_tone3 { background-position: -820px -200px; }
+.emoji-v_tone4 { background-position: -820px -220px; }
+.emoji-v_tone5 { background-position: -820px -240px; }
+.emoji-vertical_traffic_light { background-position: -820px -260px; }
+.emoji-vhs { background-position: -820px -280px; }
+.emoji-vibration_mode { background-position: -820px -300px; }
+.emoji-video_camera { background-position: -820px -320px; }
+.emoji-video_game { background-position: -820px -340px; }
+.emoji-violin { background-position: -820px -360px; }
+.emoji-virgo { background-position: -820px -380px; }
+.emoji-volcano { background-position: -820px -400px; }
+.emoji-volleyball { background-position: -820px -420px; }
+.emoji-vs { background-position: -820px -440px; }
+.emoji-vulcan { background-position: -820px -460px; }
+.emoji-vulcan_tone1 { background-position: -820px -480px; }
+.emoji-vulcan_tone2 { background-position: -820px -500px; }
+.emoji-vulcan_tone3 { background-position: -820px -520px; }
+.emoji-vulcan_tone4 { background-position: -820px -540px; }
+.emoji-vulcan_tone5 { background-position: -820px -560px; }
+.emoji-walking { background-position: -820px -580px; }
+.emoji-walking_tone1 { background-position: -820px -600px; }
+.emoji-walking_tone2 { background-position: -820px -620px; }
+.emoji-walking_tone3 { background-position: -820px -640px; }
+.emoji-walking_tone4 { background-position: -820px -660px; }
+.emoji-walking_tone5 { background-position: -820px -680px; }
+.emoji-waning_crescent_moon { background-position: -820px -700px; }
+.emoji-waning_gibbous_moon { background-position: -820px -720px; }
+.emoji-warning { background-position: -820px -740px; }
+.emoji-wastebasket { background-position: -820px -760px; }
+.emoji-watch { background-position: -820px -780px; }
+.emoji-water_buffalo { background-position: -820px -800px; }
+.emoji-water_polo { background-position: 0 -820px; }
+.emoji-water_polo_tone1 { background-position: -20px -820px; }
+.emoji-water_polo_tone2 { background-position: -40px -820px; }
+.emoji-water_polo_tone3 { background-position: -60px -820px; }
+.emoji-water_polo_tone4 { background-position: -80px -820px; }
+.emoji-water_polo_tone5 { background-position: -100px -820px; }
+.emoji-watermelon { background-position: -120px -820px; }
+.emoji-wave { background-position: -140px -820px; }
+.emoji-wave_tone1 { background-position: -160px -820px; }
+.emoji-wave_tone2 { background-position: -180px -820px; }
+.emoji-wave_tone3 { background-position: -200px -820px; }
+.emoji-wave_tone4 { background-position: -220px -820px; }
+.emoji-wave_tone5 { background-position: -240px -820px; }
+.emoji-wavy_dash { background-position: -260px -820px; }
+.emoji-waxing_crescent_moon { background-position: -280px -820px; }
+.emoji-waxing_gibbous_moon { background-position: -300px -820px; }
+.emoji-wc { background-position: -320px -820px; }
+.emoji-weary { background-position: -340px -820px; }
+.emoji-wedding { background-position: -360px -820px; }
+.emoji-whale { background-position: -380px -820px; }
+.emoji-whale2 { background-position: -400px -820px; }
+.emoji-wheel_of_dharma { background-position: -420px -820px; }
+.emoji-wheelchair { background-position: -440px -820px; }
+.emoji-white_check_mark { background-position: -460px -820px; }
+.emoji-white_circle { background-position: -480px -820px; }
+.emoji-white_flower { background-position: -500px -820px; }
+.emoji-white_large_square { background-position: -520px -820px; }
+.emoji-white_medium_small_square { background-position: -540px -820px; }
+.emoji-white_medium_square { background-position: -560px -820px; }
+.emoji-white_small_square { background-position: -580px -820px; }
+.emoji-white_square_button { background-position: -600px -820px; }
+.emoji-white_sun_cloud { background-position: -620px -820px; }
+.emoji-white_sun_rain_cloud { background-position: -640px -820px; }
+.emoji-white_sun_small_cloud { background-position: -660px -820px; }
+.emoji-wilted_rose { background-position: -680px -820px; }
+.emoji-wind_blowing_face { background-position: -700px -820px; }
+.emoji-wind_chime { background-position: -720px -820px; }
+.emoji-wine_glass { background-position: -740px -820px; }
+.emoji-wink { background-position: -760px -820px; }
+.emoji-wolf { background-position: -780px -820px; }
+.emoji-woman { background-position: -800px -820px; }
+.emoji-woman_tone1 { background-position: -820px -820px; }
+.emoji-woman_tone2 { background-position: -840px 0; }
+.emoji-woman_tone3 { background-position: -840px -20px; }
+.emoji-woman_tone4 { background-position: -840px -40px; }
+.emoji-woman_tone5 { background-position: -840px -60px; }
+.emoji-womans_clothes { background-position: -840px -80px; }
+.emoji-womans_hat { background-position: -840px -100px; }
+.emoji-womens { background-position: -840px -120px; }
+.emoji-worried { background-position: -840px -140px; }
+.emoji-wrench { background-position: -840px -160px; }
+.emoji-wrestlers { background-position: -840px -180px; }
+.emoji-wrestlers_tone1 { background-position: -840px -200px; }
+.emoji-wrestlers_tone2 { background-position: -840px -220px; }
+.emoji-wrestlers_tone3 { background-position: -840px -240px; }
+.emoji-wrestlers_tone4 { background-position: -840px -260px; }
+.emoji-wrestlers_tone5 { background-position: -840px -280px; }
+.emoji-writing_hand { background-position: -840px -300px; }
+.emoji-writing_hand_tone1 { background-position: -840px -320px; }
+.emoji-writing_hand_tone2 { background-position: -840px -340px; }
+.emoji-writing_hand_tone3 { background-position: -840px -360px; }
+.emoji-writing_hand_tone4 { background-position: -840px -380px; }
+.emoji-writing_hand_tone5 { background-position: -840px -400px; }
+.emoji-x { background-position: -840px -420px; }
+.emoji-yellow_heart { background-position: -840px -440px; }
+.emoji-yen { background-position: -840px -460px; }
+.emoji-yin_yang { background-position: -840px -480px; }
+.emoji-yum { background-position: -840px -500px; }
+.emoji-zap { background-position: -840px -520px; }
+.emoji-zero { background-position: -840px -540px; }
+.emoji-zipper_mouth { background-position: -840px -560px; }
+.emoji-100 { background-position: -840px -580px; }
.emoji-icon {
background-image: image-url('emoji.png');
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 588ec1ff3bc..609f33582e1 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -10,6 +10,10 @@
border: 0;
}
+ &.file-holder-bottom-radius {
+ border-radius: 0 0 $border-radius-small $border-radius-small;
+ }
+
&.readme-holder {
margin: $gl-padding 0;
@@ -97,13 +101,13 @@
@for $i from 0 through 5 {
.legend-box-#{$i} {
- background-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%);
+ background-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%);
}
}
@for $i from 1 through 4 {
.legend-box-#{$i + 5} {
- background-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%);
+ background-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%);
}
}
}
@@ -138,7 +142,7 @@
*/
&.blame {
table {
- border: none;
+ border: 0;
margin: 0;
}
@@ -146,65 +150,65 @@
border-bottom: 1px solid $blame-border;
&:last-child {
- border-bottom: none;
+ border-bottom: 0;
}
}
td {
- border-top: none;
- border-bottom: none;
+ border-top: 0;
+ border-bottom: 0;
&:first-child {
- border-left: none;
+ border-left: 0;
}
&:last-child {
- border-right: none;
+ border-right: 0;
}
- }
- td.blame-commit {
- padding: 5px 10px;
- min-width: 400px;
- max-width: 400px;
- background: $gray-light;
- border-left: 3px solid;
+ &.blame-commit {
+ padding: 5px 10px;
+ min-width: 400px;
+ max-width: 400px;
+ background: $gray-light;
+ border-left: 3px solid;
- .commit-row-title {
- display: flex;
+ .commit-row-title {
+ display: flex;
+ }
+
+ .item-title {
+ flex: 1;
+ margin-right: 0.5em;
+ }
}
- .item-title {
- flex: 1;
- margin-right: 0.5em;
+ &.line-numbers {
+ float: none;
+ border-left: 1px solid $blame-line-numbers-border;
+
+ i {
+ float: none;
+ margin-right: 0;
+ }
+ }
+
+ &.lines {
+ padding: 0;
}
}
@for $i from 0 through 5 {
td.blame-commit-age-#{$i} {
- border-left-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%);
+ border-left-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%);
}
}
@for $i from 1 through 4 {
td.blame-commit-age-#{$i + 5} {
- border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%);
+ border-left-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%);
}
}
-
- td.line-numbers {
- float: none;
- border-left: 1px solid $blame-line-numbers-border;
-
- i {
- float: none;
- margin-right: 0;
- }
- }
-
- td.lines {
- padding: 0;
- }
}
&.logs {
@@ -360,6 +364,18 @@ span.idiff {
float: none;
}
}
+
+ @media (max-width: $screen-xs-max) {
+ display: block;
+
+ .file-actions {
+ white-space: normal;
+
+ .btn-group {
+ padding-top: 5px;
+ }
+ }
+ }
}
.is-stl-loading {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index b2847c348eb..cf8165eab5b 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -65,7 +65,7 @@
display: flex;
flex: 1;
-webkit-flex: 1;
- padding-left: 30px;
+ padding-left: 12px;
position: relative;
margin-bottom: 0;
}
@@ -221,10 +221,6 @@
box-shadow: 0 0 4px $search-input-focus-shadow-color;
}
- &.focus .fa-filter {
- color: $common-gray-dark;
- }
-
gl-emoji {
display: inline-block;
font-family: inherit;
@@ -251,13 +247,6 @@
}
}
- .fa-filter {
- position: absolute;
- top: 10px;
- left: 10px;
- color: $gray-darkest;
- }
-
.fa-times {
right: 10px;
color: $gray-darkest;
@@ -266,7 +255,7 @@
.clear-search {
width: 35px;
background-color: $white-light;
- border: none;
+ border: 0;
outline: none;
z-index: 1;
@@ -279,12 +268,6 @@
.filtered-search-box-input-container {
flex: 1;
position: relative;
- // Fix PhantomJS not supporting `flex: 1;` properly.
- // This is important because it can change the expected `e.target` when clicking things in tests.
- // See https://gitlab.com/gitlab-org/gitlab-ce/blob/b54acba8b732688c59fe2f38510c469dc86ee499/spec/features/issues/filtered_search/visual_tokens_spec.rb#L61
- // - With `width: 100%`: `e.target` = `.tokens-container`, https://i.imgur.com/jGq7wbx.png
- // - Without `width: 100%`: `e.target` = `.filtered-search`, https://i.imgur.com/cNI2CyT.png
- width: 100%;
min-width: 0;
}
@@ -322,16 +305,11 @@
color: $gl-text-color;
border-color: $dropdown-input-focus-border;
outline: none;
-
- svg {
- fill: $gl-text-color;
- }
}
svg {
height: 14px;
width: 14px;
- fill: $gl-text-color-secondary;
vertical-align: middle;
}
@@ -435,7 +413,7 @@
.droplab-dropdown .dropdown-menu .filter-dropdown-item {
.btn {
- border: none;
+ border: 0;
width: 100%;
text-align: left;
padding: 8px 16px;
@@ -480,10 +458,10 @@
word-break: break-all;
}
}
-}
-.filter-dropdown-item.droplab-item-active .btn {
- @extend %filter-dropdown-item-btn-hover;
+ &.droplab-item-active .btn {
+ @extend %filter-dropdown-item-btn-hover;
+ }
}
.filter-dropdown-loading {
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index e1b086ebb2b..88ce119ee3a 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -34,8 +34,15 @@
}
}
+ .flash-success {
+ @extend .alert;
+ @extend .alert-success;
+ margin: 0;
+ }
+
.flash-notice,
- .flash-alert {
+ .flash-alert,
+ .flash-success {
border-radius: $border-radius-default;
.container-fluid,
@@ -48,7 +55,8 @@
margin-bottom: 0;
.flash-notice,
- .flash-alert {
+ .flash-alert,
+ .flash-success {
border-radius: 0;
}
}
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 52b87de7a3d..db36e27fa74 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -30,10 +30,6 @@
&.dropdown.open > a {
color: $color-900;
background-color: $color-alternate;
-
- svg {
- fill: currentColor;
- }
}
&.line-separator {
@@ -51,10 +47,6 @@
color: $color-200;
> a {
- svg {
- fill: $color-200;
- }
-
&.header-user-dropdown-toggle {
.header-user-avatar {
border-color: $color-200;
@@ -216,12 +208,9 @@ body {
color: $theme-gray-900;
}
- &.active > a {
+ &.active > a,
+ &.active > a:hover {
color: $white-light;
-
- &:hover {
- color: $white-light;
- }
}
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 22945e935ef..f985a3aea5c 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -10,7 +10,7 @@
z-index: 1000;
margin-bottom: 0;
min-height: $header-height;
- border: none;
+ border: 0;
border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
@@ -129,7 +129,7 @@
margin: 5px 2px 5px -8px;
border-radius: $border-radius-default;
- svg {
+ .tanuki-logo {
@media (min-width: $screen-sm-min) {
margin-right: 8px;
}
@@ -161,14 +161,15 @@
}
}
- .dropdown-bold-header {
+ li.dropdown-bold-header {
color: $gl-text-color-secondary;
font-size: 12px;
+ padding: 0 16px;
}
.navbar-collapse {
flex: 0 0 auto;
- border-top: none;
+ border-top: 0;
padding: 0;
@media (max-width: $screen-xs-max) {
@@ -234,14 +235,8 @@
opacity: 1;
color: $white-light;
- svg {
- fill: currentColor;
- }
-
- &.header-user-dropdown-toggle {
- .header-user-avatar {
- border-color: $white-light;
- }
+ &.header-user-dropdown-toggle .header-user-avatar {
+ border-color: $white-light;
}
}
}
@@ -270,14 +265,6 @@
font-size: 20px;
}
}
-
- &.active > a,
- &.dropdown.open > a {
-
- svg {
- fill: currentColor;
- }
- }
}
}
}
@@ -290,10 +277,6 @@
text-decoration: none;
outline: 0;
color: $white-light;
-
- svg {
- fill: currentColor;
- }
}
> a {
@@ -308,10 +291,6 @@
border-radius: $border-radius-default;
height: 32px;
font-weight: $gl-font-weight-bold;
-
- svg {
- fill: currentColor;
- }
}
&.line-separator {
@@ -402,10 +381,13 @@
.breadcrumbs-list {
display: -webkit-flex;
display: flex;
- flex-wrap: wrap;
margin-bottom: 0;
line-height: 16px;
+ @media (max-width: $screen-xs-max) {
+ flex-wrap: wrap;
+ }
+
> li {
display: flex;
align-items: center;
@@ -413,24 +395,35 @@
padding: 2px 0;
&:not(:last-child) {
- margin-right: 20px;
+ padding-right: 20px;
+
+ &:not(.dropdown) {
+ overflow: hidden;
+ }
}
> a {
font-size: 12px;
color: currentColor;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex: 0 1 auto;
}
}
}
.breadcrumb-item-text {
- @include str-truncated(128px);
text-decoration: inherit;
+
+ @media (max-width: $screen-xs-max) {
+ @include str-truncated(128px);
+ }
}
.breadcrumbs-list-angle {
position: absolute;
- right: -12px;
+ right: 7px;
top: 50%;
color: $gl-text-color-tertiary;
transform: translateY(-50%);
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index c63114f85b4..813a1711ea2 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -1,5 +1,5 @@
.file-content.code {
- border: none;
+ border: 0;
box-shadow: none;
margin: 0;
padding: 0;
@@ -7,7 +7,7 @@
pre {
padding: 10px 0;
- border: none;
+ border: 0;
border-radius: 0;
font-family: $monospace_font;
font-size: $code_font_size;
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index ef864e8f6a9..e2084e8f85f 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -1,15 +1,11 @@
.ci-status-icon-success,
.ci-status-icon-passed {
- color: $green-500;
-
svg {
fill: $green-500;
}
}
.ci-status-icon-failed {
- color: $gl-danger;
-
svg {
fill: $gl-danger;
}
@@ -18,16 +14,12 @@
.ci-status-icon-pending,
.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings {
- color: $orange-500;
-
svg {
fill: $orange-500;
}
}
.ci-status-icon-running {
- color: $blue-400;
-
svg {
fill: $blue-400;
}
@@ -36,8 +28,6 @@
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found {
- color: $gl-text-color;
-
svg {
fill: $gl-text-color;
}
@@ -45,16 +35,12 @@
.ci-status-icon-created,
.ci-status-icon-skipped {
- color: $gray-darkest;
-
svg {
fill: $gray-darkest;
}
}
.ci-status-icon-manual {
- color: $gl-text-color;
-
svg {
fill: $gl-text-color;
}
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 6819fd88b7f..78a8e57ddbb 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -27,6 +27,8 @@
}
svg {
+ fill: currentColor;
+
&.s8 { @include svg-size(8px); }
&.s12 { @include svg-size(12px); }
&.s16 { @include svg-size(16px); }
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 69d19ea2962..cb324ccc440 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -30,10 +30,10 @@ body {
.container {
padding-top: 0;
z-index: 5;
-}
-.container .content {
- margin: 0;
+ .content {
+ margin: 0;
+ }
}
.navless-container {
@@ -82,26 +82,26 @@ body {
transition: background-color 0.15s, border-color 0.15s;
background-color: $orange-500;
border-color: $orange-500;
- }
- .alert-warning + .alert-warning {
- background-color: $orange-600;
- border-color: $orange-600;
- }
+ &:only-of-type {
+ background-color: $orange-500;
+ border-color: $orange-500;
+ }
- .alert-warning + .alert-warning + .alert-warning {
- background-color: $orange-700;
- border-color: $orange-700;
- }
+ + .alert-warning {
+ background-color: $orange-600;
+ border-color: $orange-600;
- .alert-warning + .alert-warning + .alert-warning + .alert-warning {
- background-color: $orange-800;
- border-color: $orange-800;
- }
+ + .alert-warning {
+ background-color: $orange-700;
+ border-color: $orange-700;
- .alert-warning:only-of-type {
- background-color: $orange-500;
- border-color: $orange-500;
+ + .alert-warning {
+ background-color: $orange-800;
+ border-color: $orange-800;
+ }
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index badc7b0eba3..ad3bb0e35d1 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -42,7 +42,7 @@
}
&:last-child {
- border-bottom: none;
+ border-bottom: 0;
&.bottom {
background: $gray-light;
@@ -92,7 +92,7 @@ ul.unstyled-list {
}
ul.unstyled-list > li {
- border-bottom: none;
+ border-bottom: 0;
}
// Generic content list
@@ -178,7 +178,7 @@ ul.content-list {
// When dragging a list item
&.ui-sortable-helper {
- border-bottom: none;
+ border-bottom: 0;
}
&.list-placeholder {
@@ -281,7 +281,58 @@ ul.indent-list {
// Specific styles for tree list
+@keyframes spin-avatar {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+.groups-list-tree-container {
+ .has-no-search-results {
+ text-align: center;
+ padding: $gl-padding;
+ font-style: italic;
+ color: $well-light-text-color;
+ }
+
+ > .group-list-tree > .group-row.has-children:first-child {
+ border-top: 0;
+ }
+}
+
.group-list-tree {
+ .avatar-container.content-loading {
+ position: relative;
+
+ > a,
+ > a .avatar {
+ height: 100%;
+ border-radius: 50%;
+ }
+
+ > a {
+ padding: 2px;
+
+ .avatar {
+ border: 2px solid $white-normal;
+
+ &.identicon {
+ line-height: 30px;
+ }
+ }
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ background-color: transparent;
+ border: 2px outset $kdb-border;
+ border-radius: 50%;
+ animation: spin-avatar 3s infinite linear;
+ }
+ }
+
.folder-toggle-wrap {
float: left;
line-height: $list-text-height;
@@ -293,7 +344,7 @@ ul.indent-list {
}
.folder-caret,
- .folder-icon {
+ .item-type-icon {
display: inline-block;
}
@@ -301,11 +352,11 @@ ul.indent-list {
width: 15px;
}
- .folder-icon {
+ .item-type-icon {
width: 20px;
}
- > .group-row:not(.has-subgroups) {
+ > .group-row:not(.has-children) {
.folder-caret .fa {
opacity: 0;
}
@@ -351,12 +402,23 @@ ul.indent-list {
top: 30px;
bottom: 0;
}
+
+ &.being-removed {
+ opacity: 0.5;
+ }
}
}
.group-row {
padding: 0;
- border: none;
+
+ &.has-children {
+ border-top: 0;
+ }
+
+ &:first-child {
+ border-top: 1px solid $white-normal;
+ }
&:last-of-type {
.group-row-contents:not(:hover) {
@@ -379,6 +441,25 @@ ul.indent-list {
.avatar-container > a {
width: 100%;
}
+
+ &.has-more-items {
+ display: block;
+ padding: 20px 10px;
+ }
+ }
+}
+
+ul.group-list-tree {
+ li.group-row {
+ &.has-description {
+ .title {
+ line-height: inherit;
+ }
+ }
+
+ .title {
+ line-height: $list-text-height;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index e3920b5d3d9..5389eb0a5f2 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -57,6 +57,7 @@
.md-header {
.nav-links {
a {
+ width: 100%;
padding-top: 0;
line-height: 19px;
@@ -72,6 +73,28 @@
}
}
+.md-header-tab {
+ @media(max-width: $screen-xs-max) {
+ flex: 1;
+ width: 100%;
+ border-bottom: 1px solid $border-color;
+ text-align: center;
+ }
+}
+
+.md-header-toolbar {
+ margin-left: auto;
+
+ @media(max-width: $screen-xs-max) {
+ flex: none;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ padding-top: $gl-padding-top;
+ padding-bottom: $gl-padding-top;
+ }
+}
+
.referenced-users {
color: $gl-text-color;
padding-top: 10px;
@@ -126,27 +149,35 @@
}
}
-.toolbar-group {
- float: left;
- margin-right: -5px;
- margin-left: $gl-padding;
-
- &:first-child {
- margin-left: 0;
- }
-}
-
.toolbar-btn {
float: left;
- padding: 0 5px;
- color: $gl-text-color-secondary;
+ padding: 0 7px;
background: transparent;
border: 0;
outline: 0;
+ svg {
+ width: 14px;
+ height: 14px;
+ margin-top: 3px;
+ fill: $gl-text-color-secondary;
+ }
+
&:hover,
&:focus {
- color: $gl-link-color;
+ svg {
+ fill: $gl-link-color;
+ }
+ }
+}
+
+.toolbar-fullscreen-btn {
+ margin-left: $gl-padding;
+ margin-right: -5px;
+
+ @media(max-width: $screen-xs-max) {
+ margin-left: 0;
+ margin-right: 0;
}
}
@@ -173,21 +204,8 @@
ul > li {
white-space: nowrap;
}
-}
-
-@media(max-width: $screen-xs-max) {
- .atwho-view-ul {
- width: 350px;
- }
-
- .atwho-view ul li {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-}
-// TODO: fallback to global style
-.atwho-view {
+ // TODO: fallback to global style
.atwho-view-ul {
padding: 8px 1px;
@@ -220,3 +238,14 @@
}
}
}
+
+@media(max-width: $screen-xs-max) {
+ .atwho-view-ul {
+ width: 350px;
+ }
+
+ .atwho-view ul li {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 2fee2164190..e12b5aab381 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -36,7 +36,7 @@
margin: 0;
&:last-child {
- border-bottom: none;
+ border-bottom: 0;
}
&.active {
@@ -130,14 +130,6 @@
background-color: $color-light;
color: $color-dark;
border-color: $color-dark;
-
- svg {
- fill: $color-dark;
- }
- }
-
- svg {
- fill: $color-main;
}
}
@@ -180,3 +172,31 @@
display: none;
}
}
+
+@mixin triangle($color, $border-color, $size, $border-size) {
+ &::before,
+ &::after {
+ bottom: 100%;
+ left: 50%;
+ border: solid transparent;
+ content: '';
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ }
+
+ &::before {
+ border-color: transparent;
+ border-bottom-color: $border-color;
+ border-width: ($size + $border-size);
+ margin-left: -($size + $border-size);
+ }
+
+ &::after {
+ border-color: transparent;
+ border-bottom-color: $color;
+ border-width: $size;
+ margin-left: -$size;
+ }
+}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 1cebd02df48..5c9838c1029 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -7,6 +7,7 @@
}
.modal-body {
+ background-color: $modal-body-bg;
position: relative;
padding: #{3 * $grid-size} #{2 * $grid-size};
@@ -42,3 +43,8 @@ body.modal-open {
width: 98%;
}
}
+
+.modal.popup-dialog {
+ display: block;
+}
+
diff --git a/app/assets/stylesheets/framework/new-nav.scss b/app/assets/stylesheets/framework/new-nav.scss
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/app/assets/stylesheets/framework/new-nav.scss
+++ /dev/null
diff --git a/app/assets/stylesheets/framework/new-sidebar.scss b/app/assets/stylesheets/framework/new-sidebar.scss
deleted file mode 100644
index caf4c7a40b1..00000000000
--- a/app/assets/stylesheets/framework/new-sidebar.scss
+++ /dev/null
@@ -1,510 +0,0 @@
-@import "framework/variables";
-@import 'framework/tw_bootstrap_variables';
-@import "bootstrap/variables";
-
-$active-background: rgba(0, 0, 0, .04);
-$active-hover-background: $active-background;
-$active-hover-color: $gl-text-color;
-$inactive-badge-background: rgba(0, 0, 0, .08);
-$hover-background: $white-light;
-$hover-color: $gl-text-color;
-$inactive-color: $gl-text-color-secondary;
-$new-sidebar-width: 220px;
-$new-sidebar-collapsed-width: 50px;
-
-.page-with-new-sidebar {
- @media (min-width: $screen-md-min) {
- padding-left: $new-sidebar-collapsed-width;
- }
-
- @media (min-width: $screen-lg-min) {
- padding-left: $new-sidebar-width;
- }
-
- // Override position: absolute
- .right-sidebar {
- position: fixed;
- height: calc(100% - #{$header-height});
- }
-
- .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
- padding: 10px 0 15px;
- }
-}
-
-.page-with-icon-sidebar {
- @media (min-width: $screen-sm-min) {
- padding-left: $new-sidebar-collapsed-width;
- }
-}
-
-.context-header {
- position: relative;
- margin-right: 2px;
-
- a {
- font-weight: $gl-font-weight-bold;
- display: flex;
- align-items: center;
- padding: 10px 16px 10px 10px;
- color: $gl-text-color;
- }
-
- &:hover,
- a:hover {
- background-color: $hover-background;
- color: $hover-color;
-
- .settings-avatar {
- svg {
- fill: $hover-color;
- }
- }
- }
-
- .avatar-container {
- flex: 0 0 40px;
- background-color: $white-light;
- }
-
- .sidebar-context-title {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-}
-
-.settings-avatar {
- background-color: $white-light;
-
- svg {
- fill: $gl-text-color-secondary;
- margin: auto;
- }
-}
-
-.nav-sidebar {
- position: fixed;
- z-index: 400;
- width: $new-sidebar-width;
- transition: left $sidebar-transition-duration;
- top: $header-height;
- bottom: 0;
- left: 0;
- background-color: $gray-normal;
- box-shadow: inset -2px 0 0 $border-color;
- transform: translate3d(0, 0, 0);
-
- &:not(.sidebar-icons-only) {
- @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
- box-shadow: inset -2px 0 0 $border-color,
- 2px 1px 3px $dropdown-shadow-color;
- }
- }
-
- &.sidebar-icons-only {
- width: auto;
- min-width: $new-sidebar-collapsed-width;
-
- .nav-sidebar-inner-scroll {
- overflow-x: hidden;
- }
-
- .badge:not(.fly-out-badge),
- .sidebar-context-title,
- .nav-item-name {
- display: none;
- }
-
- .sidebar-top-level-items > li > a {
- min-height: 44px;
- }
-
- .fly-out-top-item {
- display: block;
- }
-
- .avatar-container {
- margin-right: 0;
- }
- }
-
- &.nav-sidebar-expanded {
- left: 0;
- }
-
- a {
- transition: none;
- text-decoration: none;
- }
-
- ul {
- padding-left: 0;
- list-style: none;
- }
-
- li {
- white-space: nowrap;
-
- a {
- display: flex;
- align-items: center;
- padding: 12px 16px;
- color: $inactive-color;
- }
-
- svg {
- fill: $inactive-color;
- }
- }
-
- .nav-item-name {
- flex: 1;
- }
-
- li.active {
- > a {
- font-weight: $gl-font-weight-bold;
- }
- }
-
- @media (max-width: $screen-xs-max) {
- left: (-$new-sidebar-width);
- }
-
- .nav-icon-container {
- display: flex;
- margin-right: 8px;
- }
-
- .fly-out-top-item {
- display: none;
- }
-
- svg {
- height: 16px;
- width: 16px;
- }
-}
-
-.nav-sidebar-inner-scroll {
- height: 100%;
- width: 100%;
- overflow: auto;
-
- @media (min-width: $screen-sm-min) {
- overflow: hidden;
- }
-}
-
-.with-performance-bar .nav-sidebar {
- top: $header-height + $performance-bar-height;
-}
-
-.sidebar-sub-level-items {
- display: none;
- padding-bottom: 8px;
-
- > li {
- a {
- padding: 8px 16px 8px 40px;
-
- &:hover,
- &:focus {
- background: $active-hover-background;
- color: $active-hover-color;
- }
- }
-
- &.active {
- a {
- &,
- &:hover,
- &:focus {
- background: $active-background;
- }
- }
- }
- }
-}
-
-.sidebar-top-level-items {
- margin-bottom: 60px;
-
- > li {
- > a {
- @media (min-width: $screen-sm-min) {
- margin-right: 2px;
- }
-
- &:hover {
- color: $gl-text-color;
-
- svg {
- fill: $gl-text-color;
- }
- }
- }
-
- &.is-showing-fly-out {
- > a {
- margin-right: 2px;
- }
-
- .sidebar-sub-level-items {
- @media (min-width: $screen-sm-min) {
- position: fixed;
- top: 0;
- left: 0;
- min-width: 150px;
- margin-top: -1px;
- padding: 4px 1px;
- background-color: $white-light;
- box-shadow: 2px 1px 3px $dropdown-shadow-color;
- border: 1px solid $gray-darker;
- border-left: 0;
- border-radius: 0 3px 3px 0;
-
- &::before {
- content: "";
- position: absolute;
- top: -30px;
- bottom: -30px;
- left: -10px;
- right: -30px;
- z-index: -1;
- }
-
- &.is-above {
- margin-top: 1px;
- }
-
- .divider {
- height: 1px;
- margin: 4px -1px;
- padding: 0;
- background-color: $dropdown-divider-color;
- }
-
- > .active {
- box-shadow: none;
-
- > a {
- background-color: transparent;
- }
- }
-
- a {
- padding: 8px 16px;
- color: $gl-text-color;
-
- &:hover,
- &:focus {
- background-color: $gray-darker;
- }
- }
- }
- }
- }
-
- .badge {
- background-color: $inactive-badge-background;
- color: $inactive-color;
- }
-
- &.active {
- background: $active-background;
-
- > a {
- margin-left: 4px;
- padding-left: 12px;
- }
-
- .badge {
- font-weight: $gl-font-weight-bold;
- }
-
- .sidebar-sub-level-items:not(.is-fly-out-only) {
- display: block;
- }
- }
-
- &.active > a:hover,
- &.is-over > a {
- background-color: $white-light;
- }
- }
-}
-
-
-// Collapsed nav
-
-.toggle-sidebar-button,
-.close-nav-button {
- width: $new-sidebar-width - 2px;
- position: fixed;
- bottom: 0;
- padding: 16px;
- background-color: $gray-normal;
- border: 0;
- border-top: 2px solid $border-color;
- color: $gl-text-color-secondary;
- display: flex;
- align-items: center;
-
- svg {
- fill: $gl-text-color-secondary;
- margin-right: 8px;
- }
-
- .icon-angle-double-right {
- display: none;
- }
-
- &:hover {
- background-color: $border-color;
- color: $gl-text-color;
-
- svg {
- fill: $gl-text-color;
- }
- }
-}
-
-.toggle-sidebar-button {
- @media (max-width: $screen-xs-max) {
- display: none;
- }
-}
-
-
-.sidebar-icons-only {
- .context-header {
- height: 61px;
-
- a {
- padding: 10px 4px;
- }
- }
-
- li a {
- padding: 12px 15px;
- }
-
- .sidebar-top-level-items > li {
- &.active a {
- padding-left: 12px;
- }
-
- .sidebar-sub-level-items {
- &:not(.flyout-list) {
- display: none;
- }
- }
- }
-
- .nav-icon-container {
- margin-right: 0;
- }
-
- .toggle-sidebar-button {
- width: $new-sidebar-collapsed-width - 2px;
- padding: 16px;
-
- .collapse-text,
- .icon-angle-double-left {
- display: none;
- }
-
- .icon-angle-double-right {
- display: block;
- margin: 0;
- }
- }
-}
-
-.fly-out-top-item {
- > a {
- display: flex;
- }
-
- .fly-out-badge {
- margin-left: 8px;
- }
-}
-
-.fly-out-top-item-name {
- flex: 1;
-}
-
-// Mobile nav
-
-.close-nav-button {
- display: none;
-}
-
-.toggle-mobile-nav {
- display: none;
- background-color: transparent;
- border: 0;
- padding: 6px 16px;
- margin: 0 0 0 -15px;
- height: 46px;
-
- i {
- font-size: 20px;
- color: $gl-text-color-secondary;
- }
-
- @media (max-width: $screen-xs-max) {
- display: flex;
- align-items: center;
-
- i {
- font-size: 18px;
- }
- }
-
- @media (max-width: $screen-xs-max) {
- + .breadcrumbs-links {
- padding-left: 17px;
- border-left: 1px solid $gl-text-color-quaternary;
- }
- }
-}
-
-@media (max-width: $screen-xs-max) {
- .close-nav-button {
- display: flex;
- }
-}
-
-.mobile-overlay {
- display: none;
-
- &.mobile-nav-open {
- display: block;
- position: fixed;
- background-color: $black-transparent;
- height: 100%;
- width: 100%;
- z-index: 300;
- }
-}
-
-
-// Make issue boards full-height now that sub-nav is gone
-
-.boards-list {
- height: calc(100vh - #{$header-height});
-
- @media (min-width: $screen-sm-min) {
- height: 475px; // Needed for PhantomJS
- // scss-lint:disable DuplicateProperty
- height: calc(100vh - 180px);
- // scss-lint:enable DuplicateProperty
- }
-}
-
-.with-performance-bar .boards-list {
- height: calc(100vh - #{$header-height} - #{$performance-bar-height});
-}
diff --git a/app/assets/stylesheets/framework/popup.scss b/app/assets/stylesheets/framework/popup.scss
new file mode 100644
index 00000000000..5c76205095f
--- /dev/null
+++ b/app/assets/stylesheets/framework/popup.scss
@@ -0,0 +1,15 @@
+.popup {
+ @include triangle(
+ $gray-lighter,
+ $gray-darker,
+ $popup-triangle-size,
+ $popup-triangle-border-size
+ );
+
+ padding: $gl-padding;
+ background-color: $gray-lighter;
+ border: 1px solid $gray-darker;
+ border-radius: $border-radius-default;
+ box-shadow: 0 5px 8px $popup-box-shadow-color;
+ position: relative;
+}
diff --git a/app/assets/stylesheets/framework/responsive-tables.scss b/app/assets/stylesheets/framework/responsive-tables.scss
deleted file mode 100644
index 8e653c443cf..00000000000
--- a/app/assets/stylesheets/framework/responsive-tables.scss
+++ /dev/null
@@ -1,137 +0,0 @@
-@mixin flex-max-width($max) {
- flex: 0 0 #{$max + '%'};
- max-width: #{$max + '%'};
-}
-
-.gl-responsive-table-row {
- margin-top: 10px;
- border: 1px solid $border-color;
-
- @media (min-width: $screen-md-min) {
- padding: 15px 0;
- margin: 0;
- display: flex;
- align-items: center;
- border: none;
- border-bottom: 1px solid $white-normal;
- }
-
- .table-section {
- white-space: nowrap;
-
- $section-widths: 10 15 20 25 30 40;
- @each $width in $section-widths {
- &.section-#{$width} {
- flex: 0 0 #{$width + '%'};
-
- @media (min-width: $screen-md-min) {
- max-width: #{$width + '%'};
- }
- }
- }
-
- &:not(.table-button-footer) {
- @media (max-width: $screen-sm-max) {
- display: flex;
- align-self: stretch;
- padding: 10px;
- align-items: center;
- min-height: 62px;
-
- &:not(:first-of-type) {
- border-top: 1px solid $white-normal;
- }
- }
- }
-
- &.section-wrap {
- white-space: normal;
-
- @media (max-width: $screen-sm-max) {
- flex-wrap: wrap;
- }
- }
- }
-}
-
-
-.table-button-footer {
- @media (min-width: $screen-md-min) {
- text-align: right;
- }
-
- @media (max-width: $screen-sm-max) {
- background-color: $gray-normal;
- align-self: stretch;
- border-top: 1px solid $border-color;
-
- .table-action-buttons {
- padding: 10px 5px;
- display: flex;
-
- .btn {
- border-radius: 3px;
- }
-
- > .btn-group,
- > .external-url,
- > .btn {
- flex: 1 1 28px;
- margin: 0 5px;
- }
-
- .dropdown-new {
- width: 100%;
- }
-
- .dropdown-menu {
- min-width: initial;
- }
- }
- }
-}
-
-.table-row-header {
- font-size: 13px;
-
- @media (max-width: $screen-sm-max) {
- display: none;
- }
-}
-
-.table-mobile-header {
- @include flex-max-width(40);
- color: $gl-text-color-secondary;
- text-align: left;
-
- @media (min-width: $screen-md-min) {
- display: none;
- }
-}
-
-.table-mobile-content {
- @media (max-width: $screen-sm-max) {
- @include flex-max-width(60);
- text-align: right;
- }
-}
-
-.flex-truncate-parent {
- display: flex;
-}
-
-.flex-truncate-child {
- flex: 1;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-
- @media (min-width: $screen-md-min) {
- flex: 0 0 90%;
- }
-
- .avatar {
- float: none;
- margin-right: 4px;
- }
-}
diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
new file mode 100644
index 00000000000..7829d722560
--- /dev/null
+++ b/app/assets/stylesheets/framework/responsive_tables.scss
@@ -0,0 +1,165 @@
+@mixin flex-max-width($max) {
+ flex: 0 0 #{$max + '%'};
+ max-width: #{$max + '%'};
+}
+
+.gl-responsive-table-row-layout {
+ width: 100%;
+
+ @media (min-width: $screen-md-min) {
+ display: flex;
+ align-items: center;
+
+ & > &:not(:first-child) {
+ margin-top: $gl-padding;
+ }
+ }
+}
+
+.gl-responsive-table-row {
+ @extend .gl-responsive-table-row-layout;
+ margin-top: 10px;
+ border: 1px solid $border-color;
+
+ @media (min-width: $screen-md-min) {
+ margin: 0;
+ padding: $gl-padding 0;
+ border: 0;
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $white-normal;
+ }
+ }
+}
+
+.gl-responsive-table-row-col-span {
+ flex-wrap: wrap;
+}
+
+.table-section {
+ white-space: nowrap;
+
+ $section-widths: 10 15 20 25 30 40 100;
+ @each $width in $section-widths {
+ &.section-#{$width} {
+ flex: 0 0 #{$width + '%'};
+
+ @media (min-width: $screen-md-min) {
+ max-width: #{$width + '%'};
+ }
+ }
+ }
+
+ @media (max-width: $screen-sm-max) {
+ display: flex;
+ align-self: stretch;
+ padding: 10px;
+ align-items: center;
+ min-height: 62px;
+
+ &:not(:first-child) {
+ border-top: 1px solid $white-normal;
+ }
+ }
+
+ &.section-wrap {
+ white-space: normal;
+
+ @media (max-width: $screen-sm-max) {
+ flex-wrap: wrap;
+ }
+ }
+
+ &.section-align-top {
+ align-self: flex-start;
+ }
+}
+
+.table-button-footer {
+ @media (min-width: $screen-md-min) {
+ text-align: right;
+ }
+
+ @media (max-width: $screen-sm-max) {
+ display: block;
+ align-self: stretch;
+ min-height: 0;
+ background-color: $gray-normal;
+ border-top: 1px solid $border-color;
+
+ .table-action-buttons {
+ display: flex;
+
+ .btn {
+ border-radius: 3px;
+ }
+
+ > .btn-group,
+ > .external-url,
+ > .btn {
+ flex: 1 1 28px;
+
+ &:not(:first-child) {
+ margin-left: 5px;
+ }
+
+ &:not(:last-child) {
+ margin-right: 5px;
+ }
+ }
+
+ .dropdown-new {
+ width: 100%;
+ }
+
+ .dropdown-menu {
+ min-width: initial;
+ }
+ }
+ }
+}
+
+.table-row-header {
+ font-size: 13px;
+
+ @media (max-width: $screen-sm-max) {
+ display: none;
+ }
+}
+
+.table-mobile-header {
+ @include flex-max-width(40);
+ color: $gl-text-color-secondary;
+ text-align: left;
+
+ @media (min-width: $screen-md-min) {
+ display: none;
+ }
+}
+
+.table-mobile-content {
+ @media (max-width: $screen-sm-max) {
+ @include flex-max-width(60);
+ text-align: right;
+ }
+}
+
+.flex-truncate-parent {
+ display: flex;
+}
+
+.flex-truncate-child {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ @media (min-width: $screen-md-min) {
+ flex: 0 0 90%;
+ }
+
+ .avatar {
+ float: none;
+ margin-right: 4px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
index 5c96b3b78e7..8498b37abe4 100644
--- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss
+++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss
@@ -6,6 +6,7 @@
margin: 0;
list-style: none;
height: auto;
+ border-bottom: 1px solid $border-color;
li {
display: flex;
@@ -24,6 +25,7 @@
&:focus {
text-decoration: none;
color: $black;
+ border-bottom: 2px solid $gray-darkest;
.badge {
color: $black;
@@ -61,7 +63,7 @@
.nav-links {
margin-bottom: 0;
- border-bottom: none;
+ border-bottom: 0;
float: left;
&.wide {
@@ -333,7 +335,7 @@
border-bottom: 1px solid $border-color;
.nav-links {
- border-bottom: none;
+ border-bottom: 0;
}
}
}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 50f1445bc2e..bb70b270299 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -17,7 +17,7 @@
.select2-arrow {
background-image: none;
background-color: transparent;
- border: none;
+ border: 0;
padding-top: 12px;
padding-right: 20px;
font-size: 10px;
@@ -61,6 +61,11 @@
border: 1px solid $dropdown-border-color;
min-width: 175px;
color: $gl-text-color;
+ z-index: 999;
+}
+
+.select2-drop-mask {
+ z-index: 998;
}
.select2-drop.select2-drop-above.select2-drop-active {
@@ -68,11 +73,6 @@
margin-top: -6px;
}
-.select2-results li.select2-result-with-children > .select2-result-label {
- font-weight: $gl-font-weight-bold;
- color: $gl-text-color;
-}
-
.select2-container-active {
.select2-choice,
.select2-choices {
@@ -139,28 +139,28 @@
.select2-drop-auto-width & {
padding: 15px 15px 5px;
}
-}
-.select2-search input {
- padding: 2px 25px 2px 5px;
- background: $white-light image-url('select2.png');
- background-repeat: no-repeat;
- background-position: right 0 bottom 6px;
- border: 1px solid $input-border;
- border-radius: $border-radius-default;
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-
- &:focus {
- border-color: $input-border-focus;
- }
-}
+ input {
+ padding: 2px 25px 2px 5px;
+ background: $white-light image-url('select2.png');
+ background-repeat: no-repeat;
+ background-position: right 0 bottom 6px;
+ border: 1px solid $input-border;
+ border-radius: $border-radius-default;
+ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-.select2-search input.select2-active {
- background-color: $white-light;
- background-image: image-url('select2-spinner.gif') !important;
- background-repeat: no-repeat;
- background-position: right 5px center !important;
- background-size: 16px 16px !important;
+ &:focus {
+ border-color: $input-border-focus;
+ }
+
+ &.select2-active {
+ background-color: $white-light;
+ background-image: image-url('select2-spinner.gif') !important;
+ background-repeat: no-repeat;
+ background-position: right 5px center !important;
+ background-size: 16px 16px !important;
+ }
+ }
}
.select2-results {
@@ -192,6 +192,11 @@
.select2-result {
padding: 0 1px;
}
+
+ li.select2-result-with-children > .select2-result-label {
+ font-weight: $gl-font-weight-bold;
+ color: $gl-text-color;
+ }
}
.ajax-users-select {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index ef58382ba41..792981fdc48 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -9,7 +9,7 @@
&.container-blank {
background: none;
padding: 0;
- border: none;
+ border: 0;
}
}
}
@@ -43,11 +43,13 @@
}
.sidebar-collapsed-icon {
- cursor: pointer;
-
.btn {
background-color: $gray-light;
}
+
+ &:not(.disabled) {
+ cursor: pointer;
+ }
}
}
@@ -55,6 +57,10 @@
padding-right: 0;
z-index: 300;
+ .btn-sidebar-action {
+ display: inline-flex;
+ }
+
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
@@ -111,7 +117,7 @@
}
.block:last-of-type {
- border: none;
+ border: 0;
}
}
@@ -136,3 +142,18 @@
.issuable-sidebar {
@include new-style-dropdown;
}
+
+.pikaday-container {
+ .pika-single {
+ margin-top: 2px;
+ width: 250px;
+ }
+
+ .dropdown-menu-toggle {
+ line-height: 20px;
+ }
+}
+
+.sidebar-collapsed-icon .sidebar-collapsed-value {
+ font-size: 12px;
+}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 4dd31bf28cd..5bde96caf42 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -33,7 +33,7 @@ table {
th {
background-color: $gray-light;
font-weight: $gl-font-weight-normal;
- border-bottom: none;
+ border-bottom: 0;
&.wide {
width: 55%;
diff --git a/app/assets/stylesheets/framework/tabs.scss b/app/assets/stylesheets/framework/tabs.scss
new file mode 100644
index 00000000000..c8ba14b7066
--- /dev/null
+++ b/app/assets/stylesheets/framework/tabs.scss
@@ -0,0 +1,35 @@
+.gitlab-tabs {
+ background: $gray-light;
+ border: 1px solid $border-color;
+
+ li {
+ width: 50%;
+
+ &:not(:last-child) {
+ border-right: 1px solid $border-color;
+ }
+
+ &.active {
+ background: $white-light;
+ }
+
+ a {
+ width: 100%;
+ text-align: center;
+ }
+ }
+}
+
+.gitlab-tab-content {
+ border: 1px solid $border-color;
+ border-top: 0;
+ margin-bottom: $gl-padding;
+
+ .tab-pane {
+ padding: $gl-padding;
+
+ &.no-padding {
+ padding: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index f718ec4bcad..373f35e71d8 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -21,7 +21,7 @@
}
&.text-file .diff-file {
- border-bottom: none;
+ border-bottom: 0;
}
}
@@ -66,5 +66,5 @@
.discussion .timeline-entry {
margin: 0;
- border-right: none;
+ border-right: 0;
}
diff --git a/app/assets/stylesheets/framework/tooltips.scss b/app/assets/stylesheets/framework/tooltips.scss
index 93baf73cb78..98f28987a82 100644
--- a/app/assets/stylesheets/framework/tooltips.scss
+++ b/app/assets/stylesheets/framework/tooltips.scss
@@ -3,5 +3,5 @@
border-radius: $border-radius-default;
line-height: 16px;
font-weight: $gl-font-weight-normal;
- padding: $gl-btn-padding;
+ padding: 8px;
}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index d5c6ddbb4a5..1c6e2bf3074 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -195,33 +195,6 @@ summary {
}
}
-// Typography =================================================================
-
-.text-primary,
-.text-primary:hover {
- color: $brand-primary;
-}
-
-.text-success,
-.text-success:hover {
- color: $brand-success;
-}
-
-.text-danger,
-.text-danger:hover {
- color: $brand-danger;
-}
-
-.text-warning,
-.text-warning:hover {
- color: $brand-warning;
-}
-
-.text-info,
-.text-info:hover {
- color: $brand-info;
-}
-
// Prevent datetimes on tooltips to break into two lines
.local-timeago {
white-space: nowrap;
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 3ea77eb7a43..a23131e0818 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -164,3 +164,36 @@ $pre-border-color: $border-color;
$table-bg-accent: $gray-light;
$zindex-popover: 900;
+
+//== Modals
+//
+//##
+
+//** Padding applied to the modal body
+$modal-inner-padding: $gl-padding;
+
+//** Padding applied to the modal title
+$modal-title-padding: $gl-padding;
+//** Modal title line-height
+// $modal-title-line-height: $line-height-base
+
+//** Background color of modal content area
+$modal-content-bg: $gray-light;
+$modal-body-bg: $white-light;
+//** Modal content border color
+// $modal-content-border-color: rgba(0,0,0,.2)
+//** Modal content border color **for IE8**
+// $modal-content-fallback-border-color: #999
+
+//** Modal backdrop background color
+// $modal-backdrop-bg: #000
+//** Modal backdrop opacity
+// $modal-backdrop-opacity: .5
+//** Modal header border color
+// $modal-header-border-color: #e5e5e5
+//** Modal footer border color
+// $modal-footer-border-color: $modal-header-border-color
+
+// $modal-lg: 900px
+// $modal-md: 600px
+// $modal-sm: 300px
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 3c0b4c82d19..0817cce114c 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -167,7 +167,7 @@
&.plain-readme {
background: none;
- border: none;
+ border: 0;
padding: 0;
margin: 0;
font-size: 14px;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index bbbd16322eb..cb2a237f574 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -9,6 +9,8 @@ $sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px;
$default-transition-duration: .15s;
$right-sidebar-transition-duration: .3s;
+$contextual-sidebar-width: 220px;
+$contextual-sidebar-collapsed-width: 50px;
/*
* Color schema
@@ -161,7 +163,7 @@ $gl-text-color: #2e2e2e;
$gl-text-color-secondary: #707070;
$gl-text-color-tertiary: #949494;
$gl-text-color-quaternary: #d6d6d6;
-$gl-text-color-inverted: rgba(255, 255, 255, 1.0);
+$gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-green: $green-600;
$gl-text-green-hover: $green-700;
@@ -233,6 +235,7 @@ $container-text-max-width: 540px;
$gl-avatar-size: 40px;
$error-exclamation-point: $red-500;
$border-radius-default: 4px;
+$border-radius-small: 2px;
$settings-icon-size: 18px;
$provider-btn-not-active-color: $blue-500;
$link-underline-blue: $blue-500;
@@ -358,6 +361,13 @@ $filtered-search-term-shadow-color: rgba(0, 0, 0, 0.09);
$dropdown-hover-color: $blue-400;
/*
+* Contextual Sidebar
+*/
+$link-active-background: rgba(0, 0, 0, .04);
+$link-hover-background: rgba(0, 0, 0, .06);
+$inactive-badge-background: rgba(0, 0, 0, .08);
+
+/*
* Buttons
*/
$btn-active-gray: #ececec;
@@ -403,7 +413,6 @@ $note-targe3-inside: #ffffd3;
$note-line2-border: #ddd;
$note-icon-gutter-width: 55px;
-
/*
* Zen
*/
@@ -477,8 +486,8 @@ $callout-success-color: $green-700;
/*
* Commit Page
*/
-$commit-max-width-marker-color: rgba(0, 0, 0, 0.0);
-$commit-message-text-area-bg: rgba(0, 0, 0, 0.0);
+$commit-max-width-marker-color: rgba(0, 0, 0, 0);
+$commit-message-text-area-bg: rgba(0, 0, 0, 0);
/*
* Common
@@ -699,14 +708,6 @@ $perf-bar-bucket-color: #ccc;
$perf-bar-bucket-box-shadow-from: rgba($white-light, .2);
$perf-bar-bucket-box-shadow-to: rgba($black, .25);
-
-/*
-Project Templates Icons
-*/
-$rails: #c00;
-$node: #353535;
-$java: #70ad51;
-
/*
Issuable warning
*/
@@ -718,3 +719,10 @@ Image Commenting cursor
*/
$image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 30;
+
+/*
+Popup
+*/
+$popup-triangle-size: 15px;
+$popup-triangle-border-size: 1px;
+$popup-box-shadow-color: rgba(90, 90, 90, 0.05);
diff --git a/app/assets/stylesheets/framework/vue_transitions.scss b/app/assets/stylesheets/framework/vue_transitions.scss
new file mode 100644
index 00000000000..e07a177e153
--- /dev/null
+++ b/app/assets/stylesheets/framework/vue_transitions.scss
@@ -0,0 +1,9 @@
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity $sidebar-transition-duration $general-hover-transition-curve;
+}
+
+.fade-enter,
+.fade-leave-to {
+ opacity: 0;
+}
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index 5f9756bf58a..68824ff8418 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -52,6 +52,37 @@
.label.label-gray {
background-color: $well-expand-item;
}
+
+ .branches {
+ display: inline;
+ }
+
+ .branch-link {
+ margin-bottom: 2px;
+ }
+
+ .limit-box {
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ background-color: $red-100;
+ border-radius: $border-radius-default;
+ text-align: center;
+
+ &:hover {
+ background-color: $red-200;
+ }
+
+ .limit-icon {
+ margin: 0 8px;
+ }
+
+ .limit-message {
+ line-height: 16px;
+ margin-right: 8px;
+ font-size: 12px;
+ }
+ }
}
.light-well {
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 0c226ff7598..dbd3144b9b4 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -9,7 +9,7 @@
z-index: 1031;
textarea {
- border: none;
+ border: 0;
box-shadow: none;
border-radius: 0;
color: $black;
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 65b140cd7f8..c3d8f0c61a2 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -217,13 +217,31 @@ $white-gc-bg: #eaf2f5;
.cp { color: $white-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $white-c1; font-style: italic; }
.cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
- .gd { color: $white-gd; background-color: $white-gd-bg; }
- .gd .x { color: $white-gd-x; background-color: $white-gd-x-bg; }
+
+ .gd {
+ color: $white-gd;
+ background-color: $white-gd-bg;
+
+ .x {
+ color: $white-gd-x;
+ background-color: $white-gd-x-bg;
+ }
+ }
+
.ge { font-style: italic; }
.gr { color: $white-gr; }
.gh { color: $white-gh; }
- .gi { color: $white-gi; background-color: $white-gi-bg; }
- .gi .x { color: $white-gi-x; background-color: $white-gi-x-bg; }
+
+ .gi {
+ color: $white-gi;
+ background-color: $white-gi-bg;
+
+ .x {
+ color: $white-gi-x;
+ background-color: $white-gi-x-bg;
+ }
+ }
+
.go { color: $white-go; }
.gp { color: $white-gp; }
.gs { font-weight: $gl-font-weight-bold; }
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index fbe538ad1d7..658ac26fca9 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -158,13 +158,31 @@ span.highlight_word {
.cp { color: $highlighted-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $highlighted-c1; font-style: italic; }
.cs { color: $highlighted-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
-.gd { color: $highlighted-gd; background-color: $highlighted-gd-bg; }
-.gd .x { color: $highlighted-gd; background-color: $highlighted-gd-x-bg; }
+
+.gd {
+ color: $highlighted-gd;
+ background-color: $highlighted-gd-bg;
+
+ .x {
+ color: $highlighted-gd;
+ background-color: $highlighted-gd-x-bg;
+ }
+}
+
.ge { font-style: italic; }
.gr { color: $highlighted-gr; }
.gh { color: $highlighted-gh; }
-.gi { color: $highlighted-gi; background-color: $highlighted-gi-bg; }
-.gi .x { color: $highlighted-gi; background-color: $highlighted-gi-x-bg; }
+
+.gi {
+ color: $highlighted-gi;
+ background-color: $highlighted-gi-bg;
+
+ .x {
+ color: $highlighted-gi;
+ background-color: $highlighted-gi-x-bg;
+ }
+}
+
.go { color: $highlighted-go; }
.gp { color: $highlighted-gp; }
.gs { font-weight: $gl-font-weight-bold; }
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index ca61f7a30c3..3683afa07de 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -72,7 +72,7 @@
}
.boards-list {
- height: calc(100vh - 152px);
+ height: calc(100vh - 105px);
width: 100%;
padding-top: 25px;
padding-bottom: 25px;
@@ -81,11 +81,12 @@
overflow-x: scroll;
white-space: nowrap;
- @media (min-width: $screen-sm-min) {
- height: 475px; // Needed for PhantomJS
- // scss-lint:disable DuplicateProperty
- height: calc(100vh - 222px);
- // scss-lint:enable DuplicateProperty
+ @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ height: calc(100vh - 90px);
+ }
+
+ @media (min-width: $screen-md-min) {
+ height: calc(100vh - 160px);
min-height: 475px;
}
}
@@ -414,7 +415,7 @@
margin: 5px;
}
-.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar {
+.page-with-contextual-sidebar.page-with-sidebar .issue-boards-sidebar {
.issuable-sidebar-header {
position: relative;
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 50ec5110bf1..f139f4ab650 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -48,7 +48,8 @@
overflow-x: auto;
font-size: 12px;
border-radius: 0;
- border: none;
+ border: 0;
+ padding: $grid-size;
.bash {
display: block;
@@ -57,29 +58,28 @@
.top-bar {
height: 35px;
- display: flex;
- justify-content: flex-end;
background: $gray-light;
border: 1px solid $border-color;
color: $gl-text-color;
position: sticky;
position: -webkit-sticky;
top: $header-height;
+ padding: $grid-size;
&.affix {
top: $header-height;
- }
- // with sidebar
- &.affix.sidebar-expanded {
- right: 306px;
- left: 16px;
- }
+ // with sidebar
+ &.sidebar-expanded {
+ right: 306px;
+ left: 16px;
+ }
- // without sidebar
- &.affix.sidebar-collapsed {
- right: 16px;
- left: 16px;
+ // without sidebar
+ &.sidebar-collapsed {
+ right: 16px;
+ left: 16px;
+ }
}
&.affix-top {
@@ -90,9 +90,6 @@
}
.truncated-info {
- margin: 0 auto;
- align-self: center;
-
.truncated-info-size {
margin: 0 5px;
}
@@ -118,7 +115,11 @@
.controllers-buttons {
color: $gl-text-color;
- margin: 0 10px;
+ margin: 0 $grid-size;
+
+ &:last-child {
+ margin-right: 0;
+ }
}
.btn-scroll.animate {
@@ -333,8 +334,10 @@
svg {
position: relative;
- top: 2px;
+ top: 3px;
margin-right: 3px;
+ width: 14px;
+ height: 14px;
}
}
@@ -348,9 +351,10 @@
svg {
position: relative;
- top: 2px;
+ top: 3px;
margin-right: 3px;
- height: 13px;
+ height: 14px;
+ width: 14px;
}
a {
@@ -369,7 +373,7 @@
.build-job {
position: relative;
- .fa-arrow-right {
+ .icon-arrow-right {
position: absolute;
left: 15px;
top: 20px;
@@ -379,7 +383,7 @@
&.active {
font-weight: $gl-font-weight-bold;
- .fa-arrow-right {
+ .icon-arrow-right {
display: block;
}
}
@@ -392,8 +396,7 @@
background-color: $row-hover;
}
- .fa-refresh {
- font-size: 13px;
+ .icon-retry {
margin-left: 3px;
}
}
diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
index bf6a48889bf..fbe1f3081a0 100644
--- a/app/assets/stylesheets/pages/ci_projects.scss
+++ b/app/assets/stylesheets/pages/ci_projects.scss
@@ -36,7 +36,7 @@
pre.commit-message {
background: none;
padding: 0;
- border: none;
+ border: 0;
margin: 20px 0;
border-radius: 0;
}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 5538e46a6c4..e5b9e1f2de6 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -2,8 +2,9 @@
.clipboard-addon {
background-color: $white-light;
}
+}
- .alert-block {
- margin-bottom: 20px;
- }
+.cluster-applications-table {
+ // Wait for the Vue to kick-in and render the applications block
+ min-height: 302px;
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 994707422bb..b1850be8a5f 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -1,6 +1,6 @@
.commit-description {
background: none;
- border: none;
+ border: 0;
padding: 0;
margin-top: 10px;
word-break: normal;
@@ -54,12 +54,15 @@
.mr-widget-pipeline-graph {
display: inline-block;
vertical-align: middle;
- margin-right: 4px;
.stage-cell .stage-container {
margin: 3px 3px 3px 0;
}
+ .stage-container:last-child {
+ margin-right: 0;
+ }
+
.dropdown-menu {
margin-top: 11px;
}
@@ -244,7 +247,7 @@
word-break: normal;
pre {
- border: none;
+ border: 0;
background: inherit;
padding: 0;
margin: 0;
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 2a92673d9fa..292e0ad394b 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -22,6 +22,11 @@
}
}
}
+
+ svg {
+ width: 136px;
+ height: 136px;
+ }
}
.col-headers {
@@ -75,7 +80,7 @@
.panel {
.content-block {
padding: 24px 0;
- border-bottom: none;
+ border-bottom: 0;
position: relative;
@media (max-width: $screen-xs-max) {
@@ -155,11 +160,6 @@
}
}
- .landing svg {
- width: 136px;
- height: 136px;
- }
-
.fa-spinner {
font-size: 28px;
position: relative;
@@ -222,11 +222,11 @@
}
&:first-child {
- border-top: none;
+ border-top: 0;
}
&:last-child {
- border-bottom: none;
+ border-bottom: 0;
}
.stage-nav-item-cell {
@@ -290,7 +290,7 @@
border-bottom: 1px solid $gray-darker;
&:last-child {
- border-bottom: none;
+ border-bottom: 0;
margin-bottom: 0;
}
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 3d9eff35583..538e50ee306 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -3,6 +3,7 @@
border-bottom: 1px solid $border-color;
color: $gl-text-color;
line-height: 34px;
+ display: flex;
a {
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index ffb5fc94475..848d7f144dc 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -47,7 +47,7 @@
table {
width: 100%;
font-family: $monospace_font;
- border: none;
+ border: 0;
border-collapse: separate;
margin: 0;
padding: 0;
@@ -105,7 +105,7 @@
.new_line {
@include user-select(none);
margin: 0;
- border: none;
+ border: 0;
padding: 0 5px;
border-right: 1px solid;
text-align: right;
@@ -133,7 +133,7 @@
display: block;
margin: 0;
padding: 0 1.5em;
- border: none;
+ border: 0;
position: relative;
&.parallel {
@@ -359,7 +359,7 @@
cursor: pointer;
&:first-child {
- border-left: none;
+ border-left: 0;
}
&:hover {
@@ -380,15 +380,15 @@
}
}
}
+
+ .line_content {
+ white-space: pre-wrap;
+ }
}
.file-content .diff-file {
margin: 0;
- border: none;
-}
-
-.diff-file .line_content {
- white-space: pre-wrap;
+ border: 0;
}
.diff-wrap-lines .line_content {
@@ -400,7 +400,7 @@
}
.files-changed {
- border-bottom: none;
+ border-bottom: 0;
}
.diff-stats-summary-toggler {
@@ -628,21 +628,46 @@
}
.diff-file-changes {
- width: 450px;
+ max-width: 560px;
+ width: 100%;
z-index: 150;
@media (min-width: $screen-sm-min) {
left: $gl-padding;
}
- a {
+ .diff-changed-file {
+ display: flex;
padding-top: 8px;
padding-bottom: 8px;
+ min-width: 0;
}
- .diff-changed-file {
+ .diff-file-changed-icon {
+ margin-top: 2px;
+ }
+
+ .diff-changed-file-content {
display: flex;
- align-items: center;
+ flex-direction: column;
+ min-width: 0;
+ }
+
+ .diff-changed-file-name,
+ .diff-changed-file-path {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .diff-changed-file-path {
+ direction: rtl;
+ color: $gl-text-color-tertiary;
+ }
+
+ .diff-changed-stats {
+ margin-left: auto;
+ white-space: nowrap;
}
}
@@ -707,11 +732,11 @@
.frame.click-to-comment {
position: relative;
- cursor: url(icon_image_comment.svg)
+ cursor: image-url('icon_image_comment.svg')
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
// Retina cursor
- cursor: -webkit-image-set(url(icon_image_comment.svg) 1x, url(icon_image_comment@2x.svg) 2x)
+ cursor: -webkit-image-set(image-url('icon_image_comment.svg') 1x, image-url('icon_image_comment@2x.svg') 2x)
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
.comment-indicator {
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index d3cd4d507de..c586dab4cf2 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -3,13 +3,13 @@
border-top: 1px solid $border-color;
border-right: 1px solid $border-color;
border-left: 1px solid $border-color;
- border-bottom: none;
- border-radius: 2px;
+ border-bottom: 0;
+ border-radius: $border-radius-small $border-radius-small 0 0;
background: $gray-normal;
}
#editor {
- border: none;
+ border: 0;
border-radius: 0;
height: 500px;
margin: 0;
@@ -171,7 +171,7 @@
width: 100%;
margin: 5px 0;
padding: 0;
- border-left: none;
+ border-left: 0;
}
}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 3b5e411e2c5..b0795353ec1 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -117,7 +117,7 @@
}
.no-btn {
- border: none;
+ border: 0;
background: none;
outline: none;
width: 100%;
@@ -133,12 +133,11 @@
}
.folder-row {
- padding: 15px 0;
- border-bottom: 1px solid $white-normal;
+ border-left: 0;
+ border-right: 0;
- @media (max-width: $screen-sm-max) {
- border-top: 1px solid $white-normal;
- margin-top: 10px;
+ @media (min-width: $screen-sm-max) {
+ border-top: 0;
}
}
@@ -174,7 +173,7 @@
.prometheus-graph-overlay {
fill: none;
- opacity: 0.0;
+ opacity: 0;
pointer-events: all;
}
@@ -256,29 +255,6 @@
width: 100%;
padding: 0;
padding-bottom: 100%;
-}
-
-.prometheus-svg-container > svg {
- position: absolute;
- height: 100%;
- width: 100%;
- left: 0;
- top: 0;
-
- text {
- fill: $gl-text-color;
- stroke-width: 0;
- }
-
- .text-metric-bold {
- font-weight: $gl-font-weight-bold;
- }
-
- .label-axis-text {
- fill: $black;
- font-weight: $gl-font-weight-normal;
- font-size: 10px;
- }
.text-metric-usage,
.legend-metric-title {
@@ -287,42 +263,65 @@
font-size: 12px;
}
- .legend-axis-text {
- fill: $black;
- }
+ > svg {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ left: 0;
+ top: 0;
- .tick {
- > line {
- stroke: $gray-darker;
+ text {
+ fill: $gl-text-color;
+ stroke-width: 0;
}
- > text {
- font-size: 12px;
+ .text-metric-bold {
+ font-weight: $gl-font-weight-bold;
}
- }
- .text-metric-title {
- font-size: 12px;
- }
+ .label-axis-text {
+ fill: $black;
+ font-weight: $gl-font-weight-normal;
+ font-size: 10px;
+ }
- .y-label-text,
- .x-label-text {
- fill: $gray-darkest;
- }
+ .legend-axis-text {
+ fill: $black;
+ }
- .axis-tick {
- stroke: $gray-darker;
- }
+ .tick {
+ > line {
+ stroke: $gray-darker;
+ }
- @media (max-width: $screen-sm-max) {
- .label-axis-text,
- .text-metric-usage,
- .legend-axis-text {
- font-size: 8px;
+ > text {
+ font-size: 12px;
+ }
+ }
+
+ .text-metric-title {
+ font-size: 12px;
+ }
+
+ .y-label-text,
+ .x-label-text {
+ fill: $gray-darkest;
+ }
+
+ .axis-tick {
+ stroke: $gray-darker;
}
- .tick > text {
- font-size: 8px;
+ @media (max-width: $screen-sm-max) {
+ .label-axis-text,
+ .text-metric-usage,
+ .legend-axis-text {
+ font-size: 8px;
+ }
+
+ .tick > text {
+ font-size: 8px;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 1723d716805..eea8b7dd193 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -85,7 +85,7 @@
}
pre {
- border: none;
+ border: 0;
background: $gray-light;
border-radius: 0;
color: $events-pre-color;
@@ -128,14 +128,14 @@
}
}
- &:last-child { border: none; }
+ &:last-child { border: 0; }
.event_commits {
li {
&.commit {
background: transparent;
padding: 0;
- border: none;
+ border: 0;
.commit-row-title {
font-size: $gl-font-size;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 6f6c6839975..9b7dda9b648 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -26,14 +26,117 @@
}
}
-.groups-header {
- @media (min-width: $screen-sm-min) {
- .nav-links {
- width: 35%;
+.group-nav-container .nav-controls {
+ display: flex;
+ align-items: flex-start;
+ padding: $gl-padding-top 0;
+ border-bottom: 1px solid $border-color;
+
+ .group-filter-form {
+ flex: 1;
+ }
+
+ .dropdown-menu-align-right {
+ margin-top: 0;
+ }
+
+ .new-project-subgroup {
+ .dropdown-primary {
+ min-width: 115px;
+ }
+
+ .dropdown-toggle {
+ .dropdown-btn-icon {
+ pointer-events: none;
+ color: inherit;
+ margin-left: 0;
+ }
}
- .nav-controls {
- width: 65%;
+ .dropdown-menu {
+ min-width: 280px;
+ margin-top: 2px;
+ }
+
+ li:not(.divider) {
+ padding: 0;
+
+ &.droplab-item-selected {
+ .icon-container {
+ .list-item-checkmark {
+ visibility: visible;
+ }
+ }
+ }
+
+ .menu-item {
+ padding: 8px 4px;
+
+ &:hover {
+ background-color: $gray-darker;
+ color: $theme-gray-900;
+ }
+ }
+
+ .icon-container {
+ float: left;
+ padding-left: 6px;
+
+ .list-item-checkmark {
+ visibility: hidden;
+ }
+ }
+
+ .description {
+ font-size: 14px;
+
+ strong {
+ display: block;
+ font-weight: $gl-font-weight-bold;
+ }
+ }
+ }
+ }
+
+ @media (max-width: $screen-sm-max) {
+ &,
+ .dropdown,
+ .dropdown .dropdown-toggle,
+ .btn-new {
+ display: block;
+ }
+
+ .group-filter-form,
+ .dropdown {
+ margin-bottom: 10px;
+ margin-right: 0;
+ }
+
+ .group-filter-form,
+ .dropdown .dropdown-toggle,
+ .btn-new {
+ width: 100%;
+ }
+
+ .dropdown .dropdown-toggle .fa-chevron-down {
+ position: absolute;
+ top: 11px;
+ right: 8px;
+ }
+
+ .new-project-subgroup {
+ display: flex;
+ align-items: flex-start;
+
+ .dropdown-primary {
+ flex: 1;
+ }
+
+ .dropdown-menu {
+ width: 100%;
+ max-width: inherit;
+ min-width: inherit;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index dae8ccdef6c..9cc9e11bcd1 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -1,23 +1,3 @@
-.documentation-index {
- h1 {
- margin: 0;
- }
-
- h2 {
- font-size: 20px;
- }
-
- li {
- line-height: 24px;
- color: $document-index-color;
-
- a {
- margin-right: 3px;
- }
- }
-}
-
-
.shortcut-mappings {
font-size: 12px;
color: $help-shortcut-mapping-color;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index dae3ec7ac42..63c51747f92 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -6,28 +6,20 @@
}
.issuable-warning-icon {
- color: $orange-600;
background-color: $orange-100;
border-radius: $border-radius-default;
- padding: 5px;
margin: 0 $btn-side-margin 0 0;
width: $issuable-warning-size;
height: $issuable-warning-size;
text-align: center;
- &:first-of-type {
- margin-right: $issuable-warning-icon-margin;
+ .icon {
+ fill: $orange-600;
+ vertical-align: text-bottom;
}
-}
-.sidebar-item-icon {
- border-radius: $border-radius-default;
- padding: 5px;
- margin: 0 3px 0 -4px;
-
- &.is-active {
- color: $orange-600;
- background-color: $orange-50;
+ &:first-of-type {
+ margin-right: $issuable-warning-icon-margin;
}
}
@@ -72,10 +64,19 @@
}
}
+ .title-container {
+ display: flex;
+ }
+
.title {
padding: 0;
- margin-bottom: 16px;
- border-bottom: none;
+ margin-bottom: $gl-padding;
+ border-bottom: 0;
+ }
+
+ .btn-edit {
+ margin-left: auto;
+ height: $gl-padding * 2;
}
// Border around images in issue and MR descriptions.
@@ -117,7 +118,16 @@
}
.right-sidebar {
- a,
+ position: absolute;
+ top: $header-height;
+ bottom: 0;
+ right: 0;
+ transition: width $right-sidebar-transition-duration;
+ background: $gray-light;
+ z-index: 200;
+ overflow: hidden;
+
+ a:not(.btn-retry),
.btn-link {
color: inherit;
}
@@ -145,7 +155,7 @@
}
&:last-child {
- border: none;
+ border: 0;
}
span {
@@ -218,17 +228,6 @@
.btn-clipboard:hover {
color: $gl-text-color;
}
-}
-
-.right-sidebar {
- position: absolute;
- top: $header-height;
- bottom: 0;
- right: 0;
- transition: width $right-sidebar-transition-duration;
- background: $gray-light;
- z-index: 200;
- overflow: hidden;
.issuable-sidebar {
width: calc(100% + 100px);
@@ -276,10 +275,15 @@
font-weight: $gl-font-weight-normal;
}
- .no-value {
+ .no-value,
+ .btn-secondary-hover-link {
color: $gl-text-color-secondary;
}
+ .btn-secondary-hover-link:hover {
+ color: $gl-link-color;
+ }
+
.sidebar-collapsed-icon {
display: none;
}
@@ -287,6 +291,8 @@
.gutter-toggle {
margin-top: 7px;
border-left: 1px solid $border-gray-normal;
+ padding-left: 0;
+ text-align: center;
}
.title .gutter-toggle {
@@ -330,7 +336,7 @@
.block {
width: $gutter_collapsed_width - 2px;
padding: 15px 0 0;
- border-bottom: none;
+ border-bottom: 0;
overflow: hidden;
}
@@ -359,7 +365,7 @@
fill: $issuable-sidebar-color;
}
- &:hover,
+ &:hover:not(.disabled),
&:hover .todo-undone {
color: $gl-text-color;
@@ -391,7 +397,7 @@
}
.btn-clipboard {
- border: none;
+ border: 0;
color: $issuable-sidebar-color;
&:hover {
@@ -459,7 +465,7 @@
}
}
- a {
+ a:not(.btn-retry) {
&:hover {
color: $md-link-color;
text-decoration: none;
@@ -532,7 +538,9 @@
}
.participants-list {
- margin: -5px;
+ display: flex;
+ flex-wrap: wrap;
+ margin: -7px;
}
@@ -543,7 +551,7 @@
.participants-author {
display: inline-block;
- padding: 5px;
+ padding: 7px;
&:nth-of-type(7n) {
padding-right: 0;
@@ -603,6 +611,8 @@
float: none;
display: inline-block;
margin-top: 0;
+ height: auto;
+ align-self: center;
@media (max-width: $screen-xs-max) {
position: absolute;
@@ -616,6 +626,8 @@
padding-left: 45px;
padding-right: 45px;
line-height: 35px;
+ display: flex;
+ flex-grow: 1;
@media (min-width: $screen-sm-min) {
float: left;
@@ -627,11 +639,12 @@
.issuable-actions {
@include new-style-dropdown;
- padding-top: 10px;
+ align-self: center;
+ flex-shrink: 0;
+ flex: 0 0 auto;
@media (min-width: $screen-sm-min) {
float: right;
- padding-top: 0;
}
}
@@ -645,8 +658,9 @@
.issuable-meta {
display: inline-block;
- line-height: 18px;
font-size: 14px;
+ line-height: 24px;
+ align-self: center;
}
.js-issuable-selector-wrap {
@@ -892,3 +906,21 @@
margin: 0 3px;
}
}
+
+.right-sidebar-collapsed {
+ .sidebar-grouped-item {
+ .sidebar-collapsed-icon {
+ margin-bottom: 0;
+ }
+
+ .sidebar-collapsed-divider {
+ line-height: 5px;
+ font-size: 12px;
+ color: $theme-gray-700;
+
+ + .sidebar-collapsed-icon {
+ padding-top: 0;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index e8ca5cedaee..d3dda2e7d25 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -134,11 +134,24 @@ ul.related-merge-requests > li {
}
@media (max-width: $screen-xs-max) {
- .issue-btn-group {
- width: 100%;
+ .detail-page-header,
+ .issuable-header {
+ display: block;
+
+ .issuable-meta {
+ line-height: 18px;
+ }
+ }
+
+ .issuable-actions {
+ margin-top: 10px;
- .btn {
+ .issue-btn-group {
width: 100%;
+
+ .btn {
+ width: 100%;
+ }
}
}
}
@@ -190,7 +203,24 @@ ul.related-merge-requests > li {
}
.create-mr-dropdown-wrap {
- @include new-style-dropdown;
+ .branch-message,
+ .ref-message {
+ display: none;
+ }
+
+ .ref::selection {
+ color: $placeholder-text-color;
+ }
+
+ .dropdown {
+ .dropdown-menu-toggle {
+ min-width: 285px;
+ }
+
+ .dropdown-select {
+ width: 285px;
+ }
+ }
.btn-group:not(.hide) {
display: flex;
@@ -201,15 +231,16 @@ ul.related-merge-requests > li {
flex-shrink: 0;
}
- .dropdown-menu {
+ .create-merge-request-dropdown-menu {
width: 300px;
opacity: 1;
visibility: visible;
transform: translateY(0);
display: none;
+ margin-top: 4px;
}
- .dropdown-toggle {
+ .create-merge-request-dropdown-toggle {
.fa-caret-down {
pointer-events: none;
color: inherit;
@@ -217,18 +248,50 @@ ul.related-merge-requests > li {
}
}
+ .droplab-item-ignore {
+ pointer-events: auto;
+ }
+
+ .create-item {
+ cursor: pointer;
+ margin: 0 1px;
+
+ &:hover,
+ &:focus {
+ background-color: $dropdown-item-hover-bg;
+ color: $gl-text-color;
+ }
+ }
+
+ li.divider {
+ margin: 8px 10px;
+ }
+
li:not(.divider) {
+ padding: 8px 9px;
+
+ &:last-child {
+ padding-bottom: 8px;
+ }
+
&.droplab-item-selected {
.icon-container {
i {
visibility: visible;
}
}
+
+ .description {
+ display: block;
+ }
+ }
+
+ &.droplab-item-ignore {
+ padding-top: 8px;
}
.icon-container {
float: left;
- padding-left: 6px;
i {
visibility: hidden;
@@ -236,13 +299,12 @@ ul.related-merge-requests > li {
}
.description {
- padding-left: 30px;
- font-size: 13px;
+ padding-left: 22px;
+ }
- strong {
- display: block;
- font-weight: $gl-font-weight-bold;
- }
+ input,
+ span {
+ margin: 4px 0 0;
}
}
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index cf5f933a762..b7985c4dea5 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -109,13 +109,37 @@
border-top-right-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
+ // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long).
+ // These styles prevent this from breaking the layout, and only applied when providers are configured.
+ &.custom-provider-tabs {
+ flex-wrap: wrap;
+
+ li {
+ min-width: 85px;
+ flex-basis: auto;
+
+ // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen.
+ // We are making somewhat of an assumption about the configuration here: that users do not have more than
+ // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any
+ // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border
+ // above one of the bottom row elements. If you know a better way, please implement it!
+ &:nth-child(n+5) {
+ border-top: 1px solid $border-color;
+ }
+ }
+
+ a {
+ font-size: 16px;
+ }
+ }
+
li {
flex: 1;
text-align: center;
border-left: 1px solid $border-color;
&:first-of-type {
- border-left: none;
+ border-left: 0;
border-top-left-radius: $border-radius-default;
}
@@ -141,7 +165,7 @@
border-bottom: 1px solid $border-color;
a {
- border: none;
+ border: 0;
border-bottom: 2px solid $link-underline-blue;
margin-right: 0;
color: $black;
@@ -154,32 +178,6 @@
}
}
- // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long).
- // These styles prevent this from breaking the layout, and only applied when providers are configured.
-
- .new-session-tabs.custom-provider-tabs {
- flex-wrap: wrap;
-
- li {
- min-width: 85px;
- flex-basis: auto;
-
- // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen.
- // We are making somewhat of an assumption about the configuration here: that users do not have more than
- // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any
- // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border
- // above one of the bottom row elements. If you know a better way, please implement it!
- &:nth-child(n+5) {
- border-top: 1px solid $border-color;
- }
- }
-
- a {
- font-size: 16px;
- }
- }
-
-
.form-control {
&:active,
&:focus {
@@ -231,35 +229,35 @@
margin: 0;
padding: 0;
height: 100%;
-}
-// Fixes footer container to bottom of viewport
-.devise-layout-html body {
- // offset height of fixed header + 1 to avoid scroll
- height: calc(100% - 51px);
- margin: 0;
- padding: 0;
+ // Fixes footer container to bottom of viewport
+ body {
+ // offset height of fixed header + 1 to avoid scroll
+ height: calc(100% - 51px);
+ margin: 0;
+ padding: 0;
- .page-wrap {
- min-height: 100%;
- position: relative;
- }
+ .page-wrap {
+ min-height: 100%;
+ position: relative;
+ }
- .footer-container,
- hr.footer-fixed {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 40px;
- background: $white-light;
- }
+ .footer-container,
+ hr.footer-fixed {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 40px;
+ background: $white-light;
+ }
- .navless-container {
- padding: 65px 15px; // height of footer + bottom padding of email confirmation link
+ .navless-container {
+ padding: 65px 15px; // height of footer + bottom padding of email confirmation link
- @media (max-width: $screen-xs-max) {
- padding: 0 15px 65px;
+ @media (max-width: $screen-xs-max) {
+ padding: 0 15px 65px;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index b3bab082a35..18c48405ecd 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -3,41 +3,12 @@
border-bottom: 1px solid $border-color;
}
-.project-member-tabs {
- background: $gray-light;
- border: 1px solid $border-color;
-
- li {
- width: 50%;
-
- &.active {
- background: $white-light;
- }
-
- &:first-child {
- border-right: 1px solid $border-color;
- }
-
- a {
- width: 100%;
- text-align: center;
- }
- }
-}
-
.users-project-form {
.btn-create {
margin-right: 10px;
}
}
-.project-member-tab-content {
- padding: $gl-padding;
- border: 1px solid $border-color;
- border-top: 0;
- margin-bottom: $gl-padding;
-}
-
.member {
.list-item-name {
@media (min-width: $screen-sm-min) {
@@ -78,9 +49,17 @@
width: auto;
}
}
+
+ &.existing-title {
+ @media (min-width: $screen-sm-min) {
+ float: left;
+ }
+ }
}
.member-form-control {
+ @include new-style-dropdown;
+
@media (max-width: $screen-xs-max) {
padding-bottom: 5px;
margin-left: 0;
@@ -93,12 +72,6 @@
line-height: 43px;
}
-.member.existing-title {
- @media (min-width: $screen-sm-min) {
- float: left;
- }
-}
-
.member-search-form {
@include new-style-dropdown;
@@ -310,7 +283,3 @@
}
}
}
-
-.member-form-control {
- @include new-style-dropdown;
-}
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index dbf3e2b763c..04bde64c752 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -262,7 +262,7 @@ $colors: (
.editor {
pre {
height: 350px;
- border: none;
+ border: 0;
border-radius: 0;
margin-bottom: 0;
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index d9fb3b44d29..5832cf4637f 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -150,14 +150,6 @@
display: block;
}
- .mr-widget-body {
- @include clearfix;
-
- &.media > *:first-child {
- margin-right: 10px;
- }
- }
-
.mr-widget-pipeline-graph {
padding: 0 4px;
@@ -209,12 +201,17 @@
}
}
- .mr-widget-help {
- padding: 10px 16px 10px 48px;
- font-style: italic;
- }
-
.mr-widget-body {
+ @include clearfix;
+
+ &.media > *:first-child {
+ margin-right: 10px;
+ }
+
+ .approve-btn {
+ margin-right: 5px;
+ }
+
h4 {
float: left;
font-weight: $gl-font-weight-bold;
@@ -336,6 +333,11 @@
}
}
+ .mr-widget-help {
+ padding: 10px 16px 10px 48px;
+ font-style: italic;
+ }
+
.ci-coverage {
float: right;
}
@@ -350,12 +352,6 @@
}
}
-.mr-state-widget .mr-widget-body {
- .approve-btn {
- margin-right: 5px;
- }
-}
-
.mr-widget-body-controls {
flex-wrap: wrap;
}
@@ -469,16 +465,16 @@
padding-bottom: 0;
}
}
-}
-.mr-info-list.mr-memory-usage {
- p {
- float: left;
- }
+ &.mr-memory-usage {
+ p {
+ float: left;
+ }
- .memory-graph-container {
- float: left;
- margin-left: 5px;
+ .memory-graph-container {
+ float: left;
+ margin-left: 5px;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 32039936be7..ae8fa45a2d7 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -66,6 +66,15 @@
height: 6px;
margin: 0;
}
+
+ .sidebar-collapsed-icon {
+ clear: both;
+ padding: 15px 5px 5px;
+
+ .progress {
+ margin: 5px 0;
+ }
+ }
}
.collapsed-milestone-date {
@@ -93,17 +102,6 @@
margin-right: 0;
}
- .milestone-progress {
- .sidebar-collapsed-icon {
- clear: both;
- padding: 15px 5px 5px;
-
- .progress {
- margin: 5px 0;
- }
- }
- }
-
.right-sidebar-collapsed & {
.reference {
border-top: 1px solid $border-gray-normal;
@@ -156,18 +154,16 @@
.status-box {
margin-top: 0;
- }
-
- .milestone-buttons {
- margin-left: auto;
- }
-
- .status-box {
order: 1;
}
.milestone-buttons {
+ margin-left: auto;
order: 2;
+
+ .verbose {
+ display: none;
+ }
}
.header-text-content {
@@ -175,10 +171,6 @@
width: 100%;
}
- .milestone-buttons .verbose {
- display: none;
- }
-
@media (min-width: $screen-xs-min) {
.milestone-buttons .verbose {
display: inline;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 04b132415eb..1e6992cb65e 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -7,7 +7,7 @@
.diff-file .diff-content {
tr.line_holder:hover > td .line_note_link {
- opacity: 1.0;
+ opacity: 1;
filter: alpha(opacity = 100);
}
}
@@ -16,7 +16,7 @@
.discussion {
.new-note {
margin: 0;
- border: none;
+ border: 0;
}
}
@@ -106,36 +106,55 @@
background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0;
border: 1px solid $border-gray-normal;
- border-bottom: none;
+ border-bottom: 0;
padding: 3px 12px;
margin: auto;
align-items: center;
.icon {
margin-right: $issuable-warning-icon-margin;
+ vertical-align: text-bottom;
+ fill: $orange-600;
+ }
+
+ + .md-area {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+
+ .disabled-comment {
+ border: 0;
+ border-radius: $label-border-radius;
+ padding-top: $gl-vert-padding;
+ padding-bottom: $gl-vert-padding;
+
+ .icon svg {
+ position: relative;
+ top: 2px;
+ margin-right: $btn-xs-side-margin;
+ width: $gl-font-size;
+ height: $gl-font-size;
+ fill: $orange-600;
+ }
}
}
-.disabled-comment .issuable-note-warning {
- border: none;
- border-radius: $label-border-radius;
- padding-top: $gl-vert-padding;
- padding-bottom: $gl-vert-padding;
+.sidebar-item-icon {
+ border-radius: $border-radius-default;
+ margin: 0 3px 0 -4px;
+ vertical-align: middle;
- .icon svg {
- position: relative;
- top: 2px;
- margin-right: $btn-xs-side-margin;
- width: $gl-font-size;
- height: $gl-font-size;
+ &.is-active {
fill: $orange-600;
}
}
-.sidebar-item-value {
- .fa {
- background-color: inherit;
- }
+.sidebar-collapsed-icon .sidebar-item-icon {
+ margin: 0;
+}
+
+.sidebar-item-value .sidebar-item-icon {
+ fill: $theme-gray-700;
}
.sidebar-item-warning-message {
@@ -155,11 +174,6 @@
}
}
-.issuable-note-warning + .md-area {
- border-top-left-radius: 0;
- border-top-right-radius: 0;
-}
-
.discussion-form {
background-color: $white-light;
}
@@ -249,13 +263,12 @@
width: 100%;
padding-right: 5px;
}
-
}
.discussion-actions {
display: table;
- .new-issue-for-discussion path {
+ .btn-default path {
fill: $gray-darkest;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 96b7db3b85d..4fe182c9fce 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -269,7 +269,7 @@ ul.notes {
display: none;
}
- &.system-note-commit-list {
+ &.system-note-commit-list:not(.hide-shade) {
max-height: 70px;
overflow: hidden;
display: block;
@@ -291,16 +291,6 @@ ul.notes {
bottom: 0;
background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%);
}
-
- &.hide-shade {
- max-height: 100%;
- overflow: auto;
-
- &::after {
- display: none;
- background: transparent;
- }
- }
}
}
}
@@ -322,57 +312,72 @@ ul.notes {
}
}
-.diff-file .notes_holder {
- font-family: $regular_font;
+.diff-file {
+ .is-over {
+ .add-diff-note {
+ display: inline-block;
+ }
+ }
- td {
- border: 1px solid $white-normal;
- border-left: none;
+ // Merge request notes in diffs
+ // Diff is inline
+ .notes_content .note-header .note-headline-light {
+ display: inline-block;
+ position: relative;
+ }
- &.notes_line {
- vertical-align: middle;
- text-align: center;
- padding: 10px 0;
- background: $gray-light;
- color: $text-color;
- }
+ .notes_holder {
+ font-family: $regular_font;
- &.notes_line2 {
- text-align: center;
- padding: 10px 0;
- border-left: 1px solid $note-line2-border !important;
- }
+ td {
+ border: 1px solid $white-normal;
+ border-left: 0;
- &.notes_content {
- background-color: $gray-light;
- border-width: 1px 0;
- padding: 0;
- vertical-align: top;
- white-space: normal;
+ &.notes_line {
+ vertical-align: middle;
+ text-align: center;
+ padding: 10px 0;
+ background: $gray-light;
+ color: $text-color;
+ }
- &.parallel {
- border-width: 1px;
+ &.notes_line2 {
+ text-align: center;
+ padding: 10px 0;
+ border-left: 1px solid $note-line2-border !important;
}
- .discussion-notes {
- &:not(:first-child) {
- border-top: 1px solid $white-normal;
- margin-top: 20px;
+ &.notes_content {
+ background-color: $gray-light;
+ border-width: 1px 0;
+ padding: 0;
+ vertical-align: top;
+ white-space: normal;
+
+ &.parallel {
+ border-width: 1px;
}
- &:not(:last-child) {
- border-bottom: 1px solid $white-normal;
- margin-bottom: 20px;
+ .discussion-notes {
+ &:not(:first-child) {
+ border-top: 1px solid $white-normal;
+ margin-top: 20px;
+ }
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $white-normal;
+ margin-bottom: 20px;
+ }
}
- }
- .notes {
- background-color: $white-light;
- }
+ .notes {
+ background-color: $white-light;
+ }
- a code {
- top: 0;
- margin-right: 0;
+ a code {
+ top: 0;
+ margin-right: 0;
+ }
}
}
}
@@ -466,6 +471,15 @@ ul.notes {
float: right;
margin-left: 10px;
color: $gray-darkest;
+
+ @include notes-media('max', $screen-md-max) {
+ float: none;
+ margin-left: 0;
+ }
+
+ .btn-group > .discussion-next-btn {
+ margin-left: -1px;
+ }
}
.note-actions {
@@ -475,8 +489,6 @@ ul.notes {
flex-shrink: 0;
display: inline-flex;
align-items: center;
- // For PhantomJS that does not support flex
- float: right;
margin-left: 10px;
color: $gray-darkest;
@@ -487,7 +499,6 @@ ul.notes {
}
.more-actions {
- float: right; // phantomjs fallback
display: flex;
align-items: flex-end;
@@ -508,13 +519,6 @@ ul.notes {
min-width: 180px;
}
-.discussion-actions {
- @include notes-media('max', $screen-md-max) {
- float: none;
- margin-left: 0;
- }
-}
-
.note-actions-item {
margin-left: 12px;
display: flex;
@@ -531,23 +535,15 @@ ul.notes {
padding: 0;
min-width: 16px;
color: $gray-darkest;
+ fill: $gray-darkest;
.fa {
position: relative;
font-size: 16px;
}
-
-
svg {
- height: 16px;
- width: 16px;
- top: 0;
- vertical-align: text-top;
-
- path {
- fill: currentColor;
- }
+ @include btn-svg;
}
.award-control-icon-positive,
@@ -566,10 +562,7 @@ ul.notes {
.link-highlight {
color: $gl-link-color;
-
- svg {
- fill: $gl-link-color;
- }
+ fill: $gl-link-color;
}
.award-control-icon-neutral {
@@ -666,15 +659,7 @@ ul.notes {
.timeline-entry-inner {
padding-left: $gl-padding;
padding-right: $gl-padding;
- border-bottom: none;
- }
- }
-}
-
-.diff-file {
- .is-over {
- .add-diff-note {
- display: inline-block;
+ border-bottom: 0;
}
}
}
@@ -687,7 +672,7 @@ ul.notes {
padding: 90px 0;
&.discussion-locked {
- border: none;
+ border: 0;
background-color: $white-light;
}
@@ -720,20 +705,20 @@ ul.notes {
svg path {
fill: $gray-darkest;
}
- }
- .btn.discussion-create-issue-btn {
- margin-left: -4px;
- border-radius: 0;
- border-right: 0;
+ &.discussion-create-issue-btn {
+ margin-left: -4px;
+ border-radius: 0;
+ border-right: 0;
- a {
- padding: 0;
- line-height: 0;
+ a {
+ padding: 0;
+ line-height: 0;
- &:hover {
- text-decoration: none;
- border: 0;
+ &:hover {
+ text-decoration: none;
+ border: 0;
+ }
}
}
}
@@ -767,7 +752,7 @@ ul.notes {
top: 0;
padding: 0;
background-color: transparent;
- border: none;
+ border: 0;
outline: 0;
color: $gray-darkest;
transition: color $general-hover-transition-duration $general-hover-transition-curve;
@@ -792,12 +777,6 @@ ul.notes {
}
}
- svg {
- fill: currentColor;
- height: 16px;
- width: 16px;
- }
-
.loading {
margin: 0;
height: auto;
@@ -807,12 +786,3 @@ ul.notes {
.line-resolve-text {
vertical-align: middle;
}
-
-// Merge request notes in diffs
-.diff-file {
- // Diff is inline
- .notes_content .note-header .note-headline-light {
- display: inline-block;
- position: relative;
- }
-}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 086dd528579..cb24274c612 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -31,7 +31,6 @@
}
.pipeline-actions {
- padding-right: 0;
min-width: 170px; //Guarantees buttons don't break in several lines.
.btn-default {
@@ -176,6 +175,25 @@
}
}
+ /**
+ * Play button with icon in dropdowns
+ */
+ .no-btn {
+ border: 0;
+ background: none;
+ outline: none;
+ width: 100%;
+ text-align: left;
+
+ .icon-play {
+ position: relative;
+ top: 2px;
+ margin-right: 5px;
+ height: 13px;
+ width: 12px;
+ }
+ }
+
.duration,
.finished-at {
color: $gl-text-color-secondary;
@@ -209,9 +227,11 @@
}
.stage-cell {
- @media (min-width: $screen-md-min) {
- min-width: 148px;
- margin-right: -4px;
+ &.table-section {
+ @media (min-width: $screen-md-min) {
+ min-width: 148px;
+ margin-right: -4px;
+ }
}
.mini-pipeline-graph-dropdown-toggle svg {
@@ -268,7 +288,7 @@
.pipeline-actions {
@include new-style-dropdown;
- border-bottom: none;
+ border-bottom: 0;
}
.tab-pane {
@@ -298,7 +318,7 @@
}
.build-log {
- border: none;
+ border: 0;
line-height: initial;
}
}
@@ -366,13 +386,13 @@
// Remove right connecting horizontal line from first build in last stage
&:first-child {
&::after {
- border: none;
+ border: 0;
}
}
// Remove right curved connectors from all builds in last stage
&:not(:first-child) {
&::after {
- border: none;
+ border: 0;
}
}
// Remove opposite curve
@@ -389,7 +409,7 @@
// Remove left curved connectors from all builds in first stage
&:not(:first-child) {
&::before {
- border: none;
+ border: 0;
}
}
// Remove opposite curve
@@ -449,36 +469,46 @@
@extend .build-content:hover;
}
- // Action Icons in big pipeline-graph nodes
- .ci-action-icon-container .ci-action-icon-wrapper {
- height: 30px;
- width: 30px;
- background: $white-light;
- border: 1px solid $border-color;
- border-radius: 100%;
- display: block;
-
- &:hover {
- background-color: $stage-hover-bg;
- border: 1px solid $dropdown-toggle-active-border-color;
- }
-
- svg {
- fill: $gl-text-color-secondary;
- position: relative;
- left: -1px;
- top: -1px;
- }
-
- &:hover svg {
- fill: $gl-text-color;
- }
- }
-
.ci-action-icon-container {
position: absolute;
right: 5px;
top: 5px;
+
+ // Action Icons in big pipeline-graph nodes
+ &.ci-action-icon-wrapper {
+ height: 30px;
+ width: 30px;
+ background: $white-light;
+ border: 1px solid $border-color;
+ border-radius: 100%;
+ display: block;
+
+ &:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $dropdown-toggle-active-border-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ svg {
+ fill: $gl-text-color-secondary;
+ position: relative;
+ left: 5px;
+ top: 2px;
+ width: 18px;
+ height: 18px;
+ }
+
+ &.play {
+ svg {
+ width: #{$ci-action-icon-size - 8};
+ height: #{$ci-action-icon-size - 8};
+ left: 8px;
+ }
+ }
+ }
}
.ci-status-icon svg {
@@ -488,7 +518,7 @@
.dropdown-menu-toggle {
background-color: transparent;
- border: none;
+ border: 0;
padding: 0;
&:focus {
@@ -719,17 +749,50 @@ button.mini-pipeline-graph-dropdown-toggle {
svg {
fill: $gl-text-color-secondary;
- width: $ci-action-icon-size;
- height: $ci-action-icon-size;
- left: -6px;
+ width: #{$ci-action-icon-size - 6};
+ height: #{$ci-action-icon-size - 6};
+ left: -3px;
position: relative;
- top: -3px;
+ top: -2px;
+
+ &.icon-action-stop,
+ &.icon-action-cancel {
+ width: 12px;
+ height: 12px;
+ top: 1px;
+ left: -1px;
+ }
+
+ &.icon-action-play {
+ width: 11px;
+ height: 11px;
+ top: 1px;
+ left: 1px;
+ }
+
+ &.icon-action-retry {
+ width: 16px;
+ height: 16px;
+ top: 0;
+ left: -3px;
+ }
}
&:hover svg,
&:focus svg {
fill: $gl-text-color;
}
+
+ &.icon-action-retry,
+ &.icon-action-play {
+ svg {
+ width: #{$ci-action-icon-size - 6};
+ height: #{$ci-action-icon-size - 6};
+ left: 8px;
+ }
+ }
+
+
}
// link to the build
@@ -760,6 +823,11 @@ button.mini-pipeline-graph-dropdown-toggle {
margin-left: 2px;
display: inline-block;
+ &::after {
+ content: '';
+ display: block;
+ }
+
@media (max-width: $screen-xs-max) {
max-width: 60%;
}
@@ -797,13 +865,10 @@ button.mini-pipeline-graph-dropdown-toggle {
left: 100%;
top: -10px;
box-shadow: 0 1px 5px $black-transparent;
-}
-
-/**
- * Top arrow in the dropdown in the big pipeline graph
- */
-.big-pipeline-graph-dropdown-menu {
+ /**
+ * Top arrow in the dropdown in the big pipeline graph
+ */
&::before,
&::after {
content: '';
@@ -865,22 +930,23 @@ button.mini-pipeline-graph-dropdown-toggle {
margin-top: 1px;
border-bottom-color: $white-light;
}
-}
-/**
- * Center dropdown menu in mini graph
- */
-.mini-pipeline-graph-dropdown-menu.dropdown-menu {
- transform: translate(-80%, 0);
- min-width: 150px;
+ /**
+ * Center dropdown menu in mini graph
+ */
+ &.dropdown-menu {
+ transform: translate(-80%, 0);
+ min-width: 150px;
- @media(min-width: $screen-md-min) {
- transform: translate(-50%, 0);
- right: auto;
- left: 50%;
- min-width: 240px;
+ @media(min-width: $screen-md-min) {
+ transform: translate(-50%, 0);
+ right: auto;
+ left: 50%;
+ min-width: 240px;
+ }
}
}
+
/**
* Terminal
*/
@@ -890,7 +956,7 @@ button.mini-pipeline-graph-dropdown-toggle {
.terminal-container {
.content-block {
- border-bottom: none;
+ border-bottom: 0;
}
#terminal {
@@ -904,25 +970,6 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
-/**
- * Play button with icon in dropdowns
- */
-.ci-table .no-btn {
- border: none;
- background: none;
- outline: none;
- width: 100%;
- text-align: left;
-
- .icon-play {
- position: relative;
- top: 2px;
- margin-right: 5px;
- height: 13px;
- width: 12px;
- }
-}
-
.ci-header-container {
min-height: 55px;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index eab39f698c3..28dc71dc641 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -113,7 +113,7 @@
li {
padding: 3px 0;
- border: none;
+ border: 0;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index a086c11324d..2dc0c288a6d 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -48,7 +48,8 @@
border: 1px solid $border-color;
}
- + .select2 a {
+ + .select2 a,
+ + .btn-default {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
@@ -79,7 +80,7 @@
.project-feature-settings {
background: $gray-lighter;
- border-top: none;
+ border-top: 0;
margin-bottom: 16px;
}
@@ -87,7 +88,8 @@
transition: background 2s ease-out;
&:disabled {
- opacity: 0.75;
+ opacity: 0.5;
+ pointer-events: none;
}
.highlight-changes & {
@@ -126,7 +128,7 @@
.project-feature-toggle {
position: relative;
- border: none;
+ border: 0;
outline: 0;
display: block;
width: 100px;
@@ -289,14 +291,7 @@
}
svg {
-
- path {
- fill: $layout-link-gray;
- }
-
- use {
- stroke: $layout-link-gray;
- }
+ fill: $layout-link-gray;
}
.fa-caret-down {
@@ -400,6 +395,18 @@
}
}
}
+
+ .clone-dropdown-btn {
+ background-color: $white-light;
+ }
+
+ .clone-options-dropdown {
+ min-width: 240px;
+
+ .dropdown-menu-inner-content {
+ min-width: 320px;
+ }
+ }
}
.project-repo-buttons {
@@ -481,7 +488,7 @@ a.deploy-project-label {
flex: 1;
padding: 0;
background: transparent;
- border: none;
+ border: 0;
line-height: 34px;
margin: 0;
@@ -549,10 +556,96 @@ a.deploy-project-label {
}
}
-.project-template,
+.project-template {
+ > .form-group {
+ margin-bottom: 0;
+ }
+
+ .template-option {
+ padding: $gl-padding $gl-padding $gl-padding ($gl-padding * 4);
+ position: relative;
+
+ &:not(:first-child) {
+ border-top: 1px solid $border-color;
+ }
+ }
+
+ .template-title {
+ font-size: 16px;
+ }
+
+ .template-description {
+ margin: 6px 0 12px;
+ }
+
+ .template-button {
+ input {
+ position: absolute;
+ clip: rect(0, 0, 0, 0);
+ }
+ }
+
+ svg {
+ position: absolute;
+ left: $gl-padding;
+ top: $gl-padding;
+ }
+
+ .project-fields-form {
+ display: none;
+
+ &.selected {
+ display: block;
+ padding: $gl-padding;
+ }
+ }
+
+ .template-input-group {
+ position: relative;
+
+ @media (min-width: $screen-sm-min) {
+ display: flex;
+ }
+
+ .input-group-addon {
+ flex: 1;
+ text-align: left;
+ padding-left: ($gl-padding * 3);
+ background-color: $white-light;
+ }
+
+ .selected-template {
+ line-height: 20px;
+ }
+
+ .selected-icon {
+ svg {
+ display: none;
+ top: 7px;
+ height: 20px;
+ width: 20px;
+
+ &.active {
+ display: block;
+ }
+ }
+ }
+ }
+}
+
+.gitlab-tab-content {
+ .import-project-pane {
+ padding-bottom: 6px;
+ }
+}
+
.project-import {
- .form-group {
- margin-bottom: 5px;
+ .import-btn-container {
+ margin-bottom: 0;
+ }
+
+ .toggle-import-form {
+ padding-bottom: 10px;
}
.import-buttons {
@@ -567,10 +660,6 @@ a.deploy-project-label {
margin-right: 10px;
}
- .blank-option {
- min-width: 70px;
- }
-
.btn-template-icon {
height: 24px;
width: inherit;
@@ -592,18 +681,6 @@ a.deploy-project-label {
}
}
- .icon-rails path {
- fill: $rails;
- }
-
- .icon-node-express path {
- fill: $node;
- }
-
- .icon-java-spring path {
- fill: $java;
- }
-
> div {
margin-bottom: 10px;
padding-left: 0;
@@ -611,10 +688,6 @@ a.deploy-project-label {
}
}
-.project-templates-buttons .btn:last-child {
- margin-right: 0;
-}
-
.create-project-options {
display: flex;
@@ -711,35 +784,35 @@ a.deploy-project-label {
.nav {
padding-top: 12px;
padding-bottom: 12px;
- }
- .nav > li {
- display: inline-block;
+ > li {
+ display: inline-block;
- &:not(:last-child) {
- margin-right: $gl-padding;
- }
+ &:not(:last-child) {
+ margin-right: $gl-padding;
+ }
- &.right {
- vertical-align: top;
- margin-top: 0;
+ &.right {
+ vertical-align: top;
+ margin-top: 0;
- @media (min-width: $screen-lg-min) {
- float: right;
+ @media (min-width: $screen-lg-min) {
+ float: right;
+ }
}
- }
- }
- .nav > li > a {
- padding: 0;
- background-color: transparent;
- font-size: 14px;
- line-height: 29px;
- color: $notes-light-color;
+ > a {
+ padding: 0;
+ background-color: transparent;
+ font-size: 14px;
+ line-height: 29px;
+ color: $notes-light-color;
- &:hover,
- &:focus {
- color: $gl-text-color;
+ &:hover,
+ &:focus {
+ color: $gl-text-color;
+ }
+ }
}
}
@@ -818,10 +891,6 @@ pre.light-well {
font-size: $gl-font-size;
}
- a {
- color: $gl-text-color;
- }
-
.avatar-container,
.controls {
flex: 0 0 auto;
@@ -944,7 +1013,7 @@ pre.light-well {
margin: 0;
border-radius: 0 0 1px 1px;
padding: 20px 0;
- border: none;
+ border: 0;
}
.table-bordered {
@@ -1053,6 +1122,12 @@ pre.light-well {
min-width: 100px;
}
+ &.form-group {
+ @media (min-width: $screen-sm-min) {
+ margin-bottom: 0;
+ }
+ }
+
.select2-choice {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
@@ -1087,18 +1162,11 @@ pre.light-well {
}
}
-.project-repo-select {
- &.disabled {
- opacity: 0.5;
- pointer-events: none;
- }
-}
-
.variables-table {
table-layout: fixed;
&.table-responsive {
- border: none;
+ border: 0;
}
.variable-key {
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index c36fe25f74d..402412eae71 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -1,17 +1,3 @@
-.fade-enter-active,
-.fade-leave-active {
- transition: opacity $sidebar-transition-duration;
-}
-
-.monaco-loader {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: $black-transparent;
-}
-
.modal.popup-dialog {
display: block;
background-color: $black-transparent;
@@ -49,319 +35,276 @@
}
}
-.repository-view {
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
- color: $almost-black;
+.multi-file {
+ display: flex;
+ height: calc(100vh - 145px);
+ border-top: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
- .code.white pre .hll {
- background-color: $well-light-border !important;
+ &.is-collapsed {
+ .ide-file-list {
+ max-width: 250px;
+ }
}
+}
- .tree-content-holder {
- display: flex;
- min-height: 300px;
+.ide-file-list {
+ flex: 1;
+ overflow: scroll;
+
+ .file {
+ cursor: pointer;
}
- .tree-content-holder-mini {
- height: 100vh;
+ a {
+ color: $gl-text-color;
}
- .panel-right {
- display: flex;
- flex-direction: column;
- width: 80%;
- height: 100%;
+ th {
+ position: sticky;
+ top: 0;
+ }
+}
- .monaco-editor.vs {
- .current-line {
- border: none;
- background: $well-light-border;
- }
+.multi-file-table-name,
+.multi-file-table-col-commit-message {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 0;
+}
- .line-numbers {
- cursor: pointer;
+.multi-file-table-name {
+ width: 350px;
+}
- &:hover {
- text-decoration: underline;
- }
- }
+.multi-file-table-col-commit-message {
+ width: 50%;
+}
- .cursor {
- display: none !important;
- }
- }
+.multi-file-edit-pane {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ border-left: 1px solid $white-dark;
+ overflow: hidden;
+}
- .blob-no-preview {
- .vertical-center {
- justify-content: center;
- width: 100%;
- }
- }
+.multi-file-tabs {
+ display: flex;
+ overflow: scroll;
+ background-color: $white-normal;
+ box-shadow: inset 0 -1px $white-dark;
- &.edit-mode {
- .blob-viewer-container {
- overflow: hidden;
- }
-
- .monaco-editor.vs {
- .cursor {
- background: $black;
- border-color: $black;
- display: block !important;
- }
- }
- }
+ > li {
+ position: relative;
+ }
+}
- .blob-viewer-container {
- flex: 1;
- overflow: auto;
-
- > div,
- .file-content:not(.wiki) {
- 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;
- }
- }
+.multi-file-tab {
+ @include str-truncated(150px);
+ padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
+ background-color: $gray-normal;
+ border-right: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ cursor: pointer;
+
+ &.active {
+ background-color: $white-light;
+ border-bottom-color: $white-light;
+ }
+}
- #tabs {
- flex-shrink: 0;
- display: flex;
- width: 100%;
- padding-left: 0;
- margin-bottom: 0;
- white-space: nowrap;
- overflow-y: hidden;
- overflow-x: auto;
-
- li {
- animation: swipeRightAppear ease-in 0.1s;
- animation-iteration-count: 1;
- transform-origin: 0% 50%;
- list-style-type: none;
- background: $gray-normal;
- display: inline-block;
- padding: #{$gl-padding / 2} $gl-padding;
- border-right: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
- white-space: nowrap;
- cursor: pointer;
-
- &.remove {
- animation: swipeRightDissapear ease-in 0.1s;
- animation-iteration-count: 1;
- transform-origin: 0% 50%;
-
- a {
- width: 0;
- }
- }
-
- &.active {
- background: $white-light;
- border-bottom: none;
- }
-
- a {
- @include str-truncated(100px);
- color: $black;
- vertical-align: middle;
- text-decoration: none;
- margin-right: 12px;
-
- &.close {
- width: auto;
- font-size: 15px;
- opacity: 1;
- margin-right: -6px;
- }
- }
-
- .close-icon:hover {
- color: $hint-color;
- }
-
- .close-icon,
- .unsaved-icon {
- float: right;
- margin-top: 3px;
- margin-left: 15px;
- color: $gray-darkest;
- }
-
- .unsaved-icon {
- color: $brand-success;
- }
-
- &.tabs-divider {
- width: 100%;
- background-color: $white-light;
- border-right: none;
- border-top-right-radius: 2px;
- }
- }
- }
+.multi-file-tab-close {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ padding: 0;
+ background: none;
+ border: 0;
+ font-size: $gl-font-size;
+ color: $gray-darkest;
+ transform: translateY(-50%);
+
+ &:not(.modified):hover,
+ &:not(.modified):focus {
+ color: $hint-color;
+ }
- #repo-file-buttons {
- background-color: $white-light;
- border-bottom: 1px solid $white-normal;
- padding: 5px 10px;
- position: relative;
- border-top: 1px solid $white-normal;
- }
+ &.modified {
+ color: $indigo-700;
+ }
+}
- #binary-viewer {
- height: 80vh;
- overflow: auto;
- margin: 0;
-
- .blob-viewer {
- padding-top: 20px;
- padding-left: 20px;
- }
-
- .binary-unknown {
- text-align: center;
- padding-top: 100px;
- background: $gray-light;
- height: 100%;
- font-size: 17px;
-
- span {
- display: block;
- }
- }
- }
+.multi-file-edit-pane-content {
+ flex: 1;
+ height: 0;
+}
+
+.multi-file-editor-btn-group {
+ padding: $grid-size;
+ border-top: 1px solid $white-dark;
+}
+
+// Not great, but this is to deal with our current output
+.multi-file-preview-holder {
+ height: 100%;
+ overflow: scroll;
+
+ .blob-viewer {
+ height: 100%;
}
- #commit-area {
- background: $gray-light;
- padding: 20px;
+ .file-content.code {
+ display: flex;
- .help-block {
- padding-top: 7px;
- margin-top: 0;
+ i {
+ margin-left: -10px;
}
}
- #view-toggler {
- height: 41px;
- position: relative;
- display: block;
- border-bottom: 1px solid $white-normal;
- background: $white-light;
- margin-top: -5px;
+ .line-numbers {
+ min-width: 50px;
}
- #binary-viewer {
- img {
- max-width: 100%;
- }
+ .file-content,
+ .line-numbers,
+ .blob-content,
+ .code {
+ min-height: 100%;
}
+}
- #sidebar {
- flex: 1;
- height: 100%;
+.multi-file-commit-panel {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 290px;
+ padding: $gl-padding;
+ background-color: $gray-light;
+ border-left: 1px solid $white-dark;
+
+ &.is-collapsed {
+ width: 60px;
+ padding: 0;
+ }
+}
- &.sidebar-mini {
- width: 20%;
- border-right: 1px solid $white-normal;
- overflow: auto;
- }
+.multi-file-commit-panel-section {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
- table {
- margin-bottom: 0;
- }
+.multi-file-commit-panel-header {
+ display: flex;
+ align-items: center;
+ padding: 0 0 12px;
+ margin-bottom: 12px;
+ border-bottom: 1px solid $white-dark;
- tr {
- animation: fadein 0.5s;
- cursor: pointer;
-
- &.repo-file-options td {
- padding: 0;
- border-top: none;
- background: $gray-light;
- width: 100%;
- display: inline-block;
-
- &:first-child {
- border-top-left-radius: 2px;
- }
-
- .title {
- display: inline-block;
- font-size: 10px;
- text-transform: uppercase;
- font-weight: $gl-font-weight-bold;
- color: $gray-darkest;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- vertical-align: middle;
- padding: 2px 16px;
- }
- }
-
- .file-icon {
- margin-right: 5px;
- }
-
- td {
- white-space: nowrap;
- }
- }
+ &.is-collapsed {
+ border-bottom: 1px solid $white-dark;
- a {
- @include str-truncated(250px);
- color: $almost-black;
- display: inline-block;
- vertical-align: middle;
+ svg {
+ margin-left: auto;
+ margin-right: auto;
}
}
}
-.render-error {
- min-height: calc(100vh - 62px);
+.multi-file-commit-panel-collapse-btn {
+ padding-top: 0;
+ padding-bottom: 0;
+ margin-left: auto;
+ font-size: 20px;
- p {
- width: 100%;
+ &.is-collapsed {
+ margin-right: auto;
}
}
-@keyframes swipeRightAppear {
- 0% {
- transform: scaleX(0.00);
+.multi-file-commit-list {
+ flex: 1;
+ overflow: scroll;
+}
+
+.multi-file-commit-list-item {
+ display: flex;
+ align-items: center;
+}
+
+.multi-file-addition {
+ fill: $green-500;
+}
+
+.multi-file-modified {
+ fill: $orange-500;
+}
+
+.multi-file-commit-list-collapsed {
+ display: flex;
+ flex-direction: column;
+
+ > svg {
+ margin-left: auto;
+ margin-right: auto;
}
+}
+
+.multi-file-commit-list-path {
+ @include str-truncated(100%);
+}
+
+.multi-file-commit-form {
+ padding-top: 12px;
+ border-top: 1px solid $white-dark;
+}
+
+.multi-file-commit-fieldset {
+ display: flex;
+ align-items: center;
+ padding-bottom: 12px;
- 100% {
- transform: scaleX(1.00);
+ .btn {
+ flex: 1;
}
}
-@keyframes swipeRightDissapear {
- 0% {
- transform: scaleX(1.00);
+.multi-file-commit-message.form-control {
+ height: 80px;
+ resize: none;
+}
+
+.dirty-diff {
+ // !important need to override monaco inline style
+ width: 4px !important;
+ left: 0 !important;
+
+ &-modified {
+ background-color: $blue-500;
+ }
+
+ &-added {
+ background-color: $green-600;
}
- 100% {
- transform: scaleX(0.00);
+ &-removed {
+ height: 0 !important;
+ width: 0 !important;
+ bottom: -2px;
+ border-style: solid;
+ border-width: 5px;
+ border-color: transparent transparent transparent $red-500;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100px;
+ height: 1px;
+ background-color: rgba($red-500, .5);
+ }
}
}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index 6cac37a4e28..5fb97b13470 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -50,3 +50,10 @@
font-size: 11px;
}
}
+
+@media (max-width: $screen-md-max) {
+ .runners-content {
+ width: 100%;
+ overflow: auto;
+ }
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index db0a04a5eb3..fe455a04960 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -5,7 +5,7 @@
margin-bottom: $gl-padding;
&:last-child {
- border-bottom: none;
+ border-bottom: 0;
}
}
@@ -57,7 +57,7 @@ input[type="checkbox"]:hover {
}
.search-input {
- border: none;
+ border: 0;
font-size: 14px;
padding: 0 20px 0 0;
margin-left: 5px;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 41a6ba2023a..5d630c7d61e 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -23,15 +23,14 @@
}
.settings {
- overflow: hidden;
border-bottom: 1px solid $gray-darker;
&:first-of-type {
margin-top: 10px;
}
- &.expanded {
- overflow: visible;
+ &.animating {
+ overflow: hidden;
}
}
@@ -56,14 +55,18 @@
overflow-y: scroll;
padding-right: 110px;
animation: collapseMaxHeight 300ms ease-out;
+ // Keep the section from expanding when we scroll over it
+ pointer-events: none;
- &.expanded {
+ .settings.expanded & {
max-height: none;
overflow-y: visible;
animation: expandMaxHeight 300ms ease-in;
+ // Reset and allow clicks again when expanded
+ pointer-events: auto;
}
- &.no-animate {
+ .settings.no-animate & {
animation: none;
}
@@ -238,11 +241,30 @@
margin-left: 5px;
background: $badge-bg;
}
- }
- /* Ensure we don't add border if there's only single li */
- li + li {
- border-top: 1px solid $border-color;
+ /* Ensure we don't add border if there's only single li */
+ + li {
+ border-top: 1px solid $border-color;
+ }
}
}
}
+
+.modal-doorkeepr-auth,
+.doorkeeper-app-form {
+ .scope-description {
+ color: $theme-gray-700;
+ }
+}
+
+.modal-doorkeepr-auth {
+ .modal-body {
+ padding: $gl-padding;
+ }
+}
+
+.doorkeeper-app-form {
+ .scope-description {
+ margin: 0 0 5px 17px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss
index bfe065dbbaf..2bf0bedb1f5 100644
--- a/app/assets/stylesheets/pages/sherlock.scss
+++ b/app/assets/stylesheets/pages/sherlock.scss
@@ -5,10 +5,10 @@ table .sherlock-code {
.sherlock-code {
pre {
word-wrap: normal;
- }
- pre code {
- white-space: pre;
+ code {
+ white-space: pre;
+ }
}
}
@@ -21,13 +21,13 @@ table .sherlock-code {
text-align: right;
padding: 0 10px !important;
}
+
+ .slow {
+ color: $red-500;
+ font-weight: $gl-font-weight-bold;
+ }
}
.sherlock-file-sample pre {
padding-top: 28px !important;
}
-
-.sherlock-line-samples-table .slow {
- color: $red-500;
- font-weight: $gl-font-weight-bold;
-}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index dfa4d033fb8..cede147d559 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -40,16 +40,16 @@
@media (max-width: $screen-xs-max) {
width: 100%;
}
- }
- .person .spark {
- display: block;
- background: $stat-graph-common-bg;
- width: 100%;
- }
+ .spark {
+ display: block;
+ background: $stat-graph-common-bg;
+ width: 100%;
+ }
- .person .area-contributor {
- fill: $stat-graph-orange-fill;
+ .area-contributor {
+ fill: $stat-graph-orange-fill;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 25c80e1f950..ade5ddd147b 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -55,10 +55,6 @@
&:not(span):hover {
background-color: rgba($gl-text-color-secondary, .07);
}
-
- svg {
- fill: $gl-text-color-secondary;
- }
}
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 6c8d87185e9..2139a029fc7 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -141,7 +141,7 @@
}
pre {
- border: none;
+ border: 0;
background: $gray-light;
border-radius: 0;
color: $todo-body-pre-color;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index e2f6e511c86..65b334662c2 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -125,7 +125,7 @@
color: $white-normal;
}
- &:hover {
+ &:hover:not(.tree-truncated-warning) {
td {
background-color: $row-hover;
border-top: 1px solid $row-hover-border;
@@ -198,6 +198,11 @@
}
}
+ .tree-truncated-warning {
+ color: $orange-600;
+ background-color: $orange-100;
+ }
+
.tree-time-ago {
min-width: 135px;
color: $gl-text-color-secondary;
@@ -252,7 +257,7 @@
margin-top: 20px;
padding: 0;
border-top: 1px solid $white-dark;
- border-bottom: none;
+ border-bottom: 0;
}
.commit-stats li {
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index b7d4e7bf582..e150f96f3fa 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -161,10 +161,10 @@ ul.wiki-pages-list.content-list {
list-style: none;
margin-left: 0;
padding-left: 15px;
- }
- ul li {
- padding: 5px 0;
+ li {
+ padding: 5px 0;
+ }
}
}
diff --git a/app/assets/stylesheets/test.scss b/app/assets/stylesheets/test.scss
index 06733b7f1a9..e65b49c36f3 100644
--- a/app/assets/stylesheets/test.scss
+++ b/app/assets/stylesheets/test.scss
@@ -4,11 +4,6 @@
-ms-transition: none !important;
-webkit-transition: none !important;
transition: none !important;
- -o-transform: none !important;
- -moz-transform: none !important;
- -ms-transform: none !important;
- -webkit-transform: none !important;
- transform: none !important;
-webkit-animation: none !important;
-moz-animation: none !important;
-o-animation: none !important;
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index fb6d8c0bb81..5be23c76a95 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -19,10 +19,12 @@ class Admin::ApplicationsController < Admin::ApplicationController
end
def create
- @application = Doorkeeper::Application.new(application_params)
+ @application = Applications::CreateService.new(current_user, application_params).execute(request)
- if @application.save
- redirect_to_admin_page
+ if @application.persisted?
+ flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
+
+ redirect_to admin_application_url(@application)
else
render :new
end
@@ -41,13 +43,6 @@ class Admin::ApplicationsController < Admin::ApplicationController
redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.'
end
- protected
-
- def redirect_to_admin_page
- flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
- redirect_to admin_application_url(@application)
- end
-
private
def set_application
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb
index 07c8bf714fc..7a2c7234a1e 100644
--- a/app/controllers/admin/impersonation_tokens_controller.rb
+++ b/app/controllers/admin/impersonation_tokens_controller.rb
@@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
end
def set_index_vars
- @scopes = Gitlab::Auth::API_SCOPES
+ @scopes = Gitlab::Auth.available_scopes(current_user)
@impersonation_token ||= finder.build
@inactive_impersonation_tokens = finder(state: 'inactive').execute
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 719893c0bc8..38b808cdc31 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -2,7 +2,8 @@ class Admin::RunnersController < Admin::ApplicationController
before_action :runner, except: :index
def index
- @runners = Ci::Runner.order('id DESC')
+ sort = params[:sort] == 'contacted_asc' ? { contacted_at: :asc } : { id: :desc }
+ @runners = Ci::Runner.order(sort)
@runners = @runners.search(params[:search]) if params[:search].present?
@runners = @runners.page(params[:page]).per(30)
@active_runners_cnt = Ci::Runner.online.count
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 967fe39256a..ee21d81f23e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -11,8 +11,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
- before_action :authenticate_user_from_private_token!
- before_action :authenticate_user_from_rss_token!
+ before_action :authenticate_sessionless_user!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :check_password_expiration
@@ -97,31 +96,15 @@ class ApplicationController < ActionController::Base
# (e.g. tokens) to authenticate the user, whereas Devise sets current_user
def auth_user
return current_user if current_user.present?
+
return try(:authenticated_user)
end
- # This filter handles both private tokens and personal access tokens
- def authenticate_user_from_private_token!
- token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
-
- return unless token.present?
-
- user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
+ # This filter handles personal access tokens, and atom requests with rss tokens
+ def authenticate_sessionless_user!
+ user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user
- sessionless_sign_in(user)
- end
-
- # This filter handles authentication for atom request with an rss_token
- def authenticate_user_from_rss_token!
- return unless request.format.atom?
-
- token = params[:rss_token].presence
-
- return unless token.present?
-
- user = User.find_by_rss_token(token)
-
- sessionless_sign_in(user)
+ sessionless_sign_in(user) if user
end
def log_exception(exception)
@@ -213,7 +196,11 @@ 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.ldap_user?
+ return if session[:impersonator_id] || !current_user&.allow_password_authentication?
+
+ password_expires_at = current_user&.password_expires_at
+
+ if password_expires_at && password_expires_at < Time.now
return redirect_to new_profile_password_path
end
end
@@ -349,6 +336,6 @@ class ApplicationController < ActionController::Base
def set_page_title_header
# Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8
- response.headers['Page-Title'] = page_title('GitLab').encode('ISO-8859-1')
+ response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 10e8e54f402..cde1e284d2d 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -44,6 +44,7 @@ class AutocompleteController < ApplicationController
if @project.blank? && params[:group_id].present?
group = Group.find(params[:group_id])
return render_404 unless can?(current_user, :read_group, group)
+
group
end
end
@@ -54,6 +55,7 @@ class AutocompleteController < ApplicationController
if params[:project_id].present?
project = Project.find(params[:project_id])
return render_404 unless can?(current_user, :read_project, project)
+
project
end
end
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 737656b3dcc..f8049b20b9f 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -84,6 +84,7 @@ module Boards
resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
labels: true,
+ sidebar_endpoints: true,
include: {
project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb
new file mode 100644
index 00000000000..9d4f97aa443
--- /dev/null
+++ b/app/controllers/concerns/group_tree.rb
@@ -0,0 +1,24 @@
+module GroupTree
+ def render_group_tree(groups)
+ @groups = if params[:filter].present?
+ Gitlab::GroupHierarchy.new(groups.search(params[:filter]))
+ .base_and_ancestors
+ else
+ # Only show root groups if no parent-id is given
+ groups.where(parent_id: params[:parent_id])
+ end
+ @groups = @groups.with_selects_for_list(archived: params[:archived])
+ .sort(@sort = params[:sort])
+ .page(params[:page])
+
+ respond_to do |format|
+ format.html
+ format.json do
+ serializer = GroupChildSerializer.new(current_user: current_user)
+ .with_pagination(request, response)
+ serializer.expand_hierarchy if params[:filter].present?
+ render json: serializer.represent(@groups)
+ end
+ end
+ end
+end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 4079072a930..744e448e8df 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -7,14 +7,59 @@ module IssuableActions
before_action :authorize_admin_issuable!, only: :bulk_update
end
+ def show
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: serializer.represent(issuable, serializer: params[:serializer])
+ end
+ end
+ end
+
+ def update
+ @issuable = update_service.execute(issuable)
+
+ respond_to do |format|
+ format.html do
+ recaptcha_check_with_fallback { render :edit }
+ end
+
+ format.json do
+ render_entity_json
+ end
+ end
+
+ rescue ActiveRecord::StaleObjectError
+ render_conflict_response
+ end
+
+ def realtime_changes
+ Gitlab::PollingInterval.set_header(response, interval: 3_000)
+
+ response = {
+ title: view_context.markdown_field(issuable, :title),
+ title_text: issuable.title,
+ description: view_context.markdown_field(issuable, :description),
+ description_text: issuable.description,
+ task_status: issuable.task_status
+ }
+
+ if issuable.edited?
+ response[:updated_at] = issuable.updated_at
+ response[:updated_by_name] = issuable.last_edited_by.name
+ response[:updated_by_path] = user_path(issuable.last_edited_by)
+ end
+
+ render json: response
+ end
+
def destroy
- issuable.destroy
- destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
- TodoService.new.public_send(destroy_method, issuable, current_user) # rubocop:disable GitlabSecurity/PublicSend
+ Issuable::DestroyService.new(issuable.project, current_user).execute(issuable)
+ TodoService.new.destroy_issuable(issuable, current_user)
name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted."
- index_path = polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
+ index_path = polymorphic_path([parent, issuable.class])
respond_to do |format|
format.html { redirect_to index_path }
@@ -68,6 +113,10 @@ module IssuableActions
end
end
+ def authorize_update_issuable!
+ render_404 unless can?(current_user, :"update_#{resource_name}", issuable)
+ end
+
def bulk_update_params
permitted_keys = [
:issuable_ids,
@@ -92,4 +141,24 @@ module IssuableActions
def resource_name
@resource_name ||= controller_name.singularize
end
+
+ def render_entity_json
+ if @issuable.valid?
+ render json: serializer.represent(@issuable)
+ else
+ render json: { errors: @issuable.errors.full_messages }, status: :unprocessable_entity
+ end
+ end
+
+ def serializer
+ raise NotImplementedError
+ end
+
+ def update_service
+ raise NotImplementedError
+ end
+
+ def parent
+ @project || @group
+ end
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 3181f517087..f3c9251225f 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -4,58 +4,44 @@ module IssuableCollections
include Gitlab::IssuableMetadata
included do
- helper_method :issues_finder
- helper_method :merge_requests_finder
+ helper_method :finder
end
private
- def set_issues_index
- @collection_type = "Issue"
- @issues = issues_collection
- @issues = @issues.page(params[:page])
- @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
- @total_pages = issues_page_count(@issues)
+ def set_issuables_index
+ @issuables = issuables_collection
+ @issuables = @issuables.page(params[:page])
+ @issuable_meta_data = issuable_meta_data(@issuables, collection_type)
+ @total_pages = issuable_page_count
- return if redirect_out_of_range(@issues, @total_pages)
+ return if redirect_out_of_range(@total_pages)
if params[:label_name].present?
- @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
+ labels_params = { project_id: @project.id, title: params[:label_name] }
+ @labels = LabelsFinder.new(current_user, labels_params).execute
end
@users = []
- end
-
- def issues_collection
- issues_finder.execute.preload(:project, :author, :assignees, :labels, :milestone, project: :namespace)
- end
-
- def merge_requests_collection
- 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
+ if params[:assignee_id].present?
+ assignee = User.find_by_id(params[:assignee_id])
+ @users.push(assignee) if assignee
+ end
- def issues_finder
- @issues_finder ||= issuable_finder_for(IssuesFinder)
+ if params[:author_id].present?
+ author = User.find_by_id(params[:author_id])
+ @users.push(author) if author
+ end
end
- def merge_requests_finder
- @merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder)
+ def issuables_collection
+ finder.execute.preload(preload_for_collection)
end
- def redirect_out_of_range(relation, total_pages)
+ def redirect_out_of_range(total_pages)
return false if total_pages.zero?
- out_of_range = relation.current_page > total_pages
+ out_of_range = @issuables.current_page > total_pages
if out_of_range
redirect_to(url_for(params.merge(page: total_pages, only_path: true)))
@@ -64,12 +50,8 @@ module IssuableCollections
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)
+ def issuable_page_count
+ page_count_for_relation(@issuables, finder.row_count)
end
def page_count_for_relation(relation, row_count)
@@ -145,4 +127,31 @@ module IssuableCollections
else value
end
end
+
+ def finder
+ return @finder if defined?(@finder)
+
+ @finder = issuable_finder_for(@finder_type)
+ end
+
+ def collection_type
+ @collection_type ||= case finder
+ when IssuesFinder
+ 'Issue'
+ when MergeRequestsFinder
+ 'MergeRequest'
+ end
+ end
+
+ def preload_for_collection
+ @preload_for_collection ||= case collection_type
+ when 'Issue'
+ [:project, :author, :assignees, :labels, :milestone, project: :namespace]
+ when 'MergeRequest'
+ [
+ :source_project, :target_project, :author, :assignee, :labels, :milestone,
+ head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
+ ]
+ end
+ end
end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index 404559c8707..ad594903331 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -3,14 +3,14 @@ module IssuesAction
include IssuableCollections
def issues
- @label = issues_finder.labels.first
+ @finder_type = IssuesFinder
+ @label = finder.labels.first
- @issues = issues_collection
+ @issues = issuables_collection
.non_archived
.page(params[:page])
- @collection_type = "Issue"
- @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
+ @issuable_meta_data = issuable_meta_data(@issues, collection_type)
respond_to do |format|
format.html
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 2b6afaa6233..4311f9d4db9 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -74,8 +74,9 @@ module LfsRequest
def lfs_upload_access?
return false unless project.lfs_enabled?
+ return false unless has_authentication_ability?(:push_code)
- has_authentication_ability?(:push_code) && can?(user, :push_code, project)
+ lfs_deploy_token? || can?(user, :push_code, project)
end
def lfs_deploy_token?
@@ -91,16 +92,7 @@ module LfsRequest
end
def storage_project
- @storage_project ||= begin
- result = project
-
- loop do
- break unless result.forked?
- result = result.forked_from_project
- end
-
- result
- end
+ @storage_project ||= project.lfs_storage_project
end
def objects
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index d3c8e4888bc..8b569a01afd 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -3,13 +3,12 @@ module MergeRequestsAction
include IssuableCollections
def merge_requests
- @label = merge_requests_finder.labels.first
+ @finder_type = MergeRequestsFinder
+ @label = finder.labels.first
- @merge_requests = merge_requests_collection
- .page(params[:page])
+ @merge_requests = issuables_collection.page(params[:page])
- @collection_type = "MergeRequest"
- @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
+ @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)
end
private
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 1126f706393..be2e1b47feb 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -4,6 +4,7 @@ module NotesActions
included do
before_action :set_polling_interval_header, only: [:index]
+ before_action :require_noteable!, only: [:index, :create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :note_project, only: [:create]
end
@@ -38,7 +39,7 @@ module NotesActions
@note = Notes::CreateService.new(note_project, current_user, create_params).execute
if @note.is_a?(Note)
- Banzai::NoteRenderer.render([@note], @project, current_user)
+ Notes::RenderService.new(current_user).execute([@note], @project)
end
respond_to do |format|
@@ -51,7 +52,7 @@ module NotesActions
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
- Banzai::NoteRenderer.render([@note], @project, current_user)
+ Notes::RenderService.new(current_user).execute([@note], @project)
end
respond_to do |format|
@@ -89,7 +90,7 @@ module NotesActions
if note.persisted?
attrs[:valid] = true
- if noteable.nil? || noteable.discussions_rendered_on_frontend?
+ if noteable.discussions_rendered_on_frontend?
attrs.merge!(note_serializer.represent(note))
else
attrs.merge!(
@@ -108,6 +109,8 @@ module NotesActions
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
+
+ attrs[:discussion_line_code] = discussion.line_code if discussion.diff_discussion?
end
end
else
@@ -188,7 +191,11 @@ module NotesActions
end
def noteable
- @noteable ||= notes_finder.target
+ @noteable ||= notes_finder.target || @note&.noteable
+ end
+
+ def require_noteable!
+ render_404 unless noteable
end
def last_fetched_at
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
new file mode 100644
index 00000000000..e9b9e9b38bc
--- /dev/null
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -0,0 +1,23 @@
+module PreviewMarkdown
+ extend ActiveSupport::Concern
+
+ def preview_markdown
+ result = PreviewMarkdownService.new(@project, current_user, params).execute
+
+ markdown_params =
+ case controller_name
+ when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
+ when 'snippets' then { skip_project_check: true }
+ when 'groups' then { group: group }
+ else {}
+ end
+
+ render json: {
+ body: view_context.markdown(result[:text], markdown_params),
+ references: {
+ users: result[:users],
+ commands: view_context.markdown(result[:commands])
+ }
+ }
+ end
+end
diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb
index 4791bc561a4..824ad06465c 100644
--- a/app/controllers/concerns/renders_notes.rb
+++ b/app/controllers/concerns/renders_notes.rb
@@ -3,7 +3,7 @@ module RendersNotes
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
preload_first_time_contribution_for_authors(noteable, notes)
- Banzai::NoteRenderer.render(notes, @project, current_user)
+ Notes::RenderService.new(current_user).execute(notes, @project)
notes
end
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 80ab681ed87..bc0948cd3fb 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -10,7 +10,7 @@ class ConfirmationsController < Devise::ConfirmationsController
users_almost_there_path
end
- def after_confirmation_path_for(_resource_name, resource)
+ def after_confirmation_path_for(resource_name, resource)
# incoming resource can either be a :user or an :email
if signed_in?(:user)
after_sign_in(resource)
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index 8057a0b455c..025769f512a 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,33 +1,8 @@
class Dashboard::GroupsController < Dashboard::ApplicationController
- def index
- @sort = params[:sort] || 'id_desc'
-
- @groups =
- if params[:parent_id] && Group.supports_nested_groups?
- parent = Group.find_by(id: params[:parent_id])
-
- if can?(current_user, :read_group, parent)
- GroupsFinder.new(current_user, parent: parent).execute
- else
- Group.none
- end
- else
- current_user.groups
- end
+ include GroupTree
- @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
- @groups = @groups.includes(:route)
- @groups = @groups.sort(@sort)
- @groups = @groups.page(params[:page])
-
- respond_to do |format|
- format.html
- format.json do
- render json: GroupSerializer
- .new(current_user: @current_user)
- .with_pagination(request, response)
- .represent(@groups)
- end
- end
+ def index
+ groups = GroupsFinder.new(current_user, all_available: false).execute
+ render_group_tree(groups)
end
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index cd94a36a6e7..d9884a47ec4 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -57,5 +57,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
+
+ Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 02c5857eea7..e89eaf7edda 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -76,7 +76,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def redirect_out_of_range(todos)
total_pages =
if todo_params.except(:sort, :page).empty?
- (current_user.todos_pending_count / todos.limit_value).ceil
+ (current_user.todos_pending_count.to_f / todos.limit_value).ceil
else
todos.total_pages
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 19a5db6fd17..280ed93faf8 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -32,6 +32,8 @@ class DashboardController < Dashboard::ApplicationController
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: @event_filter)
.to_a
+
+ Events::RenderService.new(current_user).execute(@events)
end
def set_show_full_reference
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index 81883c543ba..fa0a0f68fbc 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,17 +1,7 @@
class Explore::GroupsController < Explore::ApplicationController
- def index
- @groups = GroupsFinder.new(current_user).execute
- @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
- @groups = @groups.sort(@sort = params[:sort])
- @groups = @groups.page(params[:page])
+ include GroupTree
- respond_to do |format|
- format.html
- format.json do
- render json: {
- html: view_to_html_string("explore/groups/_groups", locals: { groups: @groups })
- }
- end
- end
+ def index
+ render_group_tree GroupsFinder.new(current_user).execute
end
end
diff --git a/app/controllers/groups/children_controller.rb b/app/controllers/groups/children_controller.rb
new file mode 100644
index 00000000000..b474f5d15ee
--- /dev/null
+++ b/app/controllers/groups/children_controller.rb
@@ -0,0 +1,39 @@
+module Groups
+ class ChildrenController < Groups::ApplicationController
+ before_action :group
+
+ def index
+ parent = if params[:parent_id].present?
+ GroupFinder.new(current_user).execute(id: params[:parent_id])
+ else
+ @group
+ end
+
+ if parent.nil?
+ render_404
+ return
+ end
+
+ setup_children(parent)
+
+ respond_to do |format|
+ format.json do
+ serializer = GroupChildSerializer
+ .new(current_user: current_user)
+ .with_pagination(request, response)
+ serializer.expand_hierarchy(parent) if params[:filter].present?
+ render json: serializer.represent(@children)
+ end
+ end
+ end
+
+ protected
+
+ def setup_children(parent)
+ @children = GroupDescendantsFinder.new(current_user: current_user,
+ parent_group: parent,
+ params: params).execute
+ @children = @children.page(params[:page])
+ end
+ end
+end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 7a7bcb1a3d2..f013d21275e 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -80,7 +80,8 @@ class Groups::MilestonesController < Groups::ApplicationController
milestones = MilestonesFinder.new(search_params).execute
legacy_milestones = GroupMilestone.build_collection(group, group_projects, params)
- milestones + legacy_milestones
+ @sort = params[:sort] || 'due_date_asc'
+ MilestoneArray.sort(milestones + legacy_milestones, @sort)
end
def milestone
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 3769a2cde33..eb53a522f90 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -2,6 +2,7 @@ class GroupsController < Groups::ApplicationController
include IssuesAction
include MergeRequestsAction
include ParamsBackwardCompatibility
+ include PreviewMarkdown
respond_to :html
@@ -45,15 +46,11 @@ class GroupsController < Groups::ApplicationController
end
def show
- setup_projects
-
respond_to do |format|
- format.html
-
- format.json do
- render json: {
- html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
- }
+ format.html do
+ @has_children = GroupDescendantsFinder.new(current_user: current_user,
+ parent_group: @group,
+ params: params).has_children?
end
format.atom do
@@ -63,13 +60,6 @@ class GroupsController < Groups::ApplicationController
end
end
- def subgroups
- return not_found unless Group.supports_nested_groups?
-
- @nested_groups = GroupsFinder.new(current_user, parent: group).execute
- @nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
- end
-
def activity
respond_to do |format|
format.html
@@ -106,20 +96,6 @@ class GroupsController < Groups::ApplicationController
protected
- def setup_projects
- set_non_archived_param
- params[:sort] ||= 'latest_activity_desc'
- @sort = params[:sort]
-
- options = {}
- options[:only_owned] = true if params[:shared] == '0'
- options[:only_shared] = true if params[:shared] == '1'
-
- @projects = GroupProjectsFinder.new(params: params, group: group, options: options, current_user: current_user).execute
- @projects = @projects.includes(:namespace)
- @projects = @projects.page(params[:page]) if params[:name].blank?
- end
-
def authorize_create_group!
allowed = if params[:parent_id].present?
parent = Group.find_by(id: params[:parent_id])
@@ -165,9 +141,22 @@ class GroupsController < Groups::ApplicationController
end
def load_events
+ params[:sort] ||= 'latest_activity_desc'
+
+ options = {}
+ options[:only_owned] = true if params[:shared] == '0'
+ options[:only_shared] = true if params[:shared] == '1'
+
+ @projects = GroupProjectsFinder.new(params: params, group: group, options: options, current_user: current_user)
+ .execute
+ .includes(:namespace)
+ .page(params[:page])
+
@events = EventCollection
.new(@projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
+
+ Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
def user_actions
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 572915a4930..38f379dbf4f 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -57,6 +57,10 @@ class HelpController < ApplicationController
def shortcuts
end
+ def instance_configuration
+ @instance_configuration = InstanceConfiguration.new
+ end
+
def ui
@user = User.new(id: 0, name: 'John Doe', username: '@johndoe')
end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index ab18d86dcae..b8ba7921613 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -43,7 +43,7 @@ class Import::GithubController < Import::BaseController
@target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
if can?(current_user, :create_projects, @target_namespace)
- @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute
+ @project = Gitlab::LegacyGithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute
else
render 'unauthorized'
end
@@ -52,7 +52,7 @@ class Import::GithubController < Import::BaseController
private
def client
- @client ||= Gitlab::GithubImport::Client.new(session[access_token_key], client_options)
+ @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options)
end
def verify_import_enabled
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index 510813846a4..567957ba2cb 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -4,6 +4,7 @@ class Import::GitlabProjectsController < Import::BaseController
def new
@namespace = Namespace.find(project_params[:namespace_id])
return render_404 unless current_user.can?(:create_projects, @namespace)
+
@path = project_params[:path]
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 0982a61902b..04b29aa2384 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -51,7 +51,7 @@ class InvitesController < ApplicationController
return if current_user
notice = "To accept this invitation, sign in"
- notice << " or create an account" if current_application_settings.signup_enabled?
+ notice << " or create an account" if current_application_settings.allow_signup?
notice << "."
store_location_for :user, request.fullpath
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 4bceb1d67a3..7d6fe6a0232 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -30,11 +30,11 @@ class JwtController < ApplicationController
render_unauthorized
end
end
- rescue Gitlab::Auth::MissingPersonalTokenError
- render_missing_personal_token
+ rescue Gitlab::Auth::MissingPersonalAccessTokenError
+ render_missing_personal_access_token
end
- def render_missing_personal_token
+ def render_missing_personal_access_token
render json: {
errors: [
{ code: 'UNAUTHORIZED',
diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb
index 37587a52eaf..d81ad135198 100644
--- a/app/controllers/metrics_controller.rb
+++ b/app/controllers/metrics_controller.rb
@@ -3,10 +3,16 @@ class MetricsController < ActionController::Base
protect_from_forgery with: :exception
- before_action :validate_prometheus_metrics
-
def index
- render text: metrics_service.metrics_text, content_type: 'text/plain; version=0.0.4'
+ response = if Gitlab::Metrics.prometheus_metrics_enabled?
+ metrics_service.metrics_text
+ else
+ help_page = help_page_url('administration/monitoring/prometheus/gitlab_metrics',
+ anchor: 'gitlab-prometheus-metrics'
+ )
+ "# Metrics are disabled, see: #{help_page}\n"
+ end
+ render text: response, content_type: 'text/plain; version=0.0.4'
end
private
@@ -14,8 +20,4 @@ class MetricsController < ActionController::Base
def metrics_service
@metrics_service ||= MetricsService.new
end
-
- def validate_prometheus_metrics
- render_404 unless Gitlab::Metrics.prometheus_metrics_enabled?
- end
end
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index b02e64a132b..2443f529c7b 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -16,25 +16,18 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
end
def create
- @application = Doorkeeper::Application.new(application_params)
+ @application = Applications::CreateService.new(current_user, create_application_params).execute(request)
- @application.owner = current_user
+ if @application.persisted?
+ flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
- if @application.save
- redirect_to_oauth_application_page
+ redirect_to oauth_application_url(@application)
else
set_index_vars
render :index
end
end
- protected
-
- def redirect_to_oauth_application_page
- flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
- redirect_to oauth_application_url(@application)
- end
-
private
def verify_user_oauth_applications_enabled
@@ -61,4 +54,10 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
rescue_from ActiveRecord::RecordNotFound do |exception|
render "errors/not_found", layout: "errors", status: 404
end
+
+ def create_application_params
+ application_params.tap do |params|
+ params[:owner] = current_user
+ end
+ end
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 9612b8d8514..e3c18cba1dd 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -54,7 +54,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if current_user
log_audit_event(current_user, with: :saml)
# Update SAML identity if data has changed.
- identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml)
+ identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take
if identity.nil?
current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
redirect_to profile_account_path, notice: 'Authentication method updated'
@@ -98,7 +98,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def handle_omniauth
if current_user
# Add new authentication method
- current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider'])
+ current_user.identities
+ .with_extern_uid(oauth['provider'], oauth['uid'])
+ .first_or_create(extern_uid: oauth['uid'])
log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
@@ -138,7 +140,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
- if current_application_settings.signup_enabled?
+ if current_application_settings.allow_signup?
message << " Create a GitLab account first, and then connect it to your #{label} account."
end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index fda944adecd..68a52f40342 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -1,6 +1,8 @@
class PasswordsController < Devise::PasswordsController
+ include Gitlab::CurrentSettings
+
before_action :resource_from_email, only: [:create]
- before_action :prevent_ldap_reset, only: [:create]
+ before_action :check_password_authentication_available, only: [:create]
before_action :throttle_reset, only: [:create]
def edit
@@ -25,7 +27,7 @@ class PasswordsController < Devise::PasswordsController
def update
super do |resource|
- if resource.valid? && resource.require_password_creation?
+ if resource.valid? && resource.password_automatically_set?
resource.update_attribute(:password_automatically_set, false)
end
end
@@ -38,11 +40,15 @@ class PasswordsController < Devise::PasswordsController
self.resource = resource_class.find_by_email(email)
end
- def prevent_ldap_reset
- return unless resource&.ldap_user?
+ def check_password_authentication_available
+ if resource
+ return if resource.allow_password_authentication?
+ else
+ return if current_application_settings.password_authentication_enabled?
+ end
redirect_to after_sending_reset_password_instructions_path_for(resource_name),
- alert: "Cannot reset password for LDAP user."
+ alert: "Password authentication is unavailable."
end
def throttle_reset
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 069e6a810f2..f0e5d2aa94e 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -11,10 +11,10 @@ class Profiles::KeysController < Profiles::ApplicationController
end
def create
- @key = Keys::CreateService.new(current_user, key_params).execute
+ @key = Keys::CreateService.new(current_user, key_params.merge(ip_address: request.remote_ip)).execute
if @key.persisted?
- redirect_to_profile_key_path
+ redirect_to profile_key_path(@key)
else
@keys = current_user.keys.select(&:persisted?)
render :index
@@ -50,12 +50,6 @@ class Profiles::KeysController < Profiles::ApplicationController
end
end
- protected
-
- def redirect_to_profile_key_path
- redirect_to profile_key_path(@key)
- end
-
private
def key_params
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index dcfcb855ab5..fa72f67c77e 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 if @user.ldap_user?
+ render_404 unless @user.allow_password_authentication?
end
def user_params
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 4146deefa89..6d9873e38df 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def set_index_vars
- @scopes = Gitlab::Auth.available_scopes
+ @scopes = Gitlab::Auth.available_scopes(current_user)
@inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 5d87037f012..dbf61a17724 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -24,16 +24,6 @@ class ProfilesController < Profiles::ApplicationController
end
end
- def reset_private_token
- Users::UpdateService.new(current_user, user: @user).execute! do |user|
- user.reset_authentication_token!
- end
-
- flash[:notice] = "Private token was successfully reset"
-
- redirect_to profile_account_path
- end
-
def reset_incoming_email_token
Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_incoming_email_token!
@@ -41,7 +31,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "Incoming email token was successfully reset"
- redirect_to profile_account_path
+ redirect_to profile_personal_access_tokens_path
end
def reset_rss_token
@@ -51,7 +41,7 @@ class ProfilesController < Profiles::ApplicationController
flash[:notice] = "RSS token was successfully reset"
- redirect_to profile_account_path
+ redirect_to profile_personal_access_tokens_path
end
def audit_log
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index d7dd8ddcb7d..9e79852e378 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -2,7 +2,6 @@ class Projects::ApplicationController < ApplicationController
include RoutableActions
skip_before_action :authenticate_user!
- before_action :redirect_git_extension
before_action :project
before_action :repository
layout 'project'
@@ -11,15 +10,6 @@ class Projects::ApplicationController < ApplicationController
private
- def redirect_git_extension
- # Redirect from
- # localhost/group/project.git
- # to
- # localhost/group/project
- #
- redirect_to url_for(params.merge(format: nil)) if params[:format] == 'git'
- end
-
def project
return @project if @project
return nil unless params[:project_id] || params[:id]
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 183a6f88a6a..770381472c5 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -205,6 +205,7 @@ class Projects::BlobController < Projects::ApplicationController
tree_path = path_segments.join('/')
render json: json.merge(
+ id: @blob.id,
path: blob.path,
name: blob.name,
extension: blob.extension,
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 7f03ce07dec..56df9991fda 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -15,6 +15,8 @@ class Projects::BranchesController < Projects::ApplicationController
respond_to do |format|
format.html do
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
+ @merged_branch_names =
+ repository.merged_branch_names(@branches.map(&:name))
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429
Gitlab::GitalyClient.allow_n_plus_1_calls do
@max_commits = @branches.reduce(0) do |memo, branch|
@@ -39,7 +41,7 @@ class Projects::BranchesController < Projects::ApplicationController
branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name)
- redirect_to_autodeploy = project.empty_repo? && project.deployment_services.present?
+ redirect_to_autodeploy = project.empty_repo? && project.deployment_platform.present?
result = CreateBranchService.new(project, current_user)
.execute(branch_name, ref)
diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb
new file mode 100644
index 00000000000..90c7fa62216
--- /dev/null
+++ b/app/controllers/projects/clusters/applications_controller.rb
@@ -0,0 +1,25 @@
+class Projects::Clusters::ApplicationsController < Projects::ApplicationController
+ before_action :cluster
+ before_action :application_class, only: [:create]
+ before_action :authorize_read_cluster!
+ before_action :authorize_create_cluster!, only: [:create]
+
+ def create
+ Clusters::Applications::ScheduleInstallationService.new(project, current_user,
+ application_class: @application_class,
+ cluster: @cluster).execute
+ head :no_content
+ rescue StandardError
+ head :bad_request
+ end
+
+ private
+
+ def cluster
+ @cluster ||= project.clusters.find(params[:id]) || render_404
+ end
+
+ def application_class
+ @application_class ||= Clusters::Cluster::APPLICATIONS[params[:application]] || render_404
+ end
+end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 03019b0becc..9a56c9de858 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -1,8 +1,8 @@
class Projects::ClustersController < Projects::ApplicationController
- before_action :cluster, except: [:login, :index, :new, :create]
+ before_action :cluster, except: [:login, :index, :new, :new_gcp, :create]
before_action :authorize_read_cluster!
- before_action :authorize_create_cluster!, only: [:new, :create]
- before_action :authorize_google_api, only: [:new, :create]
+ before_action :authorize_create_cluster!, only: [:new, :new_gcp, :create]
+ before_action :authorize_google_api, only: [:new_gcp, :create]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
@@ -16,7 +16,7 @@ class Projects::ClustersController < Projects::ApplicationController
def login
begin
- state = generate_session_key_redirect(namespace_project_clusters_url.to_s)
+ state = generate_session_key_redirect(providers_gcp_new_namespace_project_clusters_url.to_s)
@authorize_url = GoogleApi::CloudPlatform::Client.new(
nil, callback_google_api_auth_url,
@@ -27,18 +27,23 @@ class Projects::ClustersController < Projects::ApplicationController
end
def new
- @cluster = project.build_cluster
+ end
+
+ def new_gcp
+ @cluster = Clusters::Cluster.new.tap do |cluster|
+ cluster.build_provider_gcp
+ end
end
def create
- @cluster = Ci::CreateClusterService
+ @cluster = Clusters::CreateService
.new(project, current_user, create_params)
.execute(token_in_session)
if @cluster.persisted?
redirect_to project_cluster_path(project, @cluster)
else
- render :new
+ render :new_gcp
end
end
@@ -58,7 +63,7 @@ class Projects::ClustersController < Projects::ApplicationController
end
def update
- Ci::UpdateClusterService
+ Clusters::UpdateService
.new(project, current_user, update_params)
.execute(cluster)
@@ -88,19 +93,19 @@ class Projects::ClustersController < Projects::ApplicationController
def create_params
params.require(:cluster).permit(
- :gcp_project_id,
- :gcp_cluster_zone,
- :gcp_cluster_name,
- :gcp_cluster_size,
- :gcp_machine_type,
- :project_namespace,
- :enabled)
+ :enabled,
+ :name,
+ :provider_type,
+ provider_gcp_attributes: [
+ :gcp_project_id,
+ :zone,
+ :num_nodes,
+ :machine_type
+ ])
end
def update_params
- params.require(:cluster).permit(
- :project_namespace,
- :enabled)
+ params.require(:cluster).permit(:enabled)
end
def authorize_google_api
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index a62f05db7db..6ff96a3f295 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -16,16 +16,13 @@ class Projects::CommitController < Projects::ApplicationController
before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
+ BRANCH_SEARCH_LIMIT = 1000
+
def show
apply_diff_view_cookie!
respond_to do |format|
- format.html do
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37599
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- render
- end
- end
+ format.html { render }
format.diff { render text: @commit.to_diff }
format.patch { render text: @commit.to_patch }
end
@@ -56,8 +53,14 @@ class Projects::CommitController < Projects::ApplicationController
end
def branches
- @branches = @project.repository.branch_names_contains(commit.id)
- @tags = @project.repository.tag_names_contains(commit.id)
+ # branch_names_contains/tag_names_contains can take a long time when there are thousands of
+ # branches/tags - each `git branch --contains xxx` request can consume a cpu core.
+ # so only do the query when there are a manageable number of branches/tags
+ @branches_limit_exceeded = @project.repository.branch_count > BRANCH_SEARCH_LIMIT
+ @branches = @branches_limit_exceeded ? [] : @project.repository.branch_names_contains(commit.id)
+
+ @tags_limit_exceeded = @project.repository.tag_count > BRANCH_SEARCH_LIMIT
+ @tags = @tags_limit_exceeded ? [] : @project.repository.tag_names_contains(commit.id)
render layout: false
end
@@ -104,7 +107,7 @@ class Projects::CommitController < Projects::ApplicationController
end
def commit
- @noteable = @commit ||= @project.commit(params[:id])
+ @noteable = @commit ||= @project.commit_by(oid: params[:id])
end
def define_commit_vars
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 4a841bf2073..026708169f4 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -10,9 +10,6 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :set_commits
def show
- @note_counts = project.notes.where(commit_id: @commits.map(&:id))
- .group(:commit_id).count
-
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
.find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
@@ -48,6 +45,7 @@ class Projects::CommitsController < Projects::ApplicationController
private
def set_commits
+ render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
search = params[:search]
@@ -58,6 +56,7 @@ class Projects::CommitsController < Projects::ApplicationController
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
end
+ @commits = @commits.with_pipeline_status
@commits = prepare_commits_for_rendering(@commits)
end
end
diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb
index 47c312ffddf..1a418d0f15a 100644
--- a/app/controllers/projects/deployments_controller.rb
+++ b/app/controllers/projects/deployments_controller.rb
@@ -12,6 +12,7 @@ class Projects::DeploymentsController < Projects::ApplicationController
def metrics
return render_404 unless deployment.has_metrics?
+
@metrics = deployment.metrics
if @metrics&.any?
render json: @metrics, status: :ok
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 29e223a5273..52d528e816e 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -34,6 +34,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
folder_environments = project.environments.where(environment_type: params[:id])
@environments = folder_environments.with_state(params[:scope] || :available)
.order(:name)
+ @folder = params[:id]
respond_to do |format|
format.html
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 95d7a02e9e9..dd5e66f60e3 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -53,8 +53,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_challenges
render plain: "HTTP Basic: Access denied\n", status: 401
- rescue Gitlab::Auth::MissingPersonalTokenError
- render_missing_personal_token
+ rescue Gitlab::Auth::MissingPersonalAccessTokenError
+ render_missing_personal_access_token
end
def basic_auth_provided?
@@ -78,7 +78,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
@project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}")
end
- def render_missing_personal_token
+ def render_missing_personal_access_token
render plain: "HTTP Basic: Access denied\n" \
"You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}",
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index f59200d3b1f..f58ee3e9109 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -13,11 +13,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
if group
return render_404 unless can?(current_user, :read_group, group)
- project.project_group_links.create(
- group: group,
- group_access: params[:link_group_access],
- expires_at: params[:expires_at]
- )
+ Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group)
else
flash[:alert] = 'Please select a group.'
end
@@ -32,7 +28,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
end
def destroy
- project.project_group_links.find(params[:id]).destroy
+ group_link = project.project_group_links.find(params[:id])
+
+ ::Projects::GroupLinks::DestroyService.new(project, current_user).execute(group_link)
respond_to do |format|
format.html do
@@ -47,4 +45,8 @@ class Projects::GroupLinksController < Projects::ApplicationController
def group_link_params
params.require(:group_link).permit(:group_access, :expires_at)
end
+
+ def group_link_create_params
+ params.permit(:link_group_access, :expires_at)
+ end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b7a108a0ebd..d7a3441a245 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,13 +10,13 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update]
- before_action :set_issues_index, only: [:index]
+ before_action :set_issuables_index, only: [:index]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue
- before_action :authorize_update_issue!, only: [:update, :move]
+ before_action :authorize_update_issuable!, 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]
@@ -24,15 +24,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html
def index
- if params[:assignee_id].present?
- assignee = User.find_by_id(params[:assignee_id])
- @users.push(assignee) if assignee
- end
-
- if params[:author_id].present?
- author = User.find_by_id(params[:author_id])
- @users.push(author) if author
- end
+ @issues = @issuables
respond_to do |format|
format.html
@@ -63,16 +55,8 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue)
end
- def show
- @noteable = @issue
- @note = @project.notes.new(noteable: @issue)
-
- respond_to do |format|
- format.html
- format.json do
- render json: serializer.represent(@issue)
- end
- end
+ def edit
+ respond_with(@issue)
end
def discussions
@@ -116,21 +100,6 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- def update
- update_params = issue_params.merge(spammable_params)
-
- @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
-
- respond_to do |format|
- format.json do
- render_issue_json
- end
- end
-
- rescue ActiveRecord::StaleObjectError
- render_conflict_response
- end
-
def move
params.require(:move_to_project_id)
@@ -188,28 +157,9 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- def realtime_changes
- Gitlab::PollingInterval.set_header(response, interval: 3_000)
-
- response = {
- title: view_context.markdown_field(@issue, :title),
- title_text: @issue.title,
- description: view_context.markdown_field(@issue, :description),
- description_text: @issue.description,
- task_status: @issue.task_status
- }
-
- 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)
- end
-
- render json: response
- end
-
def create_merge_request
- result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
+ create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid)
+ result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute
if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
@@ -222,8 +172,10 @@ class Projects::IssuesController < Projects::ApplicationController
def issue
return @issue if defined?(@issue)
+
# The Sortable default scope causes performance issues when used with find_by
- @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
+ @issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take!
+ @note = @project.notes.new(noteable: @issuable)
return render_404 unless can?(current_user, :read_issue, @issue)
@@ -238,14 +190,6 @@ class Projects::IssuesController < Projects::ApplicationController
project_issue_path(@project, @issue)
end
- def authorize_update_issue!
- render_404 unless can?(current_user, :update_issue, @issue)
- end
-
- def authorize_admin_issues!
- render_404 unless can?(current_user, :admin_issue, @project)
- end
-
def authorize_create_merge_request!
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end
@@ -297,4 +241,14 @@ class Projects::IssuesController < Projects::ApplicationController
def serializer
IssueSerializer.new(current_user: current_user, project: issue.project)
end
+
+ def update_service
+ update_params = issue_params.merge(spammable_params)
+ Issues::UpdateService.new(project, current_user, update_params)
+ end
+
+ def set_issuables_index
+ @finder_type = IssuesFinder
+ super
+ end
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 1b985ea9763..1c4c09c772f 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!,
- except: [:index, :show, :status, :raw, :trace, :cancel_all]
+ except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
+ before_action :authorize_erase_build!, only: [:erase]
layout 'project'
@@ -131,6 +132,10 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :update_build, build)
end
+ def authorize_erase_build!
+ return access_denied! unless can?(current_user, :erase_build, build)
+ end
+
def build
@build ||= project.builds.find(params[:id])
.present(current_user: current_user)
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 480a2dff262..e0f4710175f 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -111,6 +111,7 @@ class Projects::LabelsController < Projects::ApplicationController
begin
return render_404 unless promote_service.execute(@label)
+
respond_to do |format|
format.html do
redirect_to(project_labels_path(@project),
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 32759672b6c..293869345bd 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -54,6 +54,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
name = request.headers['X-Gitlab-Lfs-Tmp']
return if name.include?('/')
return unless oid.present? && name.start_with?(oid)
+
name
end
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 0e71977a58a..1269759fc2b 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -2,7 +2,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
before_action :check_merge_requests_available!
before_action :merge_request
before_action :authorize_read_merge_request!
- before_action :ensure_ref_fetched
private
@@ -10,12 +9,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
@issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end
- # Make sure merge requests created before 8.0
- # have head file in refs/merge-requests/
- def ensure_ref_fetched
- @merge_request.ensure_ref_fetched if Gitlab::Database.read_write?
- end
-
def merge_request_params
params.require(:merge_request).permit(merge_request_params_attributes)
end
diff --git a/app/controllers/projects/merge_requests/conflicts_controller.rb b/app/controllers/projects/merge_requests/conflicts_controller.rb
index 28afef101a9..366524b0783 100644
--- a/app/controllers/projects/merge_requests/conflicts_controller.rb
+++ b/app/controllers/projects/merge_requests/conflicts_controller.rb
@@ -53,7 +53,7 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap
flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'
render json: { redirect_to: project_merge_request_url(@project, @merge_request, resolved_conflicts: true) }
- rescue Gitlab::Conflict::ResolutionError => e
+ rescue Gitlab::Git::Conflict::Resolver::ResolutionError => e
render status: :bad_request, json: { message: e.message }
end
end
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 99dc3dda9e7..1511fc08c89 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -4,7 +4,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
include RendersCommits
skip_before_action :merge_request
- skip_before_action :ensure_ref_fetched
before_action :authorize_create_merge_request!
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
before_action :build_merge_request, except: [:create]
@@ -66,7 +65,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
if params[:ref].present?
@ref = params[:ref]
- @commit = @repository.commit("refs/heads/#{@ref}")
+ @commit = @repository.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
end
render layout: false
@@ -77,7 +76,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
if params[:ref].present?
@ref = params[:ref]
- @commit = @target_project.commit("refs/heads/#{@ref}")
+ @commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
end
render layout: false
@@ -111,9 +110,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@commits = prepare_commits_for_rendering(@merge_request.commits)
@commit = @merge_request.diff_head_commit
- @note_counts = Note.where(commit_id: @commits.map(&:id))
- .group(:commit_id).count
-
@labels = LabelsFinder.new(current_user, project_id: @project.id).execute
set_pipeline_variables
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 7d16e77ef66..9f966889995 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -10,10 +10,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def show
@environment = @merge_request.environments_for(current_user).last
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37431
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
- end
+ render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
end
def diff_for_path
@@ -30,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@merge_request.merge_request_diff
end
- @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff.order_id_desc
+ @merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index c5204080333..abe4e5245b1 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -7,37 +7,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include IssuableCollections
skip_before_action :merge_request, only: [:index, :bulk_update]
- skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update]
- before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
+ before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
+
+ before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues]
def index
- @collection_type = "MergeRequest"
- @merge_requests = merge_requests_collection
- @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)
-
- 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] }
- @labels = LabelsFinder.new(current_user, labels_params).execute
- end
-
- @users = []
- if params[:assignee_id].present?
- assignee = User.find_by_id(params[:assignee_id])
- @users.push(assignee) if assignee
- end
-
- if params[:author_id].present?
- author = User.find_by_id(params[:author_id])
- @users.push(author) if author
- end
+ @merge_requests = @issuables
respond_to do |format|
format.html
@@ -52,7 +30,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def show
validates_merge_request
- ensure_ref_fetched
close_merge_request_without_source_project
check_if_can_be_merged
@@ -83,7 +60,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
- render json: serializer.represent(@merge_request, basic: params[:basic])
+ render json: serializer.represent(@merge_request, serializer: params[:serializer])
end
format.patch do
@@ -103,9 +80,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def commits
# Get commits from repository
# or from cache if already merged
- @commits = prepare_commits_for_rendering(@merge_request.commits)
- @note_counts = Note.where(commit_id: @commits.map(&:id))
- .group(:commit_id).count
+ @commits =
+ prepare_commits_for_rendering(@merge_request.commits.with_pipeline_status)
render json: { html: view_to_html_string('projects/merge_requests/_commits') }
end
@@ -256,14 +232,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
- def authorize_update_merge_request!
- return render_404 unless can?(current_user, :update_merge_request, @merge_request)
- end
-
- def authorize_admin_merge_request!
- return render_404 unless can?(current_user, :admin_merge_request, @merge_request)
- end
-
def validates_merge_request
# Show git not found page
# if there is no saved commits between source & target branch
@@ -348,4 +316,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@target_project = @merge_request.target_project
@target_branches = @merge_request.target_project.repository.branch_names
end
+
+ def set_issuables_index
+ @finder_type = MergeRequestsFinder
+ super
+ end
end
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index c94384d2a1a..980bbf699b6 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -2,13 +2,13 @@ class Projects::MilestonesController < Projects::ApplicationController
include MilestoneActions
before_action :check_issuables_available!
- before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels]
+ before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels, :promote]
# Allow read any milestone
before_action :authorize_read_milestone!
# Allow admin milestone
- before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels]
+ before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels, :promote]
respond_to :html
@@ -69,6 +69,14 @@ class Projects::MilestonesController < Projects::ApplicationController
end
end
+ def promote
+ promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
+ flash[:notice] = "Milestone has been promoted to group milestone."
+ redirect_to group_milestone_path(project.group, promoted_milestone.iid)
+ rescue Milestones::PromoteService::PromoteMilestoneError => error
+ redirect_to milestone, alert: error.message
+ end
+
def destroy
return access_denied! unless can?(current_user, :admin_milestone, @project)
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index ef7d047b1ad..627cb2bd93c 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -76,6 +76,7 @@ class Projects::NotesController < Projects::ApplicationController
def authorize_create_note!
return unless noteable.lockable?
+
access_denied! unless can?(current_user, :create_note, noteable)
end
end
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index abab2e2f0c9..b890818c475 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -6,11 +6,19 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
end
def update
- if @project.update(update_params)
- flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
- redirect_to project_settings_ci_cd_path(@project)
- else
- render 'show'
+ Projects::UpdateService.new(project, current_user, update_params).tap do |service|
+ if service.execute
+ flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
+
+ if service.run_auto_devops_pipeline?
+ CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
+ flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
+ end
+
+ redirect_to project_settings_ci_cd_path(@project)
+ else
+ render 'show'
+ end
end
end
@@ -21,6 +29,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
:runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_in_minutes, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path,
+ :run_auto_devops_pipeline_implicit, :run_auto_devops_pipeline_explicit,
auto_devops_attributes: [:id, :domain, :enabled]
)
end
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 2fd015df688..2376f469213 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -56,9 +56,12 @@ class Projects::RefsController < Projects::ApplicationController
contents[@offset, @limit].to_a.map do |content|
file = @path ? File.join(@path, content.name) : content.name
last_commit = @repo.last_commit_for_path(@commit.id, file)
+ commit_path = project_commit_path(@project, last_commit) if last_commit
{
file_name: content.name,
- commit: last_commit
+ commit: last_commit,
+ type: content.type,
+ commit_path: commit_path
}
end
end
@@ -70,6 +73,11 @@ class Projects::RefsController < Projects::ApplicationController
respond_to do |format|
format.html { render_404 }
+ format.json do
+ response.headers["More-Logs-Url"] = @more_log_url
+
+ render json: @logs
+ end
format.js
end
end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index 44de8a49593..d06d18c498b 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -21,14 +21,14 @@ module Projects
def access_levels_options
{
- create_access_levels: levels_for_dropdown(ProtectedTag::CreateAccessLevel),
- push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel),
- merge_access_levels: levels_for_dropdown(ProtectedBranch::MergeAccessLevel)
+ create_access_levels: levels_for_dropdown,
+ push_access_levels: levels_for_dropdown,
+ merge_access_levels: levels_for_dropdown
}
end
- def levels_for_dropdown(access_level_type)
- roles = access_level_type.human_access_levels.map do |id, text|
+ def levels_for_dropdown
+ roles = ProtectedRefAccess::HUMAN_ACCESS_LEVELS.map do |id, text|
{ id: id, text: text, before_divider: true }
end
{ roles: roles }
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index a8ebdf5a4a9..292e4158f8b 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -1,4 +1,6 @@
class Projects::WikisController < Projects::ApplicationController
+ include PreviewMarkdown
+
before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy
@@ -26,6 +28,7 @@ class Projects::WikisController < Projects::ApplicationController
)
else
return render('empty') unless can?(current_user, :create_wiki, @project)
+
@page = WikiPage.new(@project_wiki)
@page.title = params[:id]
@@ -72,7 +75,11 @@ class Projects::WikisController < Projects::ApplicationController
def history
@page = @project_wiki.find_page(params[:id])
- unless @page
+ if @page
+ @page_versions = Kaminari.paginate_array(@page.versions(page: params[:page]),
+ total_count: @page.count_versions)
+ .page(params[:page])
+ else
redirect_to(
project_wiki_path(@project, :home),
notice: "Page not found"
@@ -92,17 +99,6 @@ class Projects::WikisController < Projects::ApplicationController
def git_access
end
- def preview_markdown
- result = PreviewMarkdownService.new(@project, current_user, params).execute
-
- render json: {
- body: view_context.markdown(result[:text], pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
- references: {
- users: result[:users]
- }
- }
- end
-
private
def load_project_wiki
@@ -110,7 +106,7 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
- @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
+ @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15))
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a738ca9f361..a784c6f402a 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,8 +1,10 @@
class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
+ include PreviewMarkdown
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
+ before_action :redirect_git_extension, only: [:show]
before_action :project, except: [:index, :new, :create]
before_action :repository, except: [:index, :new, :create]
before_action :assign_ref_vars, only: [:show], if: :repo_exists?
@@ -124,7 +126,7 @@ class ProjectsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :remove_project, @project)
::Projects::DestroyService.new(@project, current_user, {}).async_execute
- flash[:alert] = _("Project '%{project_name}' will be deleted.") % { project_name: @project.name_with_namespace }
+ flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.name_with_namespace }
redirect_to dashboard_projects_path, status: 302
rescue Projects::DestroyService::DestroyError => ex
@@ -258,18 +260,6 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json
end
- def preview_markdown
- result = PreviewMarkdownService.new(@project, current_user, params).execute
-
- render json: {
- body: view_context.markdown(result[:text]),
- references: {
- users: result[:users],
- commands: view_context.markdown(result[:commands])
- }
- }
- end
-
private
# Render project landing depending of which features are available
@@ -279,13 +269,15 @@ class ProjectsController < Projects::ApplicationController
def render_landing_page
if can?(current_user, :download_code, @project)
return render 'projects/no_repo' unless @project.repository_exists?
+
render 'projects/empty' if @project.empty_repo?
else
if @project.wiki_enabled?
@project_wiki = @project.wiki
@wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user)
- @issues = issues_collection.page(params[:page])
+ @finder_type = IssuesFinder
+ @issues = issuables_collection.page(params[:page])
@collection_type = 'Issue'
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
end
@@ -310,6 +302,8 @@ class ProjectsController < Projects::ApplicationController
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
+
+ Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
def project_params
@@ -400,4 +394,13 @@ class ProjectsController < Projects::ApplicationController
def project_export_enabled
render_404 unless current_application_settings.project_export_enabled?
end
+
+ def redirect_git_extension
+ # Redirect from
+ # localhost/group/project.git
+ # to
+ # localhost/group/project
+ #
+ redirect_to request.original_url.sub(/\.git\/?\Z/, '') if params[:format] == 'git'
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index c01be42c3ee..d79108c88fb 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -63,7 +63,7 @@ class SessionsController < Devise::SessionsController
user = User.admins.last
- return unless user && user.require_password_creation?
+ return unless user && user.require_password_creation_for_web?
Users::UpdateService.new(current_user, user: user).execute do |user|
@token = user.generate_reset_token
diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb
index f9496787b15..c8b4682e6dc 100644
--- a/app/controllers/snippets/notes_controller.rb
+++ b/app/controllers/snippets/notes_controller.rb
@@ -20,6 +20,7 @@ class Snippets::NotesController < ApplicationController
def snippet
PersonalSnippet.find_by(id: params[:snippet_id])
end
+ alias_method :noteable, :snippet
def note_params
super.merge(noteable_id: params[:snippet_id])
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index c1cdc7c9831..be2d3f638ff 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -4,6 +4,7 @@ class SnippetsController < ApplicationController
include SpammableActions
include SnippetsActions
include RendersBlob
+ include PreviewMarkdown
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
@@ -87,17 +88,6 @@ class SnippetsController < ApplicationController
redirect_to snippets_path, status: 302
end
- def preview_markdown
- result = PreviewMarkdownService.new(@project, current_user, params).execute
-
- render json: {
- body: view_context.markdown(result[:text], skip_project_check: true),
- references: {
- users: result[:users]
- }
- }
- end
-
protected
def snippet
diff --git a/app/controllers/unicorn_test_controller.rb b/app/controllers/unicorn_test_controller.rb
deleted file mode 100644
index ed04bd1f77d..00000000000
--- a/app/controllers/unicorn_test_controller.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# :nocov:
-if Rails.env.test?
- class UnicornTestController < ActionController::Base
- def pid
- render plain: Process.pid.to_s
- end
-
- def kill
- Process.kill(params[:signal], Process.pid)
- render plain: 'Bye!'
- end
- end
-end
-# :nocov:
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 4ee855806ab..5fca31b4956 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -108,6 +108,8 @@ class UsersController < ApplicationController
.references(:project)
.with_associations
.limit_recent(20, params[:offset])
+
+ Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
def load_projects
diff --git a/app/finders/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb
index b8f52e31926..c3f5358b577 100644
--- a/app/finders/autocomplete_users_finder.rb
+++ b/app/finders/autocomplete_users_finder.rb
@@ -45,7 +45,7 @@ class AutocompleteUsersFinder
def find_users
return users_from_project if project
- return group.users if group
+ return group.users_with_parents if group
return User.all if current_user
User.none
diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb
index 533076585c0..852eac3647d 100644
--- a/app/finders/branches_finder.rb
+++ b/app/finders/branches_finder.rb
@@ -23,7 +23,7 @@ class BranchesFinder
def filter_by_name(branches)
if search
- branches.select { |branch| branch.name.include?(search) }
+ branches.select { |branch| branch.name.upcase.include?(search.upcase) }
else
branches
end
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
new file mode 100644
index 00000000000..1a5f6063437
--- /dev/null
+++ b/app/finders/group_descendants_finder.rb
@@ -0,0 +1,153 @@
+# GroupDescendantsFinder
+#
+# Used to find and filter all subgroups and projects of a passed parent group
+# visible to a specified user.
+#
+# When passing a `filter` param, the search is performed over all nested levels
+# of the `parent_group`. All ancestors for a search result are loaded
+#
+# Arguments:
+# current_user: The user for which the children should be visible
+# parent_group: The group to find children of
+# params:
+# Supports all params that the `ProjectsFinder` and `GroupProjectsFinder`
+# support.
+#
+# filter: string - is aliased to `search` for consistency with the frontend
+# archived: string - `only` or `true`.
+# `non_archived` is passed to the `ProjectFinder`s if none
+# was given.
+class GroupDescendantsFinder
+ attr_reader :current_user, :parent_group, :params
+
+ def initialize(current_user: nil, parent_group:, params: {})
+ @current_user = current_user
+ @parent_group = parent_group
+ @params = params.reverse_merge(non_archived: params[:archived].blank?)
+ end
+
+ def execute
+ # The children array might be extended with the ancestors of projects when
+ # filtering. In that case, take the maximum so the array does not get limited
+ # Otherwise, allow paginating through all results
+ #
+ all_required_elements = children
+ all_required_elements |= ancestors_for_projects if params[:filter]
+ total_count = [all_required_elements.size, paginator.total_count].max
+
+ Kaminari.paginate_array(all_required_elements, total_count: total_count)
+ end
+
+ def has_children?
+ projects.any? || subgroups.any?
+ end
+
+ private
+
+ def children
+ @children ||= paginator.paginate(params[:page])
+ end
+
+ def paginator
+ @paginator ||= Gitlab::MultiCollectionPaginator.new(subgroups, projects,
+ per_page: params[:per_page])
+ end
+
+ def direct_child_groups
+ GroupsFinder.new(current_user,
+ parent: parent_group,
+ all_available: true).execute
+ end
+
+ def all_visible_descendant_groups
+ groups_table = Group.arel_table
+ visible_to_user = groups_table[:visibility_level]
+ .in(Gitlab::VisibilityLevel.levels_for_user(current_user))
+ if current_user
+ authorized_groups = GroupsFinder.new(current_user,
+ all_available: false)
+ .execute.as('authorized')
+ authorized_to_user = groups_table.project(1).from(authorized_groups)
+ .where(authorized_groups[:id].eq(groups_table[:id]))
+ .exists
+ visible_to_user = visible_to_user.or(authorized_to_user)
+ end
+
+ hierarchy_for_parent
+ .descendants
+ .where(visible_to_user)
+ end
+
+ def subgroups_matching_filter
+ all_visible_descendant_groups
+ .search(params[:filter])
+ end
+
+ # When filtering we want all to preload all the ancestors upto the specified
+ # parent group.
+ #
+ # - root
+ # - subgroup
+ # - nested-group
+ # - project
+ #
+ # So when searching 'project', on the 'subgroup' page we want to preload
+ # 'nested-group' but not 'subgroup' or 'root'
+ def ancestors_for_groups(base_for_ancestors)
+ Gitlab::GroupHierarchy.new(base_for_ancestors)
+ .base_and_ancestors(upto: parent_group.id)
+ end
+
+ def ancestors_for_projects
+ projects_to_load_ancestors_of = projects.where.not(namespace: parent_group)
+ groups_to_load_ancestors_of = Group.where(id: projects_to_load_ancestors_of.select(:namespace_id))
+ ancestors_for_groups(groups_to_load_ancestors_of)
+ .with_selects_for_list(archived: params[:archived])
+ end
+
+ def subgroups
+ return Group.none unless Group.supports_nested_groups?
+
+ # When filtering subgroups, we want to find all matches withing the tree of
+ # descendants to show to the user
+ groups = if params[:filter]
+ ancestors_for_groups(subgroups_matching_filter)
+ else
+ direct_child_groups
+ end
+ groups.with_selects_for_list(archived: params[:archived]).order_by(sort)
+ end
+
+ def direct_child_projects
+ GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params)
+ .execute
+ end
+
+ # Finds all projects nested under `parent_group` or any of its descendant
+ # groups
+ def projects_matching_filter
+ projects_nested_in_group = Project.where(namespace_id: hierarchy_for_parent.base_and_descendants.select(:id))
+ params_with_search = params.merge(search: params[:filter])
+
+ ProjectsFinder.new(params: params_with_search,
+ current_user: current_user,
+ project_ids_relation: projects_nested_in_group).execute
+ end
+
+ def projects
+ projects = if params[:filter]
+ projects_matching_filter
+ else
+ direct_child_projects
+ end
+ projects.with_route.order_by(sort)
+ end
+
+ def sort
+ params.fetch(:sort, 'id_asc')
+ end
+
+ def hierarchy_for_parent
+ @hierarchy ||= Gitlab::GroupHierarchy.new(Group.where(id: parent_group.id))
+ end
+end
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
index f2d3b90b8e2..6e8733bb49c 100644
--- a/app/finders/group_projects_finder.rb
+++ b/app/finders/group_projects_finder.rb
@@ -34,7 +34,6 @@ class GroupProjectsFinder < ProjectsFinder
else
collection_without_user
end
-
union(projects)
end
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 0c4c4b10fb6..0282b378d88 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -15,6 +15,8 @@
# Anonymous users will never return any `owned` groups. They will return all
# public groups instead, even if `all_available` is set to false.
class GroupsFinder < UnionFinder
+ include CustomAttributesFilter
+
def initialize(current_user = nil, params = {})
@current_user = current_user
@params = params
@@ -22,8 +24,12 @@ class GroupsFinder < UnionFinder
def execute
items = all_groups.map do |item|
- by_parent(item)
+ item = by_parent(item)
+ item = by_custom_attributes(item)
+
+ item
end
+
find_union(items, Group).with_route.order_id_desc
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 24c07f3dc70..b46ec5e5350 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -36,6 +36,7 @@ class IssuableFinder
iids
label_name
milestone_title
+ my_reaction_emoji
non_archived
project_id
scope
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 02eb983bf55..12157818bcd 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -104,8 +104,7 @@ class NotesFinder
query = @params[:search]
return notes unless query
- pattern = "%#{query}%"
- notes.where(Note.arel_table[:note].matches(pattern))
+ notes.search(query)
end
# Notes changed since last fetch
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index 760166b453f..d975f354a88 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -18,6 +18,7 @@ class PersonalAccessTokensFinder
def by_user(tokens)
return tokens unless @params[:user]
+
tokens.where(user: @params[:user])
end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index eac6095d8dc..005612ededc 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -18,6 +18,8 @@
# non_archived: boolean
#
class ProjectsFinder < UnionFinder
+ include CustomAttributesFilter
+
attr_accessor :params
attr_reader :current_user, :project_ids_relation
@@ -44,6 +46,7 @@ class ProjectsFinder < UnionFinder
collection = by_tags(collection)
collection = by_search(collection)
collection = by_archived(collection)
+ collection = by_custom_attributes(collection)
sort(collection)
end
diff --git a/app/finders/runner_jobs_finder.rb b/app/finders/runner_jobs_finder.rb
new file mode 100644
index 00000000000..52340f94523
--- /dev/null
+++ b/app/finders/runner_jobs_finder.rb
@@ -0,0 +1,22 @@
+class RunnerJobsFinder
+ attr_reader :runner, :params
+
+ def initialize(runner, params = {})
+ @runner = runner
+ @params = params
+ end
+
+ def execute
+ items = @runner.builds
+ items = by_status(items)
+ items
+ end
+
+ private
+
+ def by_status(items)
+ return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status])
+
+ items.where(status: params[:status])
+ end
+end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 8ad94d3f723..df590cf47c8 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -30,4 +30,11 @@ module AppearancesHelper
render 'shared/logo.svg'
end
end
+
+ # Skip the 'GitLab' type logo when custom brand logo is set
+ def brand_header_logo_type
+ unless brand_item && brand_item.header_logo?
+ render 'shared/logo_type.svg'
+ end
+ end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 8d02d5de5c3..4754a67450f 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -309,4 +309,8 @@ module ApplicationHelper
def show_new_repo?
cookies["new_repo"] == "true" && body_data_page != 'projects:show'
end
+
+ def locale_path
+ asset_path("locale/#{Gitlab::I18n.locale}/app.js")
+ end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 7bd34df5c95..dccde46fa33 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -3,9 +3,9 @@ module ApplicationSettingsHelper
include Gitlab::CurrentSettings
- delegate :gravatar_enabled?,
- :signup_enabled?,
- :password_authentication_enabled?,
+ delegate :allow_signup?,
+ :gravatar_enabled?,
+ :password_authentication_enabled_for_web?,
:akismet_enabled?,
:koding_enabled?,
to: :current_application_settings
@@ -30,9 +30,9 @@ module ApplicationSettingsHelper
def enabled_project_button(project, protocol)
case protocol
when 'ssh'
- ssh_clone_button(project, 'bottom', append_link: false)
+ ssh_clone_button(project, append_link: false)
else
- http_clone_button(project, 'bottom', append_link: false)
+ http_clone_button(project, append_link: false)
end
end
@@ -108,6 +108,43 @@ module ApplicationSettingsHelper
options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues)
end
+ def circuitbreaker_failure_count_help_text
+ health_link = link_to(s_('AdminHealthPageLink|health page'), admin_health_check_path)
+ api_link = link_to(s_('CircuitBreakerApiLink|circuitbreaker api'), help_page_path("api/repository_storage_health"))
+ message = _("The number of failures of after which GitLab will completely "\
+ "prevent access to the storage. The number of failures can be "\
+ "reset in the admin interface: %{link_to_health_page} or using "\
+ "the %{api_documentation_link}.")
+ message = message % { link_to_health_page: health_link, api_documentation_link: api_link }
+
+ message.html_safe
+ end
+
+ def circuitbreaker_access_retries_help_text
+ _('The number of attempts GitLab will make to access a storage.')
+ end
+
+ def circuitbreaker_backoff_threshold_help_text
+ _("The number of failures after which GitLab will start temporarily "\
+ "disabling access to a storage shard on a host")
+ end
+
+ def circuitbreaker_failure_wait_time_help_text
+ _("When access to a storage fails. GitLab will prevent access to the "\
+ "storage for the time specified here. This allows the filesystem to "\
+ "recover. Repositories on failing shards are temporarly unavailable")
+ end
+
+ def circuitbreaker_failure_reset_time_help_text
+ _("The time in seconds GitLab will keep failure information. When no "\
+ "failures occur during this time, information about the mount is reset.")
+ end
+
+ def circuitbreaker_storage_timeout_help_text
+ _("The time in seconds GitLab will try to access storage. After this time a "\
+ "timeout error will be raised.")
+ end
+
def visible_attributes
[
:admin_notification_email,
@@ -116,6 +153,12 @@ module ApplicationSettingsHelper
:akismet_api_key,
:akismet_enabled,
:auto_devops_enabled,
+ :circuitbreaker_access_retries,
+ :circuitbreaker_backoff_threshold,
+ :circuitbreaker_failure_count_threshold,
+ :circuitbreaker_failure_reset_time,
+ :circuitbreaker_failure_wait_time,
+ :circuitbreaker_storage_timeout,
:clientside_sentry_dsn,
:clientside_sentry_enabled,
:container_registry_token_expire_delay,
@@ -134,6 +177,9 @@ module ApplicationSettingsHelper
:ed25519_key_restriction,
:email_author_in_body,
:enabled_git_access_protocol,
+ :gitaly_timeout_default,
+ :gitaly_timeout_medium,
+ :gitaly_timeout_fast,
:gravatar_enabled,
:hashed_storage_enabled,
:help_page_hide_commercial_content,
@@ -160,7 +206,7 @@ module ApplicationSettingsHelper
:metrics_port,
:metrics_sample_interval,
:metrics_timeout,
- :password_authentication_enabled,
+ :password_authentication_enabled_for_web,
:performance_bar_allowed_group_id,
:performance_bar_enabled,
:plantuml_enabled,
@@ -188,6 +234,15 @@ module ApplicationSettingsHelper
:sign_in_text,
:signup_enabled,
:terminal_max_session_time,
+ :throttle_unauthenticated_enabled,
+ :throttle_unauthenticated_requests_per_period,
+ :throttle_unauthenticated_period_in_seconds,
+ :throttle_authenticated_web_enabled,
+ :throttle_authenticated_web_requests_per_period,
+ :throttle_authenticated_web_period_in_seconds,
+ :throttle_authenticated_api_enabled,
+ :throttle_authenticated_api_requests_per_period,
+ :throttle_authenticated_api_period_in_seconds,
:two_factor_grace_period,
:unique_ips_limit_enabled,
:unique_ips_limit_per_user,
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
index 483b957decb..ec6194d204f 100644
--- a/app/helpers/auto_devops_helper.rb
+++ b/app/helpers/auto_devops_helper.rb
@@ -8,9 +8,25 @@ module AutoDevopsHelper
!project.ci_service
end
+ def show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project)
+ return false if project.repository.gitlab_ci_yml
+
+ if project&.auto_devops&.enabled.present?
+ !project.auto_devops.enabled && current_application_settings.auto_devops_enabled?
+ else
+ current_application_settings.auto_devops_enabled?
+ end
+ end
+
+ def show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project)
+ return false if project.repository.gitlab_ci_yml
+
+ !project.auto_devops_enabled?
+ end
+
def auto_devops_warning_message(project)
missing_domain = !project.auto_devops&.has_domain?
- missing_service = !project.kubernetes_service&.active?
+ missing_service = !project.deployment_platform&.active?
if missing_service
params = {
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 7112c6ee470..c4a621160af 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -20,17 +20,6 @@ module BoardsHelper
project_issues_path(@project)
end
- def current_board_json
- board = @board || @boards.first
-
- board.to_json(
- only: [:id, :name, :milestone_id],
- include: {
- milestone: { only: [:title] }
- }
- )
- end
-
def board_base_url
project_boards_path(@project)
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 48cf30a48ab..d06cf2de2c3 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -56,42 +56,36 @@ module ButtonHelper
end
end
- def http_clone_button(project, placement = 'right', append_link: true)
- klass = 'http-selector'
- klass << ' has-tooltip' if current_user.try(:require_password_creation?) || current_user.try(:require_personal_access_token_creation_for_git_auth?)
-
+ def http_clone_button(project, append_link: true)
protocol = gitlab_config.protocol.upcase
+ dropdown_description = http_dropdown_description(protocol)
+ append_url = project.http_url_to_repo if append_link
+
+ dropdown_item_with_description(protocol, dropdown_description, href: append_url)
+ end
+
+ def http_dropdown_description(protocol)
+ if current_user.try(:require_password_creation_for_git?)
+ _("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
+ else
+ _("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
+ end
+ end
- tooltip_title =
- if current_user.try(:require_password_creation?)
- _("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
- else
- _("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
- end
+ def ssh_clone_button(project, append_link: true)
+ dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") if current_user.try(:require_ssh_key?)
+ append_url = project.ssh_url_to_repo if append_link
- content_tag (append_link ? :a : :span), protocol,
- class: klass,
- href: (project.http_url_to_repo if append_link),
- data: {
- html: true,
- placement: placement,
- container: 'body',
- title: tooltip_title
- }
+ dropdown_item_with_description('SSH', dropdown_description, href: append_url)
end
- def ssh_clone_button(project, placement = 'right', append_link: true)
- klass = 'ssh-selector'
- klass << ' has-tooltip' if current_user.try(:require_ssh_key?)
+ def dropdown_item_with_description(title, description, href: nil)
+ button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
+ button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
- content_tag (append_link ? :a : :span), 'SSH',
- class: klass,
- href: (project.ssh_url_to_repo if append_link),
- data: {
- html: true,
- placement: placement,
- container: 'body',
- title: _('Add an SSH key to your profile to pull or push via SSH.')
- }
+ content_tag (href ? :a : :span),
+ button_content,
+ class: "#{title.downcase}-selector",
+ href: (href if href)
end
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 8022547a6ad..636316da80a 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -6,11 +6,6 @@
# See 'detailed_status?` method and `Gitlab::Ci::Status` module.
#
module CiStatusHelper
- def ci_status_path(pipeline)
- project = pipeline.project
- project_pipeline_path(project, pipeline)
- end
-
def ci_label_for_status(status)
if detailed_status?(status)
return status.label
@@ -63,34 +58,34 @@ module CiStatusHelper
def ci_icon_for_status(status)
if detailed_status?(status)
- return custom_icon(status.icon)
+ return sprite_icon(status.icon)
end
icon_name =
case status
when 'success'
- 'icon_status_success'
+ 'status_success'
when 'success_with_warnings'
- 'icon_status_warning'
+ 'status_warning'
when 'failed'
- 'icon_status_failed'
+ 'status_failed'
when 'pending'
- 'icon_status_pending'
+ 'status_pending'
when 'running'
- 'icon_status_running'
+ 'status_running'
when 'play'
- 'icon_play'
+ 'play'
when 'created'
- 'icon_status_created'
+ 'status_created'
when 'skipped'
- 'icon_status_skipped'
+ 'status_skipped'
when 'manual'
- 'icon_status_manual'
+ 'status_manual'
else
- 'icon_status_canceled'
+ 'status_canceled'
end
- custom_icon(icon_name)
+ sprite_icon(icon_name, size: 16)
end
def pipeline_status_cache_key(pipeline_status)
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index ef22cafc2e2..f9a666fa1e6 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -60,23 +60,33 @@ module CommitsHelper
branches.include?(project.default_branch) ? branches.delete(project.default_branch) : branches.pop
end
+ # Returns a link formatted as a commit branch link
+ def commit_branch_link(url, text)
+ link_to(url, class: 'label label-gray ref-name branch-link') do
+ icon('code-fork') + " #{text}"
+ end
+ end
+
# Returns the sorted alphabetically links to branches, separated by a comma
def commit_branches_links(project, branches)
branches.sort.map do |branch|
- link_to(project_ref_path(project, branch), class: "label label-gray ref-name") do
- icon('code-fork') + " #{branch}"
- end
- end.join(" ").html_safe
+ commit_branch_link(project_ref_path(project, branch), branch)
+ end.join(' ').html_safe
+ end
+
+ # Returns a link formatted as a commit tag link
+ def commit_tag_link(url, text)
+ link_to(url, class: 'label label-gray ref-name') do
+ icon('tag') + " #{text}"
+ end
end
# Returns the sorted links to tags, separated by a comma
def commit_tags_links(project, tags)
sorted = VersionSorter.rsort(tags)
sorted.map do |tag|
- link_to(project_ref_path(project, tag), class: "label label-gray ref-name") do
- icon('tag') + " #{tag}"
- end
- end.join(" ").html_safe
+ commit_tag_link(project_ref_path(project, tag), tag)
+ end.join(' ').html_safe
end
def link_to_browse_code(project, commit)
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index 2c28dd81c87..8bf96c0905f 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -4,8 +4,8 @@ module CompareHelper
to.present? &&
from != to &&
can?(current_user, :create_merge_request, project) &&
- project.repository.branch_names.include?(from) &&
- project.repository.branch_names.include?(to)
+ project.repository.branch_exists?(from) &&
+ project.repository.branch_exists?(to)
end
def create_mr_path(from = params[:from], to = params[:to], project = @project)
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 4e4a66e8a02..e82136f0177 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -111,6 +111,7 @@ module DiffHelper
def diff_file_old_blob_raw_path(diff_file)
sha = diff_file.old_content_sha
return unless sha
+
project_raw_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path))
end
@@ -152,11 +153,11 @@ module DiffHelper
def diff_file_changed_icon(diff_file)
if diff_file.deleted_file? || diff_file.renamed_file?
- "minus"
+ "file-deletion"
elsif diff_file.new_file?
- "plus"
+ "file-addition"
else
- "adjust"
+ "file-modified"
end
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 5f11fe62030..878bc9b5c9c 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -24,6 +24,7 @@ module EmailsHelper
def action_title(url)
return unless url
+
%w(merge_requests issues commit).each do |action|
if url.split("/").include?(action)
return "View #{action.humanize.singularize}"
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index fd88e0d794a..079b3cd3aa0 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -172,16 +172,6 @@ module EventsHelper
end
end
- def event_note(text, options = {})
- text = first_line_in_markdown(text, 150, options)
-
- sanitize(
- text,
- tags: %w(a img gl-emoji b pre code p span),
- attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version']
- )
- end
-
def event_commit_title(message)
message ||= ''
(message.split("\n").first || "").truncate(70)
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index d4a91e533c1..a77aa0ad2cc 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -71,11 +71,13 @@ module GitlabRoutingHelper
project_commit_url(entity.project, entity.sha, *args)
end
- def preview_markdown_path(project, *args)
+ def preview_markdown_path(parent, *args)
+ return group_preview_markdown_path(parent) if parent.is_a?(Group)
+
if @snippet.is_a?(PersonalSnippet)
preview_markdown_snippets_path
else
- preview_markdown_project_path(project, *args)
+ preview_markdown_project_path(parent, *args)
end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 82bceddf1f0..676c1d1988b 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -7,7 +7,12 @@ module GroupsHelper
can?(current_user, :change_share_with_group_lock, group)
end
- def group_icon(group)
+ def group_icon(group, options = {})
+ img_path = group_icon_url(group, options)
+ image_tag img_path, options
+ end
+
+ def group_icon_url(group, options = {})
if group.is_a?(String)
group = Group.find_by_full_path(group)
end
@@ -89,7 +94,7 @@ module GroupsHelper
link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do
output =
if (group.try(:avatar_url) || show_avatar) && !Rails.env.test?
- image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15)
+ group_icon(group, class: "avatar-tile", width: 15, height: 15)
else
""
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index ec779c1c447..c6a83f21ceb 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -23,10 +23,17 @@ module IconsHelper
render "shared/icons/#{icon_name}.svg", size: size
end
+ def sprite_icon_path
+ # SVG Sprites currently don't work across domains, so in the case of a CDN
+ # we have to set the current path deliberately to prevent addition of asset_host
+ sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host
+ ActionController::Base.helpers.image_path('icons.svg', host: sprite_base_url)
+ end
+
def sprite_icon(icon_name, size: nil, css_class: nil)
css_classes = size ? "s#{size}" : ""
css_classes << " #{css_class}" unless css_class.blank?
- content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{image_path('icons.svg')}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
+ content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes)
end
def audit_icon(names, options = {})
diff --git a/app/helpers/instance_configuration_helper.rb b/app/helpers/instance_configuration_helper.rb
new file mode 100644
index 00000000000..cee319f20bc
--- /dev/null
+++ b/app/helpers/instance_configuration_helper.rb
@@ -0,0 +1,18 @@
+module InstanceConfigurationHelper
+ def instance_configuration_cell_html(value, &block)
+ return '-' unless value.to_s.presence
+
+ block_given? ? yield(value) : value
+ end
+
+ def instance_configuration_host(host)
+ @instance_configuration_host ||= instance_configuration_cell_html(host).capitalize
+ end
+
+ # Value must be in bytes
+ def instance_configuration_human_size_cell(value)
+ instance_configuration_cell_html(value) do |v|
+ number_to_human_size(v, strip_insignificant_zeros: true, significant: false)
+ end
+ end
+end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 7713fb0b9f8..4c60f4b0cd0 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -33,15 +33,17 @@ module IssuablesHelper
end
def serialize_issuable(issuable)
- case issuable
- when Issue
- IssueSerializer.new(current_user: current_user, project: issuable.project).represent(issuable).to_json
- when MergeRequest
- MergeRequestSerializer
- .new(current_user: current_user, project: issuable.project)
- .represent(issuable)
- .to_json
- end
+ serializer_klass = case issuable
+ when Issue
+ IssueSerializer
+ when MergeRequest
+ MergeRequestSerializer
+ end
+
+ serializer_klass
+ .new(current_user: current_user, project: issuable.project)
+ .represent(issuable)
+ .to_json
end
def template_dropdown_tag(issuable, &block)
@@ -209,15 +211,14 @@ module IssuablesHelper
def issuable_initial_data(issuable)
data = {
- endpoint: project_issue_path(@project, issuable),
- canUpdate: can?(current_user, :update_issue, issuable),
- canDestroy: can?(current_user, :destroy_issue, issuable),
+ endpoint: issuable_path(issuable),
+ updateEndpoint: "#{issuable_path(issuable)}.json",
+ canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable),
+ canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),
issuableRef: issuable.to_reference,
- markdownPreviewPath: preview_markdown_path(@project),
+ markdownPreviewPath: preview_markdown_path(parent),
markdownDocsPath: help_page_path('user/markdown'),
issuableTemplates: issuable_templates(issuable),
- projectPath: ref_project.path,
- projectNamespace: ref_project.namespace.full_path,
initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title,
initialDescriptionHtml: markdown_field(issuable, :description),
@@ -225,6 +226,12 @@ module IssuablesHelper
initialTaskStatus: issuable.task_status
}
+ if parent.is_a?(Group)
+ data[:groupPath] = parent.path
+ else
+ data.merge!(projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path)
+ end
+
data.merge!(updated_at_by(issuable))
data.to_json
@@ -243,8 +250,6 @@ module IssuablesHelper
end
def issuables_count_for_state(issuable_type, state)
- finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend
-
Gitlab::IssuablesCountForState.new(finder)[state]
end
@@ -261,12 +266,7 @@ module IssuablesHelper
end
def issuable_path(issuable, *options)
- case issuable
- when Issue
- issue_path(issuable, *options)
- when MergeRequest
- merge_request_path(issuable, *options)
- end
+ polymorphic_path(issuable, *options)
end
def issuable_url(issuable, *options)
@@ -314,20 +314,12 @@ module IssuablesHelper
@issuable_templates ||=
case issuable
when Issue
- issue_template_names
+ ref_project.repository.issue_template_names
when MergeRequest
- merge_request_template_names
+ ref_project.repository.merge_request_template_names
end
end
- def merge_request_template_names
- @merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project)
- end
-
- def issue_template_names
- @issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project)
- end
-
def selected_template(issuable)
params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
end
@@ -365,7 +357,8 @@ module IssuablesHelper
def issuable_sidebar_options(issuable, can_edit_issuable)
{
- endpoint: "#{issuable_json_path(issuable)}?basic=true",
+ endpoint: "#{issuable_json_path(issuable)}?serializer=sidebar",
+ toggleSubscriptionEndpoint: toggle_subscription_path(issuable),
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,
@@ -374,4 +367,8 @@ module IssuablesHelper
fullPath: @project.full_path
}
end
+
+ def parent
+ @project || @group
+ end
end
diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb
index 2c5619ac41b..603b9438e35 100644
--- a/app/helpers/lazy_image_tag_helper.rb
+++ b/app/helpers/lazy_image_tag_helper.rb
@@ -10,6 +10,7 @@ module LazyImageTagHelper
unless options.delete(:lazy) == false
options[:data] ||= {}
options[:data][:src] = path_to_image(source)
+
options[:class] ||= ""
options[:class] << " lazy"
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 46bced00c72..9d269cb65d6 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -53,6 +53,7 @@ module MarkupHelper
# text, wrapping anything found in the requested link
fragment.children.each do |node|
next unless node.text?
+
node.replace(link_to(node.text, url, html_options))
end
end
@@ -69,10 +70,16 @@ module MarkupHelper
# as Markdown. HTML tags in the parsed output are not counted toward the
# +max_chars+ limit. If the length limit falls within a tag's contents, then
# the tag contents are truncated without removing the closing tag.
- def first_line_in_markdown(text, max_chars = nil, options = {})
- md = markdown(text, options).strip
+ def first_line_in_markdown(object, attribute, max_chars = nil, options = {})
+ md = markdown_field(object, attribute, options)
+
+ text = truncate_visible(md, max_chars || md.length) if md.present?
- truncate_visible(md, max_chars || md.length) if md.present?
+ sanitize(
+ text,
+ tags: %w(a img gl-emoji b pre code p span),
+ attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version']
+ )
end
def markdown(text, context = {})
@@ -83,15 +90,17 @@ module MarkupHelper
prepare_for_rendering(html, context)
end
- def markdown_field(object, field)
+ def markdown_field(object, field, context = {})
object = object.for_display if object.respond_to?(:for_display)
redacted_field_html = object.try(:"redacted_#{field}_html")
return '' unless object.present?
return redacted_field_html if redacted_field_html
- html = Banzai.render_field(object, field)
- prepare_for_rendering(html, object.banzai_render_context(field))
+ html = Banzai.render_field(object, field, context)
+ context.reverse_merge!(object.banzai_render_context(field)) if object.respond_to?(:banzai_render_context)
+
+ prepare_for_rendering(html, context)
end
def markup(file_name, text, context = {})
@@ -213,12 +222,12 @@ module MarkupHelper
data = options[:data].merge({ container: 'body' })
content_tag :button,
type: 'button',
- class: 'toolbar-btn js-md has-tooltip hidden-xs',
+ class: 'toolbar-btn js-md has-tooltip',
tabindex: -1,
data: data,
title: options[:title],
aria: { label: options[:title] } do
- icon(options[:icon])
+ sprite_icon(options[:icon])
end
end
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index d7df9bb06d2..b78d3072186 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -4,8 +4,11 @@ module NamespacesHelper
end
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
- groups = current_user.owned_groups + current_user.masters_groups
- users = [current_user.namespace]
+ groups = current_user.manageable_groups
+ .joins(:route)
+ .includes(:route)
+ .order('routes.path')
+ 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'
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index a23a43c9f43..8ada746b244 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -1,7 +1,7 @@
module NavHelper
def page_with_sidebar_class
class_name = page_gutter_class
- class_name << 'page-with-new-sidebar' if defined?(@left_sidebar) && @left_sidebar
+ class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar
class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar
class_name
@@ -19,11 +19,7 @@ module NavHelper
end
elsif current_path?('jobs#show')
%w[page-gutter build-sidebar right-sidebar-expanded]
- elsif current_path?('wikis#show') ||
- current_path?('wikis#edit') ||
- current_path?('wikis#update') ||
- current_path?('wikis#history') ||
- current_path?('wikis#git_access')
+ elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access')
%w[page-gutter wiki-sidebar right-sidebar-expanded]
else
[]
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index fde961e2da4..3e42063224e 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -78,6 +78,7 @@ module NotificationsHelper
# Create hidden field to send notification setting source to controller
def hidden_setting_source_input(notification_setting)
return unless notification_setting.source_type
+
hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 0d7347ed30d..8e822ed0ea2 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -36,7 +36,8 @@ module PreferencesHelper
def project_view_choices
[
['Files and Readme (default)', :files],
- ['Activity', :activity]
+ ['Activity', :activity],
+ ['Readme', :readme]
]
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 20e050195ea..4a6b22b5ff6 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -15,7 +15,7 @@ module ProjectsHelper
end
def link_to_member_avatar(author, opts = {})
- default_opts = { size: 16 }
+ default_opts = { size: 16, lazy_load: false }
opts = default_opts.merge(opts)
classes = %W[avatar avatar-inline s#{opts[:size]}]
@@ -27,8 +27,26 @@ module ProjectsHelper
image_tag(src, width: opts[:size], class: classes, alt: '', "data-src" => avatar)
end
+ def author_content_tag(author, opts = {})
+ default_opts = { author_class: 'author', tooltip: false, by_username: false }
+ opts = default_opts.merge(opts)
+
+ has_tooltip = !opts[:by_username] && opts[:tooltip]
+
+ username = opts[:by_username] ? author.to_reference : author.name
+ name_tag_options = { class: [opts[:author_class]] }
+
+ if has_tooltip
+ name_tag_options[:title] = author.to_reference
+ name_tag_options[:data] = { placement: 'top' }
+ name_tag_options[:class] << 'has-tooltip'
+ end
+
+ content_tag(:span, sanitize(username), name_tag_options)
+ end
+
def link_to_member(project, author, opts = {}, &block)
- default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name", tooltip: false, lazy_load: false }
+ default_opts = { avatar: true, name: true, title: ":name" }
opts = default_opts.merge(opts)
return "(deleted)" unless author
@@ -39,12 +57,7 @@ module ProjectsHelper
author_html << link_to_member_avatar(author, opts) if opts[:avatar]
# Build name span tag
- if opts[:by_username]
- author_html << content_tag(:span, sanitize("@#{author.username}"), class: opts[:author_class]) if opts[:name]
- else
- tooltip_data = { placement: 'top' }
- author_html << content_tag(:span, sanitize(author.name), class: [opts[:author_class], ('has-tooltip' if opts[:tooltip])], title: (author.to_reference if opts[:tooltip]), data: (tooltip_data if opts[:tooltip])) if opts[:name]
- end
+ author_html << author_content_tag(author, opts) if opts[:name]
author_html << capture(&block) if block
@@ -97,7 +110,15 @@ module ProjectsHelper
def remove_fork_project_message(project)
_("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") %
- { forked_from_project: @project.forked_from_project.name_with_namespace }
+ { forked_from_project: fork_source_name(project) }
+ end
+
+ def fork_source_name(project)
+ if @project.fork_source
+ @project.fork_source.full_name
+ else
+ @project.fork_network&.deleted_root_project_name
+ end
end
def project_nav_tabs
@@ -127,8 +148,8 @@ module ProjectsHelper
def can_change_visibility_level?(project, current_user)
return false unless can?(current_user, :change_visibility_level, project)
- if project.forked?
- project.forked_from_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE
+ if project.fork_source
+ project.fork_source.visibility_level > Gitlab::VisibilityLevel::PRIVATE
else
true
end
@@ -213,11 +234,11 @@ module ProjectsHelper
def show_no_password_message?
cookies[:hide_no_password_message].blank? && !current_user.hide_no_password &&
- ( current_user.require_password_creation? || current_user.require_personal_access_token_creation_for_git_auth? )
+ current_user.require_extra_setup_for_git_auth?
end
def link_to_set_password
- if current_user.require_password_creation?
+ if current_user.require_password_creation_for_git?
link_to s_('SetPasswordToCloneLink|set a password'), edit_profile_password_path
else
link_to s_('CreateTokenToCloneLink|create a personal access token'), profile_personal_access_tokens_path
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index cf28a917fd1..2f57660516d 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -140,7 +140,8 @@ module SearchHelper
placeholder: 'Search or filter results...',
data: {
'username-params' => @users.to_json(only: [:id, :username])
- }
+ },
+ autocomplete: 'off'
}
if @project.present?
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 1b542ed2a96..b05eb93b465 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -42,6 +42,17 @@ module SortingHelper
options
end
+ def groups_sort_options_hash
+ options = {
+ sort_value_recently_created => sort_title_recently_created,
+ sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_recently_updated => sort_title_recently_updated,
+ sort_value_oldest_updated => sort_title_oldest_updated
+ }
+
+ options
+ end
+
def member_sort_options_hash
{
sort_value_access_level_asc => sort_title_access_level_asc,
diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb
index 544c9efb845..4d2180f7eee 100644
--- a/app/helpers/storage_health_helper.rb
+++ b/app/helpers/storage_health_helper.rb
@@ -16,17 +16,16 @@ module StorageHealthHelper
def message_for_circuit_breaker(circuit_breaker)
maximum_failures = circuit_breaker.failure_count_threshold
current_failures = circuit_breaker.failure_count
- permanently_broken = circuit_breaker.circuit_broken? && current_failures >= maximum_failures
translation_params = { number_of_failures: current_failures,
maximum_failures: maximum_failures,
number_of_seconds: circuit_breaker.failure_wait_time }
- if permanently_broken
+ if circuit_breaker.circuit_broken?
s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\
"retry automatically. Reset storage information when the problem is "\
"resolved.") % translation_params
- elsif circuit_breaker.circuit_broken?
+ elsif circuit_breaker.backing_off?
_("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\
"block access for %{number_of_seconds} seconds.") % translation_params
else
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index c4ea0f5ac53..5b2ea38a03d 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -1,14 +1,23 @@
module TreeHelper
+ FILE_LIMIT = 1_000
+
# Sorts a repository's tree so that folders are before files and renders
# their corresponding partials
#
- # contents - A Grit::Tree object for the current tree
+ # tree - A `Tree` object for the current tree
def render_tree(tree)
# Sort submodules and folders together by name ahead of files
folders, files, submodules = tree.trees, tree.blobs, tree.submodules
- tree = ""
+ tree = ''
items = (folders + submodules).sort_by(&:name) + files
- tree << render(partial: "projects/tree/tree_row", collection: items) if items.present?
+
+ if items.size > FILE_LIMIT
+ tree << render(partial: 'projects/tree/truncated_notice_tree_row',
+ locals: { limit: FILE_LIMIT, total: items.size })
+ items = items.take(FILE_LIMIT)
+ end
+
+ tree << render(partial: 'projects/tree/tree_row', collection: items) if items.present?
tree.html_safe
end
@@ -88,6 +97,7 @@ module TreeHelper
part_path = part if part_path.empty?
next if parts.count > max_links && !parts.last(2).include?(part)
+
yield(part, part_path)
end
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 46867d2d974..c3d5628f241 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -150,6 +150,7 @@ module VisibilityLevelHelper
def restricted_visibility_levels(show_all = false)
return [] if current_user.admin? && !show_all
+
current_application_settings.restricted_visibility_levels || []
end
@@ -159,6 +160,7 @@ module VisibilityLevelHelper
def disallowed_visibility_level?(form_model, level)
return false unless form_model.respond_to?(:visibility_level_allowed?)
+
!form_model.visibility_level_allowed?(level)
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index c0cc60d5ebf..3117c98c846 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -33,6 +33,8 @@ class ApplicationSetting < ActiveRecord::Base
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
+ default_value_for :id, 1
+
validates :uuid, presence: true
validates :session_expire_delay,
@@ -151,6 +153,46 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { greater_than_or_equal_to: 0 }
+ validates :circuitbreaker_backoff_threshold,
+ :circuitbreaker_failure_count_threshold,
+ :circuitbreaker_failure_wait_time,
+ :circuitbreaker_failure_reset_time,
+ :circuitbreaker_storage_timeout,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
+ validates :circuitbreaker_access_retries,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 1 }
+
+ validates_each :circuitbreaker_backoff_threshold do |record, attr, value|
+ if value.to_i >= record.circuitbreaker_failure_count_threshold
+ record.errors.add(attr, _("The circuitbreaker backoff threshold should be "\
+ "lower than the failure count threshold"))
+ end
+ end
+
+ validates :gitaly_timeout_default,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
+ validates :gitaly_timeout_medium,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :gitaly_timeout_medium,
+ numericality: { less_than_or_equal_to: :gitaly_timeout_default },
+ if: :gitaly_timeout_default
+ validates :gitaly_timeout_medium,
+ numericality: { greater_than_or_equal_to: :gitaly_timeout_fast },
+ if: :gitaly_timeout_fast
+
+ validates :gitaly_timeout_fast,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :gitaly_timeout_fast,
+ numericality: { less_than_or_equal_to: :gitaly_timeout_default },
+ if: :gitaly_timeout_default
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
@@ -194,7 +236,10 @@ class ApplicationSetting < ActiveRecord::Base
ensure_cache_setup
Rails.cache.fetch(CACHE_KEY) do
- ApplicationSetting.last
+ ApplicationSetting.last.tap do |settings|
+ # do not cache nils
+ raise 'missing settings' unless settings
+ end
end
rescue
# Fall back to an uncached value if there are any problems (e.g. redis down)
@@ -252,7 +297,8 @@ class ApplicationSetting < ActiveRecord::Base
koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
- password_authentication_enabled: Settings.gitlab['password_authentication_enabled'],
+ password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'],
+ password_authentication_enabled_for_git: true,
performance_bar_allowed_group_id: nil,
rsa_key_restriction: 0,
plantuml_enabled: false,
@@ -271,10 +317,22 @@ class ApplicationSetting < ActiveRecord::Base
sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0,
+ throttle_unauthenticated_enabled: false,
+ throttle_unauthenticated_requests_per_period: 3600,
+ throttle_unauthenticated_period_in_seconds: 3600,
+ throttle_authenticated_web_enabled: false,
+ throttle_authenticated_web_requests_per_period: 7200,
+ throttle_authenticated_web_period_in_seconds: 3600,
+ throttle_authenticated_api_enabled: false,
+ throttle_authenticated_api_requests_per_period: 7200,
+ throttle_authenticated_api_period_in_seconds: 3600,
two_factor_grace_period: 48,
user_default_external: false,
polling_interval_multiplier: 1,
- usage_ping_enabled: Settings.gitlab['usage_ping_enabled']
+ usage_ping_enabled: Settings.gitlab['usage_ping_enabled'],
+ gitaly_timeout_fast: 10,
+ gitaly_timeout_medium: 30,
+ gitaly_timeout_default: 55
}
end
@@ -396,7 +454,7 @@ class ApplicationSetting < ActiveRecord::Base
# the enabling/disabling is `performance_bar_allowed_group_id`
# - If `enable` is false, we set `performance_bar_allowed_group_id` to `nil`
def performance_bar_enabled=(enable)
- return if enable
+ return if Gitlab::Utils.to_boolean(enable)
self.performance_bar_allowed_group_id = nil
end
@@ -441,6 +499,14 @@ class ApplicationSetting < ActiveRecord::Base
has_attribute?(attr_name) ? public_send(attr_name) : FORBIDDEN_KEY_VALUE # rubocop:disable GitlabSecurity/PublicSend
end
+ def allow_signup?
+ signup_enabled? && password_authentication_enabled_for_web?
+ end
+
+ def password_authentication_enabled?
+ password_authentication_enabled_for_web? || password_authentication_enabled_for_git?
+ end
+
private
def ensure_uuid!
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 954d4e4d779..29e762724e3 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -76,12 +76,24 @@ class Blob < SimpleDelegator
new(blob, project)
end
+ def self.lazy(project, commit_id, path)
+ BatchLoader.for(commit_id: commit_id, path: path).batch do |items, loader|
+ project.repository.blobs_at(items.map(&:values)).each do |blob|
+ loader.call({ commit_id: blob.commit_id, path: blob.path }, blob) if blob
+ end
+ end
+ end
+
def initialize(blob, project = nil)
@project = project
super(blob)
end
+ def inspect
+ "#<#{self.class.name} oid:#{id[0..8]} commit:#{commit_id[0..8]} path:#{path}>"
+ end
+
# Returns the data of the blob.
#
# If the blob is a text based blob the content is converted to UTF-8 and any
@@ -95,7 +107,10 @@ class Blob < SimpleDelegator
end
def load_all_data!
- super(project.repository) if project
+ # Endpoint needed: gitlab-org/gitaly#756
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ super(project.repository) if project
+ end
end
def no_highlighting?
@@ -156,7 +171,9 @@ class Blob < SimpleDelegator
end
def file_type
- Gitlab::FileDetector.type_of(path)
+ name = File.basename(path)
+
+ Gitlab::FileDetector.type_of(path) || Gitlab::FileDetector.type_of(name)
end
def video?
diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb
index 8b66531ec7b..ec56cc53aea 100644
--- a/app/models/ci/artifact_blob.rb
+++ b/app/models/ci/artifact_blob.rb
@@ -2,7 +2,7 @@ module Ci
class ArtifactBlob
include BlobLike
- EXTENTIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json].freeze
+ EXTENSIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json].freeze
attr_reader :entry
@@ -36,17 +36,22 @@ module Ci
def external_url(project, job)
return unless external_link?(job)
- components = project.full_path_components
- components << "-/jobs/#{job.id}/artifacts/file/#{path}"
- artifact_path = components[1..-1].join('/')
+ full_path_parts = project.full_path_components
+ top_level_group = full_path_parts.shift
- "#{pages_config.protocol}://#{components[0]}.#{pages_config.host}/#{artifact_path}"
+ artifact_path = [
+ '-', *full_path_parts, '-',
+ 'jobs', job.id,
+ 'artifacts', path
+ ].join('/')
+
+ "#{pages_config.protocol}://#{top_level_group}.#{pages_config.host}/#{artifact_path}"
end
def external_link?(job)
pages_config.enabled &&
pages_config.artifacts_server &&
- EXTENTIONS_SERVED_BY_PAGES.include?(File.extname(name)) &&
+ EXTENSIONS_SERVED_BY_PAGES.include?(File.extname(name)) &&
job.project.public?
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 6ca46ae89c1..4ea040dfad5 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -104,6 +104,7 @@ module Ci
end
before_transition any => [:failed] do |build|
+ next unless build.project
next if build.retries_max.zero?
if build.retries_count < build.retries_max
@@ -192,6 +193,10 @@ module Ci
project.build_timeout
end
+ def triggered_by?(current_user)
+ user == current_user
+ end
+
# A slugified version of the build ref, suitable for inclusion in URLs and
# domain names. Rules:
#
@@ -239,7 +244,7 @@ module Ci
@merge_request ||=
begin
- merge_requests = MergeRequest.includes(:merge_request_diff)
+ merge_requests = MergeRequest.includes(:latest_merge_request_diff)
.where(source_branch: ref,
source_project: pipeline.project)
.reorder(iid: :desc)
@@ -313,6 +318,7 @@ module Ci
def execute_hooks
return unless project
+
build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :job_hooks)
project.execute_services(build_data.dup, :job_hooks)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index cf3ce3c9e54..fd64670f6b0 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -66,8 +66,8 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
- transition created: :pending
- transition [:success, :failed, :canceled, :skipped] => :running
+ transition [:created, :skipped] => :pending
+ transition [:success, :failed, :canceled] => :running
end
event :run do
@@ -149,34 +149,70 @@ module Ci
end
end
- # ref can't be HEAD or SHA, can only be branch/tag name
- scope :latest, ->(ref = nil) do
- max_id = unscope(:select)
- .select("max(#{quoted_table_name}.id)")
- .group(:ref, :sha)
+ scope :internal, -> { where(source: internal_sources) }
- if ref
- where(ref: ref, id: max_id.where(ref: ref))
- else
- where(id: max_id)
- end
+ # Returns the pipelines in descending order (= newest first), optionally
+ # limited to a number of references.
+ #
+ # ref - The name (or names) of the branch(es)/tag(s) to limit the list of
+ # pipelines to.
+ def self.newest_first(ref = nil)
+ relation = order(id: :desc)
+
+ ref ? relation.where(ref: ref) : relation
end
- scope :internal, -> { where(source: internal_sources) }
def self.latest_status(ref = nil)
- latest(ref).status
+ newest_first(ref).pluck(:status).first
end
def self.latest_successful_for(ref)
- success.latest(ref).order(id: :desc).first
+ newest_first(ref).success.take
end
def self.latest_successful_for_refs(refs)
- success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash|
+ relation = newest_first(refs).success
+
+ relation.each_with_object({}) do |pipeline, hash|
hash[pipeline.ref] ||= pipeline
end
end
+ # Returns a Hash containing the latest pipeline status for every given
+ # commit.
+ #
+ # The keys of this Hash are the commit SHAs, the values the statuses.
+ #
+ # commits - The list of commit SHAs to get the status for.
+ # ref - The ref to scope the data to (e.g. "master"). If the ref is not
+ # given we simply get the latest status for the commits, regardless
+ # of what refs their pipelines belong to.
+ def self.latest_status_per_commit(commits, ref = nil)
+ p1 = arel_table
+ p2 = arel_table.alias
+
+ # This LEFT JOIN will filter out all but the newest row for every
+ # combination of (project_id, sha) or (project_id, sha, ref) if a ref is
+ # given.
+ cond = p1[:sha].eq(p2[:sha])
+ .and(p1[:project_id].eq(p2[:project_id]))
+ .and(p1[:id].lt(p2[:id]))
+
+ cond = cond.and(p1[:ref].eq(p2[:ref])) if ref
+ join = p1.join(p2, Arel::Nodes::OuterJoin).on(cond)
+
+ relation = select(:sha, :status)
+ .where(sha: commits)
+ .where(p2[:id].eq(nil))
+ .joins(join.join_sources)
+
+ relation = relation.where(ref: ref) if ref
+
+ relation.each_with_object({}) do |row, hash|
+ hash[row[:sha]] = row[:status]
+ end
+ end
+
def self.truncate_sha(sha)
sha[0...8]
end
@@ -249,9 +285,7 @@ module Ci
end
def commit
- @commit ||= project.commit(sha)
- rescue
- nil
+ @commit ||= project.commit_by(oid: sha)
end
def branch?
@@ -302,8 +336,10 @@ module Ci
def latest?
return false unless ref
+
commit = project.commit(ref)
return false unless commit
+
commit.sha == sha
end
@@ -329,7 +365,7 @@ module Ci
end
def has_kubernetes_active?
- project.kubernetes_service&.active?
+ project.deployment_platform&.active?
end
def has_stage_seeds?
@@ -411,7 +447,7 @@ module Ci
end
def notes
- Note.for_commit_id(sha)
+ project.notes.for_commit_id(sha)
end
def process!
@@ -471,7 +507,10 @@ module Ci
end
def latest_builds_with_artifacts
- @latest_builds_with_artifacts ||= builds.latest.with_artifacts
+ # We purposely cast the builds to an Array here. Because we always use the
+ # rows if there are more than 0 this prevents us from having to run two
+ # queries: one to get the count and one to get the rows.
+ @latest_builds_with_artifacts ||= builds.latest.with_artifacts.to_a
end
private
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index c6509f89117..d39610a8995 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -1,6 +1,7 @@
module Ci
class Runner < ActiveRecord::Base
extend Gitlab::Ci::Model
+ include Gitlab::SQL::Pattern
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour
@@ -59,10 +60,7 @@ module Ci
#
# Returns an ActiveRecord::Relation.
def self.search(query)
- t = arel_table
- pattern = "%#{query}%"
-
- where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
+ fuzzy_search(query, [:token, :description])
end
def self.contact_time_deadline
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
new file mode 100644
index 00000000000..c7949d11ef8
--- /dev/null
+++ b/app/models/clusters/applications/helm.rb
@@ -0,0 +1,35 @@
+module Clusters
+ module Applications
+ class Helm < ActiveRecord::Base
+ self.table_name = 'clusters_applications_helm'
+
+ include ::Clusters::Concerns::ApplicationStatus
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
+
+ default_value_for :version, Gitlab::Kubernetes::Helm::HELM_VERSION
+
+ validates :cluster, presence: true
+
+ after_initialize :set_initial_status
+
+ def self.application_name
+ self.to_s.demodulize.underscore
+ end
+
+ def set_initial_status
+ return unless not_installable?
+
+ self.status = 'installable' if cluster&.platform_kubernetes_active?
+ end
+
+ def name
+ self.class.application_name
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(name, true)
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
new file mode 100644
index 00000000000..44bd979741e
--- /dev/null
+++ b/app/models/clusters/applications/ingress.rb
@@ -0,0 +1,44 @@
+module Clusters
+ module Applications
+ class Ingress < ActiveRecord::Base
+ self.table_name = 'clusters_applications_ingress'
+
+ include ::Clusters::Concerns::ApplicationStatus
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
+
+ validates :cluster, presence: true
+
+ default_value_for :ingress_type, :nginx
+ default_value_for :version, :nginx
+
+ after_initialize :set_initial_status
+
+ enum ingress_type: {
+ nginx: 1
+ }
+
+ def self.application_name
+ self.to_s.demodulize.underscore
+ end
+
+ def set_initial_status
+ return unless not_installable?
+
+ self.status = 'installable' if cluster&.application_helm_installed?
+ end
+
+ def name
+ self.class.application_name
+ end
+
+ def chart
+ 'stable/nginx-ingress'
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(name, false, chart)
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
new file mode 100644
index 00000000000..6d7fb4b7dbf
--- /dev/null
+++ b/app/models/clusters/cluster.rb
@@ -0,0 +1,95 @@
+module Clusters
+ class Cluster < ActiveRecord::Base
+ include Presentable
+
+ self.table_name = 'clusters'
+
+ APPLICATIONS = {
+ Applications::Helm.application_name => Applications::Helm,
+ Applications::Ingress.application_name => Applications::Ingress
+ }.freeze
+
+ belongs_to :user
+
+ has_many :cluster_projects, class_name: 'Clusters::Project'
+ has_many :projects, through: :cluster_projects, class_name: '::Project'
+
+ # we force autosave to happen when we save `Cluster` model
+ has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
+
+ has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes'
+
+ has_one :application_helm, class_name: 'Clusters::Applications::Helm'
+ has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
+
+ accepts_nested_attributes_for :provider_gcp, update_only: true
+ accepts_nested_attributes_for :platform_kubernetes, update_only: true
+
+ validates :name, cluster_name: true
+ validate :restrict_modification, on: :update
+
+ delegate :status, to: :provider, allow_nil: true
+ delegate :status_reason, to: :provider, allow_nil: true
+ delegate :on_creation?, to: :provider, allow_nil: true
+
+ delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
+ delegate :installed?, to: :application_helm, prefix: true, allow_nil: true
+
+ enum platform_type: {
+ kubernetes: 1
+ }
+
+ enum provider_type: {
+ user: 0,
+ gcp: 1
+ }
+
+ scope :enabled, -> { where(enabled: true) }
+ scope :disabled, -> { where(enabled: false) }
+
+ def status_name
+ if provider
+ provider.status_name
+ else
+ :created
+ end
+ end
+
+ def applications
+ [
+ application_helm || build_application_helm,
+ application_ingress || build_application_ingress
+ ]
+ end
+
+ def provider
+ return provider_gcp if gcp?
+ end
+
+ def platform
+ return platform_kubernetes if kubernetes?
+ end
+
+ def first_project
+ return @first_project if defined?(@first_project)
+
+ @first_project = projects.first
+ end
+ alias_method :project, :first_project
+
+ def kubeclient
+ platform_kubernetes.kubeclient if kubernetes?
+ end
+
+ private
+
+ def restrict_modification
+ if provider&.on_creation?
+ errors.add(:base, "cannot modify during creation")
+ return false
+ end
+
+ true
+ end
+ end
+end
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
new file mode 100644
index 00000000000..7b7c8eac773
--- /dev/null
+++ b/app/models/clusters/concerns/application_status.rb
@@ -0,0 +1,43 @@
+module Clusters
+ module Concerns
+ module ApplicationStatus
+ extend ActiveSupport::Concern
+
+ included do
+ state_machine :status, initial: :not_installable do
+ state :not_installable, value: -2
+ state :errored, value: -1
+ state :installable, value: 0
+ state :scheduled, value: 1
+ state :installing, value: 2
+ state :installed, value: 3
+
+ event :make_scheduled do
+ transition [:installable, :errored] => :scheduled
+ end
+
+ event :make_installing do
+ transition [:scheduled] => :installing
+ end
+
+ event :make_installed do
+ transition [:installing] => :installed
+ end
+
+ event :make_errored do
+ transition any => :errored
+ end
+
+ before_transition any => [:scheduled] do |app_status, _|
+ app_status.status_reason = nil
+ end
+
+ before_transition any => [:errored] do |app_status, transition|
+ status_reason = transition.args.first
+ app_status.status_reason = status_reason if status_reason
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
new file mode 100644
index 00000000000..7ab670cf1ef
--- /dev/null
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -0,0 +1,178 @@
+module Clusters
+ module Platforms
+ class Kubernetes < ActiveRecord::Base
+ include Gitlab::CurrentSettings
+ include Gitlab::Kubernetes
+ include ReactiveCaching
+
+ self.table_name = 'cluster_platforms_kubernetes'
+ self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.id] }
+
+ belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster'
+
+ attr_encrypted :password,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ before_validation :enforce_namespace_to_lower_case
+
+ validates :namespace,
+ allow_blank: true,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned)
+ validates :api_url, url: true, presence: true
+ validates :token, presence: true
+
+ after_save :clear_reactive_cache!
+
+ alias_attribute :ca_pem, :ca_cert
+
+ delegate :project, to: :cluster, allow_nil: true
+ delegate :enabled?, to: :cluster, allow_nil: true
+
+ alias_method :active?, :enabled?
+
+ def actual_namespace
+ if namespace.present?
+ namespace
+ else
+ default_namespace
+ end
+ end
+
+ def predefined_variables
+ config = YAML.dump(kubeconfig)
+
+ variables = [
+ { key: 'KUBE_URL', value: api_url, public: true },
+ { key: 'KUBE_TOKEN', value: token, public: false },
+ { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true },
+ { key: 'KUBECONFIG', value: config, public: false, file: true }
+ ]
+
+ if ca_pem.present?
+ variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true }
+ variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
+ end
+
+ variables
+ end
+
+ # Constructs a list of terminals from the reactive cache
+ #
+ # Returns nil if the cache is empty, in which case you should try again a
+ # short time later
+ def terminals(environment)
+ with_reactive_cache do |data|
+ pods = filter_by_label(data[:pods], app: environment.slug)
+ terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }
+ terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
+ end
+ end
+
+ # Caches resources in the namespace so other calls don't need to block on
+ # network access
+ def calculate_reactive_cache
+ return unless enabled? && project && !project.pending_delete?
+
+ # We may want to cache extra things in the future
+ { pods: read_pods }
+ end
+
+ def kubeclient
+ @kubeclient ||= build_kubeclient!
+ end
+
+ private
+
+ def kubeconfig
+ to_kubeconfig(
+ url: api_url,
+ namespace: actual_namespace,
+ token: token,
+ ca_pem: ca_pem)
+ end
+
+ def default_namespace
+ return unless project
+
+ slug = "#{project.path}-#{project.id}".downcase
+ slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
+ end
+
+ def build_kubeclient!(api_path: 'api', api_version: 'v1')
+ raise "Incomplete settings" unless api_url && actual_namespace
+
+ unless (username && password) || token
+ raise "Either username/password or token is required to access API"
+ end
+
+ ::Kubeclient::Client.new(
+ join_api_url(api_path),
+ api_version,
+ auth_options: kubeclient_auth_options,
+ ssl_options: kubeclient_ssl_options,
+ http_proxy_uri: ENV['http_proxy']
+ )
+ end
+
+ # Returns a hash of all pods in the namespace
+ def read_pods
+ kubeclient = build_kubeclient!
+
+ kubeclient.get_pods(namespace: actual_namespace).as_json
+ rescue KubeException => err
+ raise err unless err.error_code == 404
+
+ []
+ end
+
+ def kubeclient_ssl_options
+ opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
+
+ if ca_pem.present?
+ opts[:cert_store] = OpenSSL::X509::Store.new
+ opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
+ end
+
+ opts
+ end
+
+ def kubeclient_auth_options
+ { bearer_token: token }
+ end
+
+ def join_api_url(api_path)
+ url = URI.parse(api_url)
+ prefix = url.path.sub(%r{/+\z}, '')
+
+ url.path = [prefix, api_path].join("/")
+
+ url.to_s
+ end
+
+ def terminal_auth
+ {
+ token: token,
+ ca_pem: ca_pem,
+ max_session_time: current_application_settings.terminal_max_session_time
+ }
+ end
+
+ def enforce_namespace_to_lower_case
+ self.namespace = self.namespace&.downcase
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/project.rb b/app/models/clusters/project.rb
new file mode 100644
index 00000000000..eeb734b20b8
--- /dev/null
+++ b/app/models/clusters/project.rb
@@ -0,0 +1,8 @@
+module Clusters
+ class Project < ActiveRecord::Base
+ self.table_name = 'cluster_projects'
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster'
+ belongs_to :project, class_name: '::Project'
+ end
+end
diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb
new file mode 100644
index 00000000000..7fac32466ab
--- /dev/null
+++ b/app/models/clusters/providers/gcp.rb
@@ -0,0 +1,80 @@
+module Clusters
+ module Providers
+ class Gcp < ActiveRecord::Base
+ self.table_name = 'cluster_providers_gcp'
+
+ belongs_to :cluster, inverse_of: :provider_gcp, class_name: 'Clusters::Cluster'
+
+ default_value_for :zone, 'us-central1-a'
+ default_value_for :num_nodes, 3
+ default_value_for :machine_type, 'n1-standard-2'
+
+ attr_encrypted :access_token,
+ mode: :per_attribute_iv,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ validates :gcp_project_id,
+ length: 1..63,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
+ validates :zone, presence: true
+
+ validates :num_nodes,
+ presence: true,
+ numericality: {
+ only_integer: true,
+ greater_than: 0
+ }
+
+ state_machine :status, initial: :scheduled do
+ state :scheduled, value: 1
+ state :creating, value: 2
+ state :created, value: 3
+ state :errored, value: 4
+
+ event :make_creating do
+ transition any - [:creating] => :creating
+ end
+
+ event :make_created do
+ transition any - [:created] => :created
+ end
+
+ event :make_errored do
+ transition any - [:errored] => :errored
+ end
+
+ before_transition any => [:errored, :created] do |provider|
+ provider.access_token = nil
+ provider.operation_id = nil
+ end
+
+ before_transition any => [:creating] do |provider, transition|
+ operation_id = transition.args.first
+ raise ArgumentError.new('operation_id is required') unless operation_id.present?
+
+ provider.operation_id = operation_id
+ end
+
+ before_transition any => [:errored] do |provider, transition|
+ status_reason = transition.args.first
+ provider.status_reason = status_reason if status_reason
+ end
+ end
+
+ def on_creation?
+ scheduled? || creating?
+ end
+
+ def api_client
+ return unless access_token
+
+ @api_client ||= GoogleApi::CloudPlatform::Client.new(access_token, nil)
+ end
+ end
+ end
+end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 6dba154a6ea..6b28d290f99 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -80,10 +80,11 @@ class Commit
@raw = raw_commit
@project = project
+ @statuses = {}
end
def id
- @raw.id
+ raw.id
end
def ==(other)
@@ -108,12 +109,12 @@ class Commit
@link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/)
end
- def to_reference(from_project = nil, full: false)
- commit_reference(from_project, id, full: full)
+ def to_reference(from = nil, full: false)
+ commit_reference(from, id, full: full)
end
- def reference_link_text(from_project = nil, full: false)
- commit_reference(from_project, short_id, full: full)
+ def reference_link_text(from = nil, full: false)
+ commit_reference(from, short_id, full: full)
end
def diff_line_count
@@ -236,11 +237,13 @@ class Commit
end
def status(ref = nil)
- @statuses ||= {}
-
return @statuses[ref] if @statuses.key?(ref)
- @statuses[ref] = pipelines.latest_status(ref)
+ @statuses[ref] = project.pipelines.latest_status_per_commit(id, ref)[id]
+ end
+
+ def set_status_for_ref(ref, status)
+ @statuses[ref] = status
end
def signature
@@ -358,7 +361,7 @@ class Commit
@deltas ||= raw.deltas
end
- def diffs(diff_options = nil)
+ def diffs(diff_options = {})
Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
end
@@ -378,8 +381,8 @@ class Commit
private
- def commit_reference(from_project, referable_commit_id, full: false)
- reference = project.to_reference(from_project, full: full)
+ def commit_reference(from, referable_commit_id, full: false)
+ reference = project.to_reference(from, full: full)
if reference.present?
"#{reference}#{self.class.reference_prefix}#{referable_commit_id}"
diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb
new file mode 100644
index 00000000000..dd93af9df64
--- /dev/null
+++ b/app/models/commit_collection.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# A collection of Commit instances for a specific project and Git reference.
+class CommitCollection
+ include Enumerable
+
+ attr_reader :project, :ref, :commits
+
+ # project - The project the commits belong to.
+ # commits - The Commit instances to store.
+ # ref - The name of the ref (e.g. "master").
+ def initialize(project, commits, ref = nil)
+ @project = project
+ @commits = commits
+ @ref = ref
+ end
+
+ def each(&block)
+ commits.each(&block)
+ end
+
+ # Sets the pipeline status for every commit.
+ #
+ # Setting this status ahead of time removes the need for running a query for
+ # every commit we're displaying.
+ def with_pipeline_status
+ statuses = project.pipelines.latest_status_per_commit(map(&:id), ref)
+
+ each do |commit|
+ commit.set_status_for_ref(ref, statuses[commit.id])
+ end
+
+ self
+ end
+
+ def respond_to_missing?(message, inc_private = false)
+ commits.respond_to?(message, inc_private)
+ end
+
+ # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(message, *args, &block)
+ commits.public_send(message, *args, &block)
+ end
+end
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 84e2e8a5dd5..b93c111dabc 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -89,8 +89,8 @@ class CommitRange
alias_method :id, :to_s
- def to_reference(from_project = nil, full: false)
- project_reference = project.to_reference(from_project, full: full)
+ def to_reference(from = nil, full: false)
+ project_reference = project.to_reference(from, full: full)
if project_reference.present?
project_reference + self.class.reference_prefix + self.id
@@ -99,8 +99,8 @@ class CommitRange
end
end
- def reference_link_text(from_project = nil)
- project_reference = project.to_reference(from_project)
+ def reference_link_text(from = nil)
+ project_reference = project.to_reference(from)
reference = ref_from + notation + ref_to
if project_reference.present?
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index f3888528940..ee21ed8e420 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -14,10 +14,10 @@ class CommitStatus < ActiveRecord::Base
delegate :sha, :short_sha, to: :pipeline
validates :pipeline, presence: true, unless: :importing?
-
validates :name, presence: true, unless: :importing?
alias_attribute :author, :user
+ alias_attribute :pipeline_id, :commit_id
scope :failed_but_allowed, -> do
where(allow_failure: true, status: [:failed, :canceled])
@@ -46,6 +46,17 @@ class CommitStatus < ActiveRecord::Base
runner_system_failure: 4
}
+ ##
+ # We still create some CommitStatuses outside of CreatePipelineService.
+ #
+ # These are pages deployments and external statuses.
+ #
+ before_create unless: :importing? do
+ Ci::EnsureStageService.new(project, user).execute(self) do |stage|
+ self.run_after_commit { StageUpdateWorker.perform_async(stage.id) }
+ end
+ end
+
state_machine :status do
event :process do
transition [:skipped, :manual] => :created
@@ -93,26 +104,29 @@ class CommitStatus < ActiveRecord::Base
end
after_transition do |commit_status, transition|
+ next unless commit_status.project
next if transition.loopback?
commit_status.run_after_commit do
- if pipeline
+ if pipeline_id
if complete? || manual?
- PipelineProcessWorker.perform_async(pipeline.id)
+ PipelineProcessWorker.perform_async(pipeline_id)
else
- PipelineUpdateWorker.perform_async(pipeline.id)
+ PipelineUpdateWorker.perform_async(pipeline_id)
end
end
- StageUpdateWorker.perform_async(commit_status.stage_id)
- ExpireJobCacheWorker.perform_async(commit_status.id)
+ StageUpdateWorker.perform_async(stage_id)
+ ExpireJobCacheWorker.perform_async(id)
end
end
after_transition any => :failed do |commit_status|
+ next unless commit_status.project
+
commit_status.run_after_commit do
MergeRequests::AddTodoWhenBuildFailsService
- .new(pipeline.project, nil).execute(self)
+ .new(project, nil).execute(self)
end
end
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 8fbfed11bdf..10659030910 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -4,15 +4,26 @@ module Avatarable
def avatar_path(only_path: true)
return unless self[:avatar].present?
- # If only_path is true then use the relative path of avatar.
- # Otherwise use full path (including host).
asset_host = ActionController::Base.asset_host
- gitlab_host = only_path ? gitlab_config.relative_url_root : gitlab_config.url
+ use_asset_host = asset_host.present?
- # If asset_host is set then it is expected that assets are handled by a standalone host.
- # That means we do not want to get GitLab's relative_url_root option anymore.
- host = asset_host.present? ? asset_host : gitlab_host
+ # Avatars for private and internal groups and projects require authentication to be viewed,
+ # which means they can only be served by Rails, on the regular GitLab host.
+ # If an asset host is configured, we need to return the fully qualified URL
+ # instead of only the avatar path, so that Rails doesn't prefix it with the asset host.
+ if use_asset_host && respond_to?(:public?) && !public?
+ use_asset_host = false
+ only_path = false
+ end
- [host, avatar.url].join
+ url_base = ""
+ if use_asset_host
+ url_base << asset_host unless only_path
+ else
+ url_base << gitlab_config.base_url unless only_path
+ url_base << gitlab_config.relative_url_root
+ end
+
+ url_base + avatar.url
end
end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 9adc309a22b..d8394415362 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -98,6 +98,7 @@ module Awardable
def create_award_emoji(name, current_user)
return unless emoji_awardable?
+
award_emoji.create(name: normalize_name(name), user: current_user)
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 9417033d1f6..98776eab424 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -49,7 +49,8 @@ module CacheMarkdownField
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
- context = cached_markdown_fields[field].merge(project: project)
+ group = self.group if self.respond_to?(:group)
+ context = cached_markdown_fields[field].merge(project: project, group: group)
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb
new file mode 100644
index 00000000000..01957da0bf3
--- /dev/null
+++ b/app/models/concerns/group_descendant.rb
@@ -0,0 +1,56 @@
+module GroupDescendant
+ # Returns the hierarchy of a project or group in the from of a hash upto a
+ # given top.
+ #
+ # > project.hierarchy
+ # => { parent_group => { child_group => project } }
+ def hierarchy(hierarchy_top = nil, preloaded = nil)
+ preloaded ||= ancestors_upto(hierarchy_top)
+ expand_hierarchy_for_child(self, self, hierarchy_top, preloaded)
+ end
+
+ # Merges all hierarchies of the given groups or projects into an array of
+ # hashes. All ancestors need to be loaded into the given `descendants` to avoid
+ # queries down the line.
+ #
+ # > GroupDescendant.merge_hierarchy([project, child_group, child_group2, parent])
+ # => { parent => [{ child_group => project}, child_group2] }
+ def self.build_hierarchy(descendants, hierarchy_top = nil)
+ descendants = Array.wrap(descendants).uniq
+ return [] if descendants.empty?
+
+ unless descendants.all? { |hierarchy| hierarchy.is_a?(GroupDescendant) }
+ raise ArgumentError.new('element is not a hierarchy')
+ end
+
+ all_hierarchies = descendants.map do |descendant|
+ descendant.hierarchy(hierarchy_top, descendants)
+ end
+
+ Gitlab::Utils::MergeHash.merge(all_hierarchies)
+ end
+
+ private
+
+ def expand_hierarchy_for_child(child, hierarchy, hierarchy_top, preloaded)
+ parent = hierarchy_top if hierarchy_top && child.parent_id == hierarchy_top.id
+ parent ||= preloaded.detect { |possible_parent| possible_parent.is_a?(Group) && possible_parent.id == child.parent_id }
+
+ if parent.nil? && !child.parent_id.nil?
+ raise ArgumentError.new('parent was not preloaded')
+ end
+
+ if parent.nil? && hierarchy_top.present?
+ raise ArgumentError.new('specified top is not part of the tree')
+ end
+
+ if parent && parent != hierarchy_top
+ expand_hierarchy_for_child(parent,
+ { parent => hierarchy },
+ hierarchy_top,
+ preloaded)
+ else
+ hierarchy
+ end
+ end
+end
diff --git a/app/models/concerns/has_variable.rb b/app/models/concerns/has_variable.rb
index 9585b5583dc..8a241e4374a 100644
--- a/app/models/concerns/has_variable.rb
+++ b/app/models/concerns/has_variable.rb
@@ -16,6 +16,10 @@ module HasVariable
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
+ def key=(new_key)
+ super(new_key.to_s.strip)
+ end
+
def to_runner_variable
{ key: key, value: value, public: false }
end
diff --git a/app/models/concerns/ignorable_column.rb b/app/models/concerns/ignorable_column.rb
index eb9f3423e48..03793e8bcbb 100644
--- a/app/models/concerns/ignorable_column.rb
+++ b/app/models/concerns/ignorable_column.rb
@@ -21,8 +21,8 @@ module IgnorableColumn
@ignored_columns ||= Set.new
end
- def ignore_column(name)
- ignored_columns << name.to_s
+ def ignore_column(*names)
+ ignored_columns.merge(names.map(&:to_s))
end
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index fc30d008dea..5ca4a7086cb 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -14,10 +14,11 @@ module Issuable
include StripAttribute
include Awardable
include Taskable
- include TimeTrackable
include Importable
include Editable
include AfterCommitQueue
+ include Sortable
+ include CreatedAtFilterable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
@@ -95,8 +96,6 @@ module Issuable
strip_attributes :title
- acts_as_paranoid
-
after_save :record_metrics, unless: :imported?
# We want to use optimistic lock for cases when only title or description are involved
@@ -123,9 +122,7 @@ module Issuable
#
# Returns an ActiveRecord::Relation.
def search(query)
- title = to_fuzzy_arel(:title, query)
-
- where(title)
+ fuzzy_search(query, [:title])
end
# Searches for records with a matching title or description.
@@ -136,10 +133,7 @@ module Issuable
#
# Returns an ActiveRecord::Relation.
def full_search(query)
- title = to_fuzzy_arel(:title, query)
- description = to_fuzzy_arel(:description, query)
-
- where(title&.or(description))
+ fuzzy_search(query, [:title, :description])
end
def sort(method, excluded_labels: [])
@@ -256,23 +250,32 @@ module Issuable
participants(user).include?(user)
end
- def to_hook_data(user)
- hook_data = {
- object_kind: self.class.name.underscore,
- user: user.hook_attrs,
- project: project.hook_attrs,
- object_attributes: hook_attrs,
- labels: labels.map(&:hook_attrs),
- # DEPRECATED
- repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
- }
- if self.is_a?(Issue)
- hook_data[:assignees] = assignees.map(&:hook_attrs) if assignees.any?
- else
- hook_data[:assignee] = assignee.hook_attrs if assignee
+ def to_hook_data(user, old_associations: {})
+ changes = previous_changes
+ old_labels = old_associations.fetch(:labels, [])
+ old_assignees = old_associations.fetch(:assignees, [])
+
+ if old_labels != labels
+ changes[:labels] = [old_labels.map(&:hook_attrs), labels.map(&:hook_attrs)]
+ end
+
+ if old_assignees != assignees
+ if self.is_a?(Issue)
+ changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
+ else
+ changes[:assignee] = [old_assignees&.first&.hook_attrs, assignee&.hook_attrs]
+ end
+ end
+
+ if self.respond_to?(:total_time_spent)
+ old_total_time_spent = old_associations.fetch(:total_time_spent, nil)
+
+ if old_total_time_spent != total_time_spent
+ changes[:total_time_spent] = [old_total_time_spent, total_time_spent]
+ end
end
- hook_data
+ Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes)
end
def labels_array
@@ -343,4 +346,11 @@ module Issuable
def first_contribution?
false
end
+
+ ##
+ # Overriden in MergeRequest
+ #
+ def wipless_title_changed(old_title)
+ old_title != title
+ end
end
diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb
new file mode 100644
index 00000000000..dcb3b2b5ff3
--- /dev/null
+++ b/app/models/concerns/loaded_in_group_list.rb
@@ -0,0 +1,72 @@
+module LoadedInGroupList
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def with_counts(archived:)
+ selects_including_counts = [
+ 'namespaces.*',
+ "(#{project_count_sql(archived).to_sql}) AS preloaded_project_count",
+ "(#{member_count_sql.to_sql}) AS preloaded_member_count",
+ "(#{subgroup_count_sql.to_sql}) AS preloaded_subgroup_count"
+ ]
+
+ select(selects_including_counts)
+ end
+
+ def with_selects_for_list(archived: nil)
+ with_route.with_counts(archived: archived)
+ end
+
+ private
+
+ def project_count_sql(archived = nil)
+ projects = Project.arel_table
+ namespaces = Namespace.arel_table
+
+ base_count = projects.project(Arel.star.count.as('preloaded_project_count'))
+ .where(projects[:namespace_id].eq(namespaces[:id]))
+ if archived == 'only'
+ base_count.where(projects[:archived].eq(true))
+ elsif Gitlab::Utils.to_boolean(archived)
+ base_count
+ else
+ base_count.where(projects[:archived].not_eq(true))
+ end
+ end
+
+ def subgroup_count_sql
+ namespaces = Namespace.arel_table
+ children = namespaces.alias('children')
+
+ namespaces.project(Arel.star.count.as('preloaded_subgroup_count'))
+ .from(children)
+ .where(children[:parent_id].eq(namespaces[:id]))
+ end
+
+ def member_count_sql
+ members = Member.arel_table
+ namespaces = Namespace.arel_table
+
+ members.project(Arel.star.count.as('preloaded_member_count'))
+ .where(members[:source_type].eq(Namespace.name))
+ .where(members[:source_id].eq(namespaces[:id]))
+ .where(members[:requested_at].eq(nil))
+ end
+ end
+
+ def children_count
+ @children_count ||= project_count + subgroup_count
+ end
+
+ def project_count
+ @project_count ||= try(:preloaded_project_count) || projects.non_archived.count
+ end
+
+ def subgroup_count
+ @subgroup_count ||= try(:preloaded_subgroup_count) || children.count
+ end
+
+ def member_count
+ @member_count ||= try(:preloaded_member_count) || users.count
+ end
+end
diff --git a/app/models/concerns/manual_inverse_association.rb b/app/models/concerns/manual_inverse_association.rb
new file mode 100644
index 00000000000..0fca8feaf89
--- /dev/null
+++ b/app/models/concerns/manual_inverse_association.rb
@@ -0,0 +1,17 @@
+module ManualInverseAssociation
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def manual_inverse_association(association, inverse)
+ define_method(association) do |*args|
+ super(*args).tap do |value|
+ next unless value
+
+ child_association = value.association(inverse)
+ child_association.set_inverse_instance(self)
+ child_association.target = self
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 1db6b2d2fa2..b43eaeaeea0 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -31,11 +31,11 @@ module Mentionable
#
# By default this will be the class name and the result of calling
# `to_reference` on the object.
- def gfm_reference(from_project = nil)
+ def gfm_reference(from = nil)
# "MergeRequest" > "merge_request" > "Merge request" > "merge request"
friendly_name = self.class.to_s.underscore.humanize.downcase
- "#{friendly_name} #{to_reference(from_project)}"
+ "#{friendly_name} #{to_reference(from)}"
end
# The GFM reference to this Mentionable, which shouldn't be included in its #references.
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 710fc1ed647..7026f565706 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -86,6 +86,14 @@ module Milestoneish
false
end
+ def total_issue_time_spent
+ @total_issue_time_spent ||= issues.joins(:timelogs).sum(:time_spent)
+ end
+
+ def human_total_issue_time_spent
+ Gitlab::TimeTrackingFormatter.output(total_issue_time_spent)
+ end
+
private
def count_issues_by_state(user)
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index fde1cc44afa..e62f42e8e70 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -1,12 +1,6 @@
module ProtectedBranchAccess
extend ActiveSupport::Concern
- ALLOWED_ACCESS_LEVELS ||= [
- Gitlab::Access::MASTER,
- Gitlab::Access::DEVELOPER,
- Gitlab::Access::NO_ACCESS
- ].freeze
-
included do
include ProtectedRefAccess
@@ -14,18 +8,6 @@ module ProtectedBranchAccess
delegate :project, to: :protected_branch
- validates :access_level, presence: true, inclusion: {
- in: ALLOWED_ACCESS_LEVELS
- }
-
- def self.human_access_levels
- {
- Gitlab::Access::MASTER => "Masters",
- Gitlab::Access::DEVELOPER => "Developers + Masters",
- Gitlab::Access::NO_ACCESS => "No one"
- }.with_indifferent_access
- end
-
def check_access(user)
return false if access_level == Gitlab::Access::NO_ACCESS
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index c4f158e569a..80c9f7d4eb4 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -1,13 +1,35 @@
module ProtectedRefAccess
extend ActiveSupport::Concern
+ ALLOWED_ACCESS_LEVELS = [
+ Gitlab::Access::MASTER,
+ Gitlab::Access::DEVELOPER,
+ Gitlab::Access::NO_ACCESS
+ ].freeze
+
+ HUMAN_ACCESS_LEVELS = {
+ Gitlab::Access::MASTER => "Masters".freeze,
+ Gitlab::Access::DEVELOPER => "Developers + Masters".freeze,
+ Gitlab::Access::NO_ACCESS => "No one".freeze
+ }.freeze
+
included do
scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
+
+ validates :access_level, presence: true, if: :role?, inclusion: {
+ in: ALLOWED_ACCESS_LEVELS
+ }
end
def humanize
- self.class.human_access_levels[self.access_level]
+ HUMAN_ACCESS_LEVELS[self.access_level]
+ end
+
+ # CE access levels are always role-based,
+ # where as EE allows groups and users too
+ def role?
+ true
end
def check_access(user)
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index 78ac4f324e7..b782e85717e 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -7,7 +7,7 @@ module Referable
# Returns the String necessary to reference this object in Markdown
#
- # from_project - Refering Project object
+ # from - Referring parent object
#
# This should be overridden by the including class.
#
@@ -17,12 +17,12 @@ module Referable
# Issue.last.to_reference(other_project) # => "cross-project#1"
#
# Returns a String
- def to_reference(_from_project = nil, full:)
+ def to_reference(_from = nil, full:)
''
end
- def reference_link_text(from_project = nil)
- to_reference(from_project)
+ def reference_link_text(from = nil)
+ to_reference(from)
end
included do
diff --git a/app/models/concerns/repository_mirroring.rb b/app/models/concerns/repository_mirroring.rb
deleted file mode 100644
index f6aba91bc4c..00000000000
--- a/app/models/concerns/repository_mirroring.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-module RepositoryMirroring
- IMPORT_HEAD_REFS = '+refs/heads/*:refs/heads/*'.freeze
- IMPORT_TAG_REFS = '+refs/tags/*:refs/tags/*'.freeze
-
- def set_remote_as_mirror(name)
- # This is used to define repository as equivalent as "git clone --mirror"
- raw_repository.rugged.config["remote.#{name}.fetch"] = 'refs/*:refs/*'
- raw_repository.rugged.config["remote.#{name}.mirror"] = true
- raw_repository.rugged.config["remote.#{name}.prune"] = true
- end
-
- def set_import_remote_as_mirror(remote_name)
- # Add first fetch with Rugged so it does not create its own.
- raw_repository.rugged.config["remote.#{remote_name}.fetch"] = IMPORT_HEAD_REFS
-
- add_remote_fetch_config(remote_name, IMPORT_TAG_REFS)
-
- raw_repository.rugged.config["remote.#{remote_name}.mirror"] = true
- raw_repository.rugged.config["remote.#{remote_name}.prune"] = true
- end
-
- def add_remote_fetch_config(remote_name, refspec)
- run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}])
- end
-
- def fetch_mirror(remote, url)
- add_remote(remote, url)
- set_remote_as_mirror(remote)
- fetch_remote(remote, forced: true)
- remove_remote(remote)
- end
-end
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index 5ab5c80a2f5..b3020484738 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -7,6 +7,8 @@ module Storage
raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')
end
+ expires_full_path_cache
+
# Move the namespace directory in all storage paths used by member projects
repository_storage_paths.each do |repository_storage_path|
# Ensure old directory exists before moving it
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 274b38a7708..f478c8ede18 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -13,6 +13,8 @@ module Subscribable
end
def subscribed?(user, project = nil)
+ return false unless user
+
if subscription = subscriptions.find_by(user: user, project: project)
subscription.subscribed
else
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index b517ddaebd7..9f403d96ed5 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -9,7 +9,7 @@ module TimeTrackable
extend ActiveSupport::Concern
included do
- attr_reader :time_spent, :time_spent_user
+ attr_reader :time_spent, :time_spent_user, :spent_at
alias_method :time_spent?, :time_spent
@@ -24,6 +24,7 @@ module TimeTrackable
def spend_time(options)
@time_spent = options[:duration]
@time_spent_user = options[:user]
+ @spent_at = options[:spent_at]
@original_total_time_spent = nil
return if @time_spent == 0
@@ -55,7 +56,11 @@ module TimeTrackable
end
def add_or_subtract_spent_time
- timelogs.new(time_spent: time_spent, user: @time_spent_user)
+ timelogs.new(
+ time_spent: time_spent,
+ user: @time_spent_user,
+ spent_at: @spent_at
+ )
end
def check_negative_time_spent
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index d88a92dc027..ae5f138a920 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -18,7 +18,8 @@ class DiffNote < Note
validate :positions_complete
validate :verify_supported
- before_validation :set_original_position, :update_position, on: :create
+ before_validation :set_original_position, on: :create
+ before_validation :update_position, on: :create, if: :on_text?
before_validation :set_line_code
after_save :keep_around_commits
diff --git a/app/models/email.rb b/app/models/email.rb
index 384f38f2db7..d6516761f0a 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -1,5 +1,6 @@
class Email < ActiveRecord::Base
include Sortable
+ include Gitlab::SQL::Pattern
belongs_to :user
@@ -14,6 +15,8 @@ class Email < ActiveRecord::Base
devise :confirmable
self.reconfirmable = false # currently email can't be changed, no need to reconfirm
+ delegate :username, to: :user
+
def email=(value)
write_attribute(:email, value.downcase.strip)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index b6868ccbe8f..bf69b4c50f0 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -30,7 +30,6 @@ class Environment < ActiveRecord::Base
message: Gitlab::Regex.environment_slug_regex_message }
validates :external_url,
- uniqueness: { scope: :project_id },
length: { maximum: 255 },
allow_nil: true,
addressable_url: true
@@ -110,7 +109,7 @@ class Environment < ActiveRecord::Base
end
def ref_path
- "refs/#{Repository::REF_ENVIRONMENTS}/#{Shellwords.shellescape(name)}"
+ "refs/#{Repository::REF_ENVIRONMENTS}/#{slug}"
end
def formatted_external_url
@@ -139,11 +138,11 @@ class Environment < ActiveRecord::Base
end
def has_terminals?
- project.deployment_service.present? && available? && last_deployment.present?
+ project.deployment_platform.present? && available? && last_deployment.present?
end
def terminals
- project.deployment_service.terminals(self) if has_terminals?
+ project.deployment_platform.terminals(self) if has_terminals?
end
def has_metrics?
@@ -164,6 +163,10 @@ class Environment < ActiveRecord::Base
end
end
+ def slug
+ super.presence || generate_slug
+ end
+
# An environment name is not necessarily suitable for use in URLs, DNS
# or other third-party contexts, so provide a slugified version. A slug has
# the following properties:
diff --git a/app/models/epic.rb b/app/models/epic.rb
new file mode 100644
index 00000000000..62898a02e2d
--- /dev/null
+++ b/app/models/epic.rb
@@ -0,0 +1,7 @@
+# Placeholder class for model that is implemented in EE
+# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE
+class Epic < ActiveRecord::Base
+ # TODO: this will be implemented as part of #3853
+ def to_reference
+ end
+end
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 0bf18e529f0..2aaba2e4c90 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -38,13 +38,17 @@ class ExternalIssue
@project.id
end
- def to_reference(_from_project = nil, full: nil)
+ def to_reference(_from = nil, full: nil)
id
end
- def reference_link_text(from_project = nil)
+ def reference_link_text(from = nil)
return "##{id}" if id =~ /^\d+$/
id
end
+
+ def notes
+ Note.none
+ end
end
diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb
index 218e37a5312..7f1728e8c77 100644
--- a/app/models/fork_network.rb
+++ b/app/models/fork_network.rb
@@ -12,4 +12,8 @@ class ForkNetwork < ActiveRecord::Base
def find_forks_in(other_projects)
projects.where(id: other_projects)
end
+
+ def merge_requests
+ MergeRequest.where(target_project: projects)
+ end
end
diff --git a/app/models/fork_network_member.rb b/app/models/fork_network_member.rb
index 6a9b52a1ef8..eb9417dc34f 100644
--- a/app/models/fork_network_member.rb
+++ b/app/models/fork_network_member.rb
@@ -4,4 +4,14 @@ class ForkNetworkMember < ActiveRecord::Base
belongs_to :forked_from_project, class_name: 'Project'
validates :fork_network, :project, presence: true
+
+ after_destroy :cleanup_fork_network
+
+ private
+
+ def cleanup_fork_network
+ # Explicitly using `#count` makes sure we have the correct number if the
+ # relation was loaded in the fork_network.
+ fork_network.destroy if fork_network.fork_network_members.count == 0
+ end
end
diff --git a/app/models/gcp/cluster.rb b/app/models/gcp/cluster.rb
deleted file mode 100644
index 18bd6a6dcb4..00000000000
--- a/app/models/gcp/cluster.rb
+++ /dev/null
@@ -1,113 +0,0 @@
-module Gcp
- class Cluster < ActiveRecord::Base
- extend Gitlab::Gcp::Model
- include Presentable
-
- belongs_to :project, inverse_of: :cluster
- belongs_to :user
- belongs_to :service
-
- default_value_for :gcp_cluster_zone, 'us-central1-a'
- default_value_for :gcp_cluster_size, 3
- default_value_for :gcp_machine_type, 'n1-standard-4'
-
- attr_encrypted :password,
- mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
- algorithm: 'aes-256-cbc'
-
- attr_encrypted :kubernetes_token,
- mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
- algorithm: 'aes-256-cbc'
-
- attr_encrypted :gcp_token,
- mode: :per_attribute_iv,
- key: Gitlab::Application.secrets.db_key_base,
- algorithm: 'aes-256-cbc'
-
- validates :gcp_project_id,
- length: 1..63,
- format: {
- with: Gitlab::Regex.kubernetes_namespace_regex,
- message: Gitlab::Regex.kubernetes_namespace_regex_message
- }
-
- validates :gcp_cluster_name,
- length: 1..63,
- format: {
- with: Gitlab::Regex.kubernetes_namespace_regex,
- message: Gitlab::Regex.kubernetes_namespace_regex_message
- }
-
- validates :gcp_cluster_zone, presence: true
-
- validates :gcp_cluster_size,
- presence: true,
- numericality: {
- only_integer: true,
- greater_than: 0
- }
-
- validates :project_namespace,
- allow_blank: true,
- length: 1..63,
- format: {
- with: Gitlab::Regex.kubernetes_namespace_regex,
- message: Gitlab::Regex.kubernetes_namespace_regex_message
- }
-
- # if we do not do status transition we prevent change
- validate :restrict_modification, on: :update, unless: :status_changed?
-
- state_machine :status, initial: :scheduled do
- state :scheduled, value: 1
- state :creating, value: 2
- state :created, value: 3
- state :errored, value: 4
-
- event :make_creating do
- transition any - [:creating] => :creating
- end
-
- event :make_created do
- transition any - [:created] => :created
- end
-
- event :make_errored do
- transition any - [:errored] => :errored
- end
-
- before_transition any => [:errored, :created] do |cluster|
- cluster.gcp_token = nil
- cluster.gcp_operation_id = nil
- end
-
- before_transition any => [:errored] do |cluster, transition|
- status_reason = transition.args.first
- cluster.status_reason = status_reason if status_reason
- end
- end
-
- def project_namespace_placeholder
- "#{project.path}-#{project.id}"
- end
-
- def on_creation?
- scheduled? || creating?
- end
-
- def api_url
- 'https://' + endpoint if endpoint
- end
-
- def restrict_modification
- if on_creation?
- errors.add(:base, "cannot modify during creation")
- return false
- end
-
- true
- end
- end
-end
diff --git a/app/models/group.rb b/app/models/group.rb
index e746e4a12c9..76262acf50c 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -6,6 +6,8 @@ class Group < Namespace
include Avatarable
include Referable
include SelectForProjectAuthorization
+ include LoadedInGroupList
+ include GroupDescendant
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
@@ -24,6 +26,7 @@ class Group < Namespace
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable'
+ has_many :custom_attributes, class_name: 'GroupCustomAttribute'
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects
@@ -40,26 +43,13 @@ class Group < Namespace
after_create :post_create_hook
after_destroy :post_destroy_hook
after_save :update_two_factor_requirement
+ after_update :path_changed_hook, if: :path_changed?
class << self
def supports_nested_groups?
Gitlab::Database.postgresql?
end
- # Searches for groups matching the given query.
- #
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
- #
- # query - The search query as a String
- #
- # Returns an ActiveRecord::Relation.
- def search(query)
- table = Namespace.arel_table
- pattern = "%#{query}%"
-
- where(table[:name].matches(pattern).or(table[:path].matches(pattern)))
- end
-
def sort(method)
if method == 'storage_size_desc'
# storage_size is a virtual column so we need to
@@ -93,7 +83,7 @@ class Group < Namespace
end
end
- def to_reference(_from_project = nil, full: nil)
+ def to_reference(_from = nil, full: nil)
"#{self.class.reference_prefix}#{full_path}"
end
@@ -178,6 +168,12 @@ class Group < Namespace
add_user(user, :owner, current_user: current_user)
end
+ def member?(user, min_access_level = Gitlab::Access::GUEST)
+ return false unless user
+
+ max_member_access_for_user(user) >= min_access_level
+ end
+
def has_owner?(user)
return false unless user
@@ -287,6 +283,12 @@ class Group < Namespace
list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten
end
+ def full_path_was
+ return path_was unless has_parent?
+
+ "#{parent.full_path}/#{path_was}"
+ end
+
private
def update_two_factor_requirement
@@ -295,6 +297,10 @@ class Group < Namespace
users.find_each(&:update_two_factor_requirement)
end
+ def path_changed_hook
+ system_hook_service.execute_hooks_for(self, :rename)
+ end
+
def visibility_level_allowed_by_parent
return if visibility_level_allowed_by_parent?
diff --git a/app/models/group_custom_attribute.rb b/app/models/group_custom_attribute.rb
new file mode 100644
index 00000000000..8157d602d67
--- /dev/null
+++ b/app/models/group_custom_attribute.rb
@@ -0,0 +1,6 @@
+class GroupCustomAttribute < ActiveRecord::Base
+ belongs_to :group
+
+ validates :group, :key, :value, presence: true
+ validates :key, uniqueness: { scope: [:group_id] }
+end
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 920a25932b4..ff811e19f8a 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -1,15 +1,27 @@
class Identity < ActiveRecord::Base
include Sortable
include CaseSensitivity
+
belongs_to :user
validates :provider, presence: true
- validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
+ validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false }
validates :user_id, uniqueness: { scope: :provider }
- scope :with_extern_uid, ->(provider, extern_uid) { where(extern_uid: extern_uid, provider: provider) }
+ scope :with_provider, ->(provider) { where(provider: provider) }
+ scope :with_extern_uid, ->(provider, extern_uid) do
+ iwhere(extern_uid: normalize_uid(provider, extern_uid)).with_provider(provider)
+ end
def ldap?
provider.starts_with?('ldap')
end
+
+ def self.normalize_uid(provider, uid)
+ if provider.to_s.starts_with?('ldap')
+ Gitlab::LDAP::Person.normalize_dn(uid)
+ else
+ uid.to_s
+ end
+ end
end
diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb
new file mode 100644
index 00000000000..b30b707e5fe
--- /dev/null
+++ b/app/models/instance_configuration.rb
@@ -0,0 +1,71 @@
+require 'resolv'
+
+class InstanceConfiguration
+ SSH_ALGORITHMS = %w(DSA ECDSA ED25519 RSA).freeze
+ SSH_ALGORITHMS_PATH = '/etc/ssh/'.freeze
+ CACHE_KEY = 'instance_configuration'.freeze
+ EXPIRATION_TIME = 24.hours
+
+ def settings
+ @configuration ||= Rails.cache.fetch(CACHE_KEY, expires_in: EXPIRATION_TIME) do
+ { ssh_algorithms_hashes: ssh_algorithms_hashes,
+ host: host,
+ gitlab_pages: gitlab_pages,
+ gitlab_ci: gitlab_ci }.deep_symbolize_keys
+ end
+ end
+
+ private
+
+ def ssh_algorithms_hashes
+ SSH_ALGORITHMS.map { |algo| ssh_algorithm_hashes(algo) }.compact
+ end
+
+ def host
+ Settings.gitlab.host
+ end
+
+ def gitlab_pages
+ Settings.pages.to_h.merge(ip_address: resolv_dns(Settings.pages.host))
+ end
+
+ def resolv_dns(dns)
+ Resolv.getaddress(dns)
+ rescue Resolv::ResolvError
+ end
+
+ def gitlab_ci
+ Settings.gitlab_ci
+ .to_h
+ .merge(artifacts_max_size: { value: Settings.artifacts.max_size&.megabytes,
+ default: 100.megabytes })
+ end
+
+ def ssh_algorithm_file(algorithm)
+ File.join(SSH_ALGORITHMS_PATH, "ssh_host_#{algorithm.downcase}_key.pub")
+ end
+
+ def ssh_algorithm_hashes(algorithm)
+ content = ssh_algorithm_file_content(algorithm)
+ return unless content.present?
+
+ { name: algorithm,
+ md5: ssh_algorithm_md5(content),
+ sha256: ssh_algorithm_sha256(content) }
+ end
+
+ def ssh_algorithm_file_content(algorithm)
+ file = ssh_algorithm_file(algorithm)
+ return unless File.exist?(file)
+
+ File.read(file)
+ end
+
+ def ssh_algorithm_md5(ssh_file_content)
+ OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':')
+ end
+
+ def ssh_algorithm_sha256(ssh_file_content)
+ OpenSSL::Digest::SHA256.hexdigest(ssh_file_content)
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 155c5d972b7..d6ef58d150b 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -5,11 +5,10 @@ class Issue < ActiveRecord::Base
include Issuable
include Noteable
include Referable
- include Sortable
include Spammable
include FasterCacheKeys
include RelativePositioning
- include CreatedAtFilterable
+ include TimeTrackable
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
@@ -50,7 +49,6 @@ class Issue < ActiveRecord::Base
scope :public_only, -> { where(confidential: false) }
after_save :expire_etag_cache
- after_commit :update_project_counter_caches, on: :destroy
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
@@ -74,19 +72,7 @@ class Issue < ActiveRecord::Base
end
end
- def hook_attrs
- assignee_ids = self.assignee_ids
-
- attrs = {
- total_time_spent: total_time_spent,
- human_total_time_spent: human_total_time_spent,
- human_time_estimate: human_time_estimate,
- assignee_ids: assignee_ids,
- assignee_id: assignee_ids.first # This key is deprecated
- }
-
- attributes.merge!(attrs)
- end
+ acts_as_paranoid
def self.reference_prefix
'#'
@@ -131,6 +117,10 @@ class Issue < ActiveRecord::Base
"id DESC")
end
+ def hook_attrs
+ Gitlab::HookData::IssueBuilder.new(self).build
+ end
+
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
@@ -255,7 +245,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
- json[:subscribed] = subscribed?(options[:user], project) if options.key?(:user) && options[:user]
+ if options.key?(:sidebar_endpoints) && project
+ url_helper = Gitlab::Routing.url_helpers
+
+ json.merge!(issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
+ toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self))
+ end
if options.key?(:labels)
json[:labels] = labels.as_json(
@@ -271,10 +266,6 @@ class Issue < ActiveRecord::Base
true
end
- def update_project_counter_caches?
- state_changed? || confidential_changed?
- end
-
def update_project_counter_caches
Projects::OpenIssuesCountService.new(project).refresh_cache
end
diff --git a/app/models/key.rb b/app/models/key.rb
index f119b15c737..815fd1de909 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -27,8 +27,10 @@ class Key < ActiveRecord::Base
after_commit :add_to_shell, on: :create
after_create :post_create_hook
+ after_create :refresh_user_cache
after_commit :remove_from_shell, on: :destroy
after_destroy :post_destroy_hook
+ after_destroy :refresh_user_cache
def key=(value)
value&.delete!("\n\r")
@@ -76,6 +78,12 @@ class Key < ActiveRecord::Base
)
end
+ def refresh_user_cache
+ return unless user
+
+ Users::KeysCountService.new(user).refresh_cache
+ end
+
def post_destroy_hook
SystemHooksService.new.execute_hooks_for(self, :destroy)
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 899028a01a0..b5bfa6ea2dd 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -165,12 +165,12 @@ class Label < ActiveRecord::Base
#
# Returns a String
#
- def to_reference(from_project = nil, target_project: nil, format: :id, full: false)
+ def to_reference(from = nil, target_project: nil, format: :id, full: false)
format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
- if from_project
- "#{from_project.to_reference(target_project, full: full)}#{reference}"
+ if from
+ "#{from.to_reference(target_project, full: full)}#{reference}"
else
reference
end
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index b7cf96abe83..fc586fa216e 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -6,16 +6,8 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
- def storage_project(project)
- if project && project.forked?
- storage_project(project.forked_from_project)
- else
- project
- end
- end
-
def project_allowed_access?(project)
- projects.exists?(storage_project(project).id)
+ projects.exists?(project.lfs_storage_project.id)
end
def self.destroy_unreferenced
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 292122f779e..bbc01e9677c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -3,20 +3,41 @@ class MergeRequest < ActiveRecord::Base
include Issuable
include Noteable
include Referable
- include Sortable
include IgnorableColumn
- include CreatedAtFilterable
+ include TimeTrackable
+ include ManualInverseAssociation
+ include EachBatch
- ignore_column :locked_at
+ ignore_column :locked_at,
+ :ref_fetched
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User"
has_many :merge_request_diffs
+
has_one :merge_request_diff,
-> { order('merge_request_diffs.id DESC') }, inverse_of: :merge_request
+ belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff'
+ manual_inverse_association :latest_merge_request_diff, :merge_request
+
+ # This is the same as latest_merge_request_diff unless:
+ # 1. There are arguments - in which case we might be trying to force-reload.
+ # 2. This association is already loaded.
+ # 3. The latest diff does not exist.
+ #
+ # The second one in particular is important - MergeRequestDiff#merge_request
+ # is the inverse of MergeRequest#merge_request_diff, which means it may not be
+ # the latest diff, because we could have loaded any diff from this particular
+ # MR. If we haven't already loaded a diff, then it's fine to load the latest.
+ def merge_request_diff(*args)
+ fallback = latest_merge_request_diff if args.empty? && !association(:merge_request_diff).loaded?
+
+ fallback || super
+ end
+
belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -31,7 +52,6 @@ class MergeRequest < ActiveRecord::Base
after_create :ensure_merge_request_diff, unless: :importing?
after_update :reload_diff_if_branch_changed
- after_commit :update_project_counter_caches, on: :destroy
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
@@ -119,6 +139,8 @@ class MergeRequest < ActiveRecord::Base
after_save :keep_around_commit
+ acts_as_paranoid
+
def self.reference_prefix
'!'
end
@@ -165,6 +187,22 @@ class MergeRequest < ActiveRecord::Base
where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
+ # This is used after project import, to reset the IDs to the correct
+ # values. It is not intended to be called without having already scoped the
+ # relation.
+ def self.set_latest_merge_request_diff_ids!
+ update = '
+ latest_merge_request_diff_id = (
+ SELECT MAX(id)
+ FROM merge_request_diffs
+ WHERE merge_requests.id = merge_request_diffs.merge_request_id
+ )'.squish
+
+ self.each_batch do |batch|
+ batch.update_all(update)
+ end
+ end
+
WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
def self.work_in_progress?(title)
@@ -179,6 +217,16 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}"
end
+ # Verifies if title has changed not taking into account WIP prefix
+ # for merge requests.
+ def wipless_title_changed(old_title)
+ self.class.wipless_title(old_title) != self.wipless_title
+ end
+
+ def hook_attrs
+ Gitlab::HookData::MergeRequestBuilder.new(self).build
+ end
+
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
@@ -235,9 +283,9 @@ class MergeRequest < ActiveRecord::Base
if persisted?
merge_request_diff.commit_shas
elsif compare_commits
- compare_commits.reverse.map(&:sha)
+ compare_commits.to_a.reverse.map(&:sha)
else
- []
+ Array(diff_head_sha)
end
end
@@ -316,16 +364,28 @@ class MergeRequest < ActiveRecord::Base
# We use these attributes to force these to the intended values.
attr_writer :target_branch_sha, :source_branch_sha
+ def source_branch_ref
+ return @source_branch_sha if @source_branch_sha
+ return unless source_branch
+
+ Gitlab::Git::BRANCH_REF_PREFIX + source_branch
+ end
+
+ def target_branch_ref
+ return @target_branch_sha if @target_branch_sha
+ return unless target_branch
+
+ Gitlab::Git::BRANCH_REF_PREFIX + target_branch
+ end
+
def source_branch_head
return unless source_project
- source_branch_ref = @source_branch_sha || source_branch
source_project.repository.commit(source_branch_ref) if source_branch_ref
end
def target_branch_head
- target_branch_ref = @target_branch_sha || target_branch
- target_project.repository.commit(target_branch_ref) if target_branch_ref
+ target_project.repository.commit(target_branch_ref)
end
def branch_merge_base_commit
@@ -392,7 +452,11 @@ class MergeRequest < ActiveRecord::Base
end
def merge_ongoing?
- !!merge_jid && !merged?
+ # While the MergeRequest is locked, it should present itself as 'merge ongoing'.
+ # The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron.
+ return true if locked?
+
+ !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid)
end
def closed_without_fork?
@@ -415,7 +479,7 @@ class MergeRequest < ActiveRecord::Base
end
def create_merge_request_diff
- fetch_ref
+ fetch_ref!
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435
Gitlab::GitalyClient.allow_n_plus_1_calls do
@@ -430,7 +494,7 @@ class MergeRequest < ActiveRecord::Base
def merge_request_diff_for(diff_refs_or_sha)
@merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha|
- diffs = merge_request_diffs.viewable.select_without_diff
+ diffs = merge_request_diffs.viewable
h[diff_refs_or_sha] =
if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
diffs.find_by_diff_refs(diff_refs_or_sha)
@@ -566,7 +630,7 @@ class MergeRequest < ActiveRecord::Base
commit_notes = Note
.except(:order)
.where(project_id: [source_project_id, target_project_id])
- .where(noteable_type: 'Commit', commit_id: commit_ids)
+ .for_commit_id(commit_ids)
# We're using a UNION ALL here since this results in better performance
# compared to using OR statements. We're using UNION ALL since the queries
@@ -587,24 +651,6 @@ class MergeRequest < ActiveRecord::Base
!discussions_to_be_resolved?
end
- def hook_attrs
- attrs = {
- source: source_project.try(:hook_attrs),
- target: target_project.hook_attrs,
- last_commit: nil,
- work_in_progress: work_in_progress?,
- total_time_spent: total_time_spent,
- human_total_time_spent: human_total_time_spent,
- human_time_estimate: human_time_estimate
- }
-
- if diff_head_commit
- attrs[:last_commit] = diff_head_commit.hook_attrs
- end
-
- attributes.merge!(attrs)
- end
-
def for_fork?
target_project != source_project
end
@@ -689,13 +735,13 @@ class MergeRequest < ActiveRecord::Base
def source_branch_exists?
return false unless self.source_project
- self.source_project.repository.branch_names.include?(self.source_branch)
+ self.source_project.repository.branch_exists?(self.source_branch)
end
def target_branch_exists?
return false unless self.target_project
- self.target_project.repository.branch_names.include?(self.target_branch)
+ self.target_project.repository.branch_exists?(self.target_branch)
end
def merge_commit_message(include_description: false)
@@ -818,29 +864,14 @@ class MergeRequest < ActiveRecord::Base
end
end
- def fetch_ref
- write_ref
- update_column(:ref_fetched, true)
+ def fetch_ref!
+ target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
end
def ref_path
"refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
end
- def ref_fetched?
- super ||
- begin
- computed_value = project.repository.ref_exists?(ref_path)
- update_column(:ref_fetched, true) if computed_value
-
- computed_value
- end
- end
-
- def ensure_ref_fetched
- fetch_ref unless ref_fetched?
- end
-
def in_locked_state
begin
lock_mr
@@ -868,7 +899,8 @@ class MergeRequest < ActiveRecord::Base
def compute_diverged_commits_count
return 0 unless source_branch_sha && target_branch_sha
- Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
+ target_project.repository
+ .count_commits_between(source_branch_sha, target_branch_sha)
end
private :compute_diverged_commits_count
@@ -887,16 +919,18 @@ class MergeRequest < ActiveRecord::Base
# Note that this could also return SHA from now dangling commits
#
def all_commit_shas
- if persisted?
- column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).pluck('DISTINCT(sha)')
- serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)
+ return commit_shas unless persisted?
- (column_shas + serialised_shas).uniq
- elsif compare_commits
- compare_commits.to_a.reverse.map(&:id)
- else
- [diff_head_sha]
- end
+ diffs_relation = merge_request_diffs
+
+ # MySQL doesn't support LIMIT in a subquery.
+ diffs_relation = diffs_relation.recent if Gitlab::Database.postgresql?
+
+ MergeRequestDiffCommit
+ .where(merge_request_diff: diffs_relation)
+ .limit(10_000)
+ .pluck('sha')
+ .uniq
end
def merge_commit
@@ -969,10 +1003,6 @@ class MergeRequest < ActiveRecord::Base
true
end
- def update_project_counter_caches?
- state_changed?
- end
-
def update_project_counter_caches
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
@@ -982,10 +1012,4 @@ class MergeRequest < ActiveRecord::Base
project.merge_requests.merged.where(author_id: author_id).empty?
end
-
- private
-
- def write_ref
- target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path)
- end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index faf0b95f842..c37aa0a594b 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -1,21 +1,21 @@
class MergeRequestDiff < ActiveRecord::Base
include Sortable
include Importable
- include Gitlab::EncodingHelper
+ include ManualInverseAssociation
+ include IgnorableColumn
- # Prevent store of diff if commits amount more then 500
+ # Don't display more than 100 commits at once
COMMITS_SAFE_SIZE = 100
- # Valid types of serialized diffs allowed by Gitlab::Git::Diff
- VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze
+ ignore_column :st_commits,
+ :st_diffs
belongs_to :merge_request
+ manual_inverse_association :merge_request, :merge_request_diff
+
has_many :merge_request_diff_files, -> { order(:merge_request_diff_id, :relative_order) }
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
- serialize :st_commits # rubocop:disable Cop/ActiveRecordSerialize
- serialize :st_diffs # rubocop:disable Cop/ActiveRecordSerialize
-
state_machine :state, initial: :empty do
state :collected
state :overflow
@@ -29,6 +29,8 @@ class MergeRequestDiff < ActiveRecord::Base
scope :viewable, -> { without_state(:empty) }
+ scope :recent, -> { order(id: :desc).limit(100) }
+
# All diff information is collected from repository after object is created.
# It allows you to override variables like head_commit_sha before getting diff.
after_create :save_git_content, unless: :importing?
@@ -37,17 +39,13 @@ class MergeRequestDiff < ActiveRecord::Base
find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
end
- def self.select_without_diff
- select(column_names - ['st_diffs'])
- end
-
- def st_commits
- super || []
- end
-
# Collect information about commits and diff from repository
# and save it to the database as serialized data
def save_git_content
+ MergeRequest
+ .where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id)
+ .update_all(latest_merge_request_diff_id: self.id)
+
ensure_commit_shas
save_commits
save_diffs
@@ -122,11 +120,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def commit_shas
- if st_commits.present?
- st_commits.map { |commit| commit[:id] }
- else
- merge_request_diff_commits.map(&:sha)
- end
+ merge_request_diff_commits.map(&:sha)
end
def diff_refs=(new_diff_refs)
@@ -190,7 +184,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def latest?
- self == merge_request.merge_request_diff
+ self.id == merge_request.latest_merge_request_diff_id
end
def compare_with(sha)
@@ -201,34 +195,11 @@ class MergeRequestDiff < ActiveRecord::Base
end
def commits_count
- if st_commits.present?
- st_commits.size
- else
- merge_request_diff_commits.size
- end
- end
-
- def utf8_st_diffs
- return [] if st_diffs.blank?
-
- st_diffs.map do |diff|
- diff.each do |k, v|
- diff[k] = encode_utf8(v) if v.respond_to?(:encoding)
- end
- end
+ merge_request_diff_commits.size
end
private
- # Old GitLab implementations may have generated diffs as ["--broken-diff"].
- # Avoid an error 500 by ignoring bad elements. See:
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/20776
- def valid_raw_diff?(raw)
- return false unless raw.respond_to?(:each)
-
- raw.any? { |element| VALID_CLASSES.include?(element.class) }
- end
-
def create_merge_request_diff_files(diffs)
rows = diffs.map.with_index do |diff, index|
diff_hash = diff.to_hash.merge(
@@ -252,9 +223,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def load_diffs(options)
- return Gitlab::Git::DiffCollection.new([]) unless diffs_from_database
-
- raw = diffs_from_database
+ raw = merge_request_diff_files.map(&:to_hash)
if paths = options[:paths]
raw = raw.select do |diff|
@@ -265,23 +234,11 @@ class MergeRequestDiff < ActiveRecord::Base
Gitlab::Git::DiffCollection.new(raw, options)
end
- def diffs_from_database
- return @diffs_from_database if defined?(@diffs_from_database)
-
- @diffs_from_database =
- if st_diffs.present?
- if valid_raw_diff?(st_diffs)
- st_diffs
- end
- elsif merge_request_diff_files.present?
- merge_request_diff_files.map(&:to_hash)
- end
- end
-
def load_commits
- commits = st_commits.presence || merge_request_diff_commits
+ commits = merge_request_diff_commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
- commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
+ CommitCollection
+ .new(merge_request.source_project, commits, merge_request.source_branch)
end
def save_diffs
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index 670b26d4ca3..b75387e236e 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -17,7 +17,9 @@ class MergeRequestDiffCommit < ActiveRecord::Base
commit_hash.merge(
merge_request_diff_id: merge_request_diff_id,
relative_order: index,
- sha: sha_attribute.type_cast_for_database(sha)
+ sha: sha_attribute.type_cast_for_database(sha),
+ authored_date: Gitlab::Database.sanitize_timestamp(commit_hash[:authored_date]),
+ committed_date: Gitlab::Database.sanitize_timestamp(commit_hash[:committed_date])
)
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 47e6b785c39..c06ee8083f0 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -13,6 +13,7 @@ class Milestone < ActiveRecord::Base
include Referable
include StripAttribute
include Milestoneish
+ include Gitlab::SQL::Pattern
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
@@ -73,10 +74,7 @@ class Milestone < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def search(query)
- t = arel_table
- pattern = "%#{query}%"
-
- where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
+ fuzzy_search(query, [:title, :description])
end
def filter_by_state(milestones, state)
@@ -162,18 +160,18 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1"
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
- def to_reference(from_project = nil, format: :name, full: false)
+ def to_reference(from = nil, format: :name, full: false)
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
if project
- "#{project.to_reference(from_project, full: full)}#{reference}"
+ "#{project.to_reference(from, full: full)}#{reference}"
else
reference
end
end
- def reference_link_text(from_project = nil)
+ def reference_link_text(from = nil)
self.title
end
@@ -256,7 +254,7 @@ class Milestone < ActiveRecord::Base
def start_date_should_be_less_than_due_date
if due_date <= start_date
- errors.add(:start_date, "Can't be greater than due date")
+ errors.add(:due_date, "must be greater than start date")
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 4672881e220..fa76729a702 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -9,6 +9,7 @@ class Namespace < ActiveRecord::Base
include Routable
include AfterCommitQueue
include Storage::LegacyNamespace
+ include Gitlab::SQL::Pattern
# Prevent users from creating unreasonably deep level of nesting.
# The number 20 was taken based on maximum nesting level of
@@ -36,7 +37,7 @@ class Namespace < ActiveRecord::Base
validates :path,
presence: true,
length: { maximum: 255 },
- dynamic_path: true
+ namespace_path: true
validate :nesting_level_allowed
@@ -86,10 +87,7 @@ class Namespace < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation
def search(query)
- t = arel_table
- pattern = "%#{query}%"
-
- where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
+ fuzzy_search(query, [:name, :path])
end
def clean_path(path)
@@ -162,6 +160,13 @@ class Namespace < ActiveRecord::Base
.base_and_ancestors
end
+ # returns all ancestors upto but excluding the the given namespace
+ # when no namespace is given, all ancestors upto the top are returned
+ def ancestors_upto(top = nil)
+ Gitlab::GroupHierarchy.new(self.class.where(id: id))
+ .ancestors(upto: top)
+ end
+
def self_and_ancestors
return self.class.where(id: id) unless parent_id
diff --git a/app/models/note.rb b/app/models/note.rb
index ceded9f2aef..340fe087f82 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -14,6 +14,7 @@ class Note < ActiveRecord::Base
include ResolvableNote
include IgnorableColumn
include Editable
+ include Gitlab::SQL::Pattern
module SpecialRole
FIRST_TIME_CONTRIBUTOR = :first_time_contributor
@@ -69,7 +70,7 @@ class Note < ActiveRecord::Base
delegate :title, to: :noteable, allow_nil: true
validates :note, presence: true
- validates :project, presence: true, unless: :for_personal_snippet?
+ validates :project, presence: true, if: :for_project_noteable?
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
@@ -110,11 +111,12 @@ class Note < ActiveRecord::Base
includes(:author, :noteable, :updated_by,
project: [:project_members, { group: [:group_members] }])
end
+ scope :with_metadata, -> { includes(:system_note_metadata) }
after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id, on: :create
- after_save :keep_around_commit, unless: :for_personal_snippet?
+ after_save :keep_around_commit, if: :for_project_noteable?
after_save :expire_etag_cache
after_destroy :expire_etag_cache
@@ -166,10 +168,20 @@ class Note < ActiveRecord::Base
def has_special_role?(role, note)
note.special_role == role
end
+
+ def search(query)
+ fuzzy_search(query, [:note])
+ end
end
def cross_reference?
- system? && SystemNoteService.cross_reference?(note)
+ return unless system?
+
+ if force_cross_reference_regex_check?
+ matches_cross_reference_regex?
+ else
+ SystemNoteService.cross_reference?(note)
+ end
end
def diff_note?
@@ -208,6 +220,10 @@ class Note < ActiveRecord::Base
noteable.is_a?(PersonalSnippet)
end
+ def for_project_noteable?
+ !for_personal_snippet?
+ end
+
def skip_project_check?
for_personal_snippet?
end
@@ -378,4 +394,10 @@ class Note < ActiveRecord::Base
def set_discussion_id
self.discussion_id ||= discussion_class.discussion_id(self)
end
+
+ def force_cross_reference_regex_check?
+ return unless system?
+
+ SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.include?(system_note_metadata&.action)
+ end
end
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
index b85f5dbaf2e..e8595b13d6d 100644
--- a/app/models/oauth_access_token.rb
+++ b/app/models/oauth_access_token.rb
@@ -1,4 +1,14 @@
class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application'
+
+ alias_attribute :user, :resource_owner
+
+ def scopes=(value)
+ if value.is_a?(Array)
+ super(Doorkeeper::OAuth::Scopes.from_array(value).to_s)
+ else
+ super
+ end
+ end
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 5d798247863..8de42ff9d2e 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -16,9 +16,9 @@ class PagesDomain < ActiveRecord::Base
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
- after_create :update
- after_save :update
- after_destroy :update
+ after_create :update_daemon
+ after_save :update_daemon
+ after_destroy :update_daemon
def to_param
domain
@@ -65,12 +65,18 @@ class PagesDomain < ActiveRecord::Base
def expired?
return false unless x509
+
current = Time.new
current < x509.not_before || x509.not_after < current
end
+ def expiration
+ x509&.not_after
+ end
+
def subject
return unless x509
+
x509.subject.to_s
end
@@ -80,7 +86,7 @@ class PagesDomain < ActiveRecord::Base
private
- def update
+ def update_daemon
::Projects::UpdatePagesConfigurationService.new(project).execute
end
@@ -98,6 +104,7 @@ class PagesDomain < ActiveRecord::Base
def validate_pages_domain
return unless domain
+
if domain.downcase.ends_with?(Settings.pages.host.downcase)
self.errors.add(:domain, "*.#{Settings.pages.host} is restricted")
end
@@ -105,6 +112,7 @@ class PagesDomain < ActiveRecord::Base
def x509
return unless certificate
+
@x509 ||= OpenSSL::X509::Certificate.new(certificate)
rescue OpenSSL::X509::CertificateError
nil
@@ -112,6 +120,7 @@ class PagesDomain < ActiveRecord::Base
def pkey
return unless key
+
@pkey ||= OpenSSL::PKey::RSA.new(key)
rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError
nil
diff --git a/app/models/project.rb b/app/models/project.rb
index 57e91ab3b88..c6f7f56f311 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -17,6 +17,8 @@ class Project < ActiveRecord::Base
include ProjectFeaturesCompatibility
include SelectForProjectAuthorization
include Routable
+ include GroupDescendant
+ include Gitlab::SQL::Pattern
extend Gitlab::ConfigHelper
extend Gitlab::CurrentSettings
@@ -25,7 +27,15 @@ class Project < ActiveRecord::Base
NUMBER_OF_PERMITTED_BOARDS = 1
UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
- LATEST_STORAGE_VERSION = 1
+ # Hashed Storage versions handle rolling out new storage to project and dependents models:
+ # nil: legacy
+ # 1: repository
+ # 2: attachments
+ LATEST_STORAGE_VERSION = 2
+ HASHED_STORAGE_FEATURES = {
+ repository: 1,
+ attachments: 2
+ }.freeze
cache_markdown_field :description, pipeline: :description
@@ -81,6 +91,8 @@ class Project < ActiveRecord::Base
belongs_to :creator, class_name: 'User'
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
belongs_to :namespace
+ alias_method :parent, :namespace
+ alias_attribute :parent_id, :namespace_id
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards, before_add: :validate_board_limit
@@ -117,6 +129,7 @@ class Project < ActiveRecord::Base
has_one :mock_deployment_service
has_one :mock_monitoring_service
has_one :microsoft_teams_service
+ has_one :packagist_service
# TODO: replace these relations with the fork network versions
has_one :forked_project_link, foreign_key: "forked_to_project_id"
@@ -174,7 +187,10 @@ class Project < ActiveRecord::Base
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics'
- has_one :cluster, class_name: 'Gcp::Cluster', inverse_of: :project
+
+ has_one :cluster_project, class_name: 'Clusters::Project'
+ has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster'
+ has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
@@ -201,6 +217,7 @@ class Project < ActiveRecord::Base
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_one :auto_devops, class_name: 'ProjectAutoDevops'
+ has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
@@ -228,10 +245,8 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
- dynamic_path: true,
+ project_path: true,
length: { maximum: 255 },
- format: { with: Gitlab::PathRegex.project_path_format_regex,
- message: Gitlab::PathRegex.project_path_format_message },
uniqueness: { scope: :namespace_id }
validates :namespace, presence: true
@@ -258,8 +273,9 @@ class Project < ActiveRecord::Base
scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) }
- scope :with_hashed_storage, -> { where('storage_version >= 1') }
- scope :with_legacy_storage, -> { where(storage_version: [nil, 0]) }
+ scope :with_storage_feature, ->(feature) { where('storage_version >= :version', version: HASHED_STORAGE_FEATURES[feature]) }
+ scope :without_storage_feature, ->(feature) { where('storage_version < :version OR storage_version IS NULL', version: HASHED_STORAGE_FEATURES[feature]) }
+ scope :with_unmigrated_storage, -> { where('storage_version < :version OR storage_version IS NULL', version: LATEST_STORAGE_VERSION) }
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
@@ -351,6 +367,7 @@ class Project < ActiveRecord::Base
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
scope :excluding_project, ->(project) { where.not(id: project) }
+ scope :import_started, -> { where(import_status: 'started') }
state_machine :import_status, initial: :none do
event :import_schedule do
@@ -409,32 +426,11 @@ class Project < ActiveRecord::Base
#
# query - The search query as a String.
def search(query)
- ptable = arel_table
- ntable = Namespace.arel_table
- pattern = "%#{query}%"
-
- # unscoping unnecessary conditions that'll be applied
- # when executing `where("projects.id IN (#{union.to_sql})")`
- projects = unscoped.select(:id).where(
- ptable[:path].matches(pattern)
- .or(ptable[:name].matches(pattern))
- .or(ptable[:description].matches(pattern))
- )
-
- namespaces = unscoped.select(:id)
- .joins(:namespace)
- .where(ntable[:name].matches(pattern))
-
- union = Gitlab::SQL::Union.new([projects, namespaces])
-
- where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ fuzzy_search(query, [:path, :name, :description])
end
def search_by_title(query)
- pattern = "%#{query}%"
- table = Project.arel_table
-
- non_archived.where(table[:name].matches(pattern))
+ non_archived.fuzzy_search(query, [:name])
end
def visibility_levels
@@ -479,6 +475,13 @@ class Project < ActiveRecord::Base
end
end
+ # returns all ancestor-groups upto but excluding the given namespace
+ # when no namespace is given, all ancestors upto the top are returned
+ def ancestors_upto(top = nil)
+ Gitlab::GroupHierarchy.new(Group.where(id: namespace_id))
+ .base_and_ancestors(upto: top)
+ end
+
def lfs_enabled?
return namespace.lfs_enabled? if self[:lfs_enabled].nil?
@@ -530,6 +533,10 @@ class Project < ActiveRecord::Base
repository.commit(ref)
end
+ def commit_by(oid:)
+ repository.commit_by(oid: oid)
+ end
+
# ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = default_branch)
latest_pipeline = pipelines.latest_successful_for(ref)
@@ -543,7 +550,7 @@ class Project < ActiveRecord::Base
def merge_base_commit(first_commit_id, second_commit_id)
sha = repository.merge_base(first_commit_id, second_commit_id)
- repository.commit(sha) if sha
+ commit_by(oid: sha) if sha
end
def saved?
@@ -678,10 +685,6 @@ class Project < ActiveRecord::Base
import_type == 'gitea'
end
- def github_import?
- import_type == 'github'
- end
-
def check_limit
unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit
@@ -738,10 +741,10 @@ class Project < ActiveRecord::Base
end
end
- def to_human_reference(from_project = nil)
- if cross_namespace_reference?(from_project)
+ def to_human_reference(from = nil)
+ if cross_namespace_reference?(from)
name_with_namespace
- elsif cross_project_reference?(from_project)
+ elsif cross_project_reference?(from)
name
end
end
@@ -894,12 +897,10 @@ class Project < ActiveRecord::Base
@ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
- def deployment_services
- services.where(category: :deployment)
- end
-
- def deployment_service
- @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
+ # TODO: This will be extended for multiple enviroment clusters
+ def deployment_platform
+ @deployment_platform ||= clusters.find_by(enabled: true)&.platform_kubernetes
+ @deployment_platform ||= services.where(category: :deployment).reorder(nil).find_by(active: true)
end
def monitoring_services
@@ -1017,6 +1018,22 @@ class Project < ActiveRecord::Base
!(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
end
+ def fork_source
+ forked_from_project || fork_network&.root_project
+ end
+
+ def lfs_storage_project
+ @lfs_storage_project ||= begin
+ result = self
+
+ # TODO: Make this go to the fork_network root immeadiatly
+ # dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
+ result = result.fork_source while result&.forked?
+
+ result || self
+ end
+ end
+
def personal?
!group
end
@@ -1069,6 +1086,7 @@ class Project < ActiveRecord::Base
def hook_attrs(backward: true)
attrs = {
+ id: id,
name: name,
description: description,
web_url: web_url,
@@ -1160,6 +1178,10 @@ class Project < ActiveRecord::Base
!!repository.exists?
end
+ def wiki_repository_exists?
+ wiki.repository_exists?
+ end
+
# update visibility_level of forks
def update_forks_visibility_level
return unless visibility_level < visibility_level_was
@@ -1262,7 +1284,7 @@ class Project < ActiveRecord::Base
# self.forked_from_project will be nil before the project is saved, so
# we need to go through the relation
- original_project = forked_project_link.forked_from_project
+ original_project = forked_project_link&.forked_from_project
return true unless original_project
level <= original_project.visibility_level
@@ -1380,6 +1402,19 @@ class Project < ActiveRecord::Base
end
end
+ def after_rename_repo
+ path_before_change = previous_changes['path'].first
+
+ # We need to check if project had been rolled out to move resource to hashed storage or not and decide
+ # if we need execute any take action or no-op.
+
+ unless hashed_storage?(:attachments)
+ Gitlab::UploadsTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
+ end
+
+ Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
+ end
+
def rename_repo_notify!
send_move_instructions(full_path_was)
expires_full_path_cache
@@ -1390,11 +1425,29 @@ class Project < ActiveRecord::Base
reload_repository!
end
- def after_rename_repo
- path_before_change = previous_changes['path'].first
+ def after_import
+ repository.after_import
+ import_finish
+ remove_import_jid
+ update_project_counter_caches
+ end
- Gitlab::UploadsTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
- Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
+ def update_project_counter_caches
+ classes = [
+ Projects::OpenIssuesCountService,
+ Projects::OpenMergeRequestsCountService
+ ]
+
+ classes.each do |klass|
+ klass.new(self).refresh_cache
+ end
+ end
+
+ def remove_import_jid
+ return unless import_jid
+
+ Gitlab::SidekiqStatus.unset(import_jid)
+ update_column(:import_jid, nil)
end
def running_or_pending_build_count(force: false)
@@ -1458,7 +1511,8 @@ class Project < ActiveRecord::Base
{ key: 'CI_PROJECT_PATH', value: full_path, public: true },
{ key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true },
{ key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true },
- { key: 'CI_PROJECT_URL', value: web_url, public: true }
+ { key: 'CI_PROJECT_URL', value: web_url, public: true },
+ { key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level), public: true }
]
end
@@ -1491,9 +1545,9 @@ class Project < ActiveRecord::Base
end
def deployment_variables
- return [] unless deployment_service
+ return [] unless deployment_platform
- deployment_service.predefined_variables
+ deployment_platform.predefined_variables
end
def auto_devops_variables
@@ -1549,10 +1603,6 @@ class Project < ActiveRecord::Base
map.public_path_for_source_path(path)
end
- def parent
- namespace
- end
-
def parent_changed?
namespace_id_changed?
end
@@ -1590,8 +1640,13 @@ class Project < ActiveRecord::Base
[nil, 0].include?(self.storage_version)
end
- def hashed_storage?
- self.storage_version && self.storage_version >= 1
+ # Check if Hashed Storage is enabled for the project with at least informed feature rolled out
+ #
+ # @param [Symbol] feature that needs to be rolled out for the project (:repository, :attachments)
+ def hashed_storage?(feature)
+ raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature)
+
+ self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]
end
def renamed?
@@ -1627,7 +1682,7 @@ class Project < ActiveRecord::Base
end
def migrate_to_hashed_storage!
- return if hashed_storage?
+ return if hashed_storage?(:repository)
update!(repository_read_only: true)
@@ -1648,11 +1703,26 @@ class Project < ActiveRecord::Base
Gitlab::GlRepository.gl_repository(self, is_wiki)
end
+ def reference_counter(wiki: false)
+ Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki))
+ end
+
+ # Refreshes the expiration time of the associated import job ID.
+ #
+ # This method can be used by asynchronous importers to refresh the status,
+ # preventing the StuckImportJobsWorker from marking the import as failed.
+ def refresh_import_jid_expiration
+ return unless import_jid
+
+ Gitlab::SidekiqStatus
+ .set(import_jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+ end
+
private
def storage
@storage ||=
- if hashed_storage?
+ if hashed_storage?(:repository)
Storage::HashedProject.new(self)
else
Storage::LegacyProject.new(self)
@@ -1666,11 +1736,11 @@ class Project < ActiveRecord::Base
end
def repo_reference_count
- Gitlab::ReferenceCounter.new(gl_repository(is_wiki: false)).value
+ reference_counter.value
end
def wiki_reference_count
- Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value
+ reference_counter(wiki: true).value
end
def check_repository_absence!
diff --git a/app/models/project_custom_attribute.rb b/app/models/project_custom_attribute.rb
new file mode 100644
index 00000000000..3f1a7b86a82
--- /dev/null
+++ b/app/models/project_custom_attribute.rb
@@ -0,0 +1,6 @@
+class ProjectCustomAttribute < ActiveRecord::Base
+ belongs_to :project
+
+ validates :project, :key, :value, presence: true
+ validates :key, uniqueness: { scope: [:project_id] }
+end
diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
index 1327b075858..3273f41dbd2 100644
--- a/app/models/project_services/chat_message/issue_message.rb
+++ b/app/models/project_services/chat_message/issue_message.rb
@@ -39,7 +39,7 @@ module ChatMessage
private
def message
- if state == 'opened'
+ if opened_issue?
"[#{project_link}] Issue #{state} by #{user_combined_name}"
else
"[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 976d85246a8..768f0a7472e 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -51,8 +51,10 @@ class HipchatService < Service
def execute(data)
return unless supported_events.include?(data[:object_kind])
+
message = create_message(data)
return unless message.present?
+
gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 9ee3a533c1e..1c065e1ddbd 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -3,6 +3,8 @@ class JiraService < IssueTrackerService
validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true
+ validates :username, presence: true, if: :activated?
+ validates :password, presence: true, if: :activated?
prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description
@@ -174,6 +176,7 @@ class JiraService < IssueTrackerService
def test_settings
return unless client_url.present?
+
# Test settings by getting the project
jira_request { client.ServerInfo.all.attrs }
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index 8ba07173c74..b82567ce2b3 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -1,3 +1,8 @@
+##
+# NOTE:
+# We'll move this class to Clusters::Platforms::Kubernetes, which contains exactly the same logic.
+# After we've migrated data, we'll remove KubernetesService. This would happen in a few months.
+# If you're modyfiyng this class, please note that you should update the same change in Clusters::Platforms::Kubernetes.
class KubernetesService < DeploymentService
include Gitlab::CurrentSettings
include Gitlab::Kubernetes
@@ -136,6 +141,10 @@ class KubernetesService < DeploymentService
{ pods: read_pods }
end
+ def kubeclient
+ @kubeclient ||= build_kubeclient!
+ end
+
TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
private
@@ -153,7 +162,10 @@ class KubernetesService < DeploymentService
end
def default_namespace
- "#{project.path}-#{project.id}" if project.present?
+ return unless project
+
+ slug = "#{project.path}-#{project.id}".downcase
+ slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
def build_kubeclient!(api_path: 'api', api_version: 'v1')
@@ -175,6 +187,7 @@ class KubernetesService < DeploymentService
kubeclient.get_pods(namespace: actual_namespace).as_json
rescue KubeException => err
raise err unless err.error_code == 404
+
[]
end
diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb
new file mode 100644
index 00000000000..f68a0c1a3c3
--- /dev/null
+++ b/app/models/project_services/packagist_service.rb
@@ -0,0 +1,65 @@
+class PackagistService < Service
+ include HTTParty
+
+ prop_accessor :username, :token, :server
+
+ validates :username, presence: true, if: :activated?
+ validates :token, presence: true, if: :activated?
+
+ default_value_for :push_events, true
+ default_value_for :tag_push_events, true
+
+ after_save :compose_service_hook, if: :activated?
+
+ def title
+ 'Packagist'
+ end
+
+ def description
+ 'Update your project on Packagist, the main Composer repository'
+ end
+
+ def self.to_param
+ 'packagist'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'username', placeholder: '', required: true },
+ { type: 'text', name: 'token', placeholder: '', required: true },
+ { type: 'text', name: 'server', placeholder: 'https://packagist.org', required: false }
+ ]
+ end
+
+ def self.supported_events
+ %w(push merge_request tag_push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ service_hook.execute(data)
+ end
+
+ def test(data)
+ begin
+ result = execute(data)
+ return { success: false, result: result[:message] } if result[:http_status] != 202
+ rescue StandardError => error
+ return { success: false, result: error }
+ end
+
+ { success: true, result: result[:message] }
+ end
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.url = hook_url
+ hook.save
+ end
+
+ def hook_url
+ base_url = server.present? ? server : 'https://packagist.org'
+ "#{base_url}/api/update-package?username=#{username}&apiToken=#{token}"
+ end
+end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 217f753f05f..fa7b3f2bcaf 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -25,7 +25,7 @@ class PrometheusService < MonitoringService
end
def description
- 'Prometheus monitoring'
+ s_('PrometheusService|Prometheus monitoring')
end
def self.to_param
@@ -38,8 +38,8 @@ class PrometheusService < MonitoringService
type: 'text',
name: 'api_url',
title: 'API URL',
- placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/',
- help: 'By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.',
+ placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'),
+ help: s_('PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.'),
required: true
}
]
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index bb7be29ef66..a0af749a93f 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -21,7 +21,7 @@ class ProjectWiki
end
delegate :empty?, to: :pages
- delegate :repository_storage_path, to: :project
+ delegate :repository_storage_path, :hashed_storage?, to: :project
def path
@project.path + '.wiki'
@@ -76,8 +76,8 @@ class ProjectWiki
# Returns an Array of Gitlab WikiPage instances or an
# empty Array if this Wiki has no pages.
- def pages
- wiki.pages.map { |page| WikiPage.new(self, page, true) }
+ def pages(limit: nil)
+ wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) }
end
# Finds a page within the repository based on a tile
@@ -135,7 +135,7 @@ class ProjectWiki
end
def repository
- @repository ||= Repository.new(full_path, @project, disk_path: disk_path)
+ @repository ||= Repository.new(full_path, @project, disk_path: disk_path, is_wiki: true)
end
def default_branch
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 89bfc5f9a9c..d28fed11ca8 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -10,7 +10,9 @@ class ProtectedBranch < ActiveRecord::Base
def self.protected?(project, ref_name)
return true if project.empty_repo? && default_branch_protected?
- self.matching(ref_name, protected_refs: project.protected_branches).present?
+ refs = project.protected_branches.select(:name)
+
+ self.matching(ref_name, protected_refs: refs).present?
end
def self.default_branch_protected?
diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb
index f38109c0e52..42a9bcf7723 100644
--- a/app/models/protected_tag.rb
+++ b/app/models/protected_tag.rb
@@ -5,6 +5,8 @@ class ProtectedTag < ActiveRecord::Base
protected_ref_access_levels :create
def self.protected?(project, ref_name)
- self.matching(ref_name, protected_refs: project.protected_tags).present?
+ refs = project.protected_tags.select(:name)
+
+ self.matching(ref_name, protected_refs: refs).present?
end
end
diff --git a/app/models/protected_tag/create_access_level.rb b/app/models/protected_tag/create_access_level.rb
index c7e1319719d..6b6ab3d8279 100644
--- a/app/models/protected_tag/create_access_level.rb
+++ b/app/models/protected_tag/create_access_level.rb
@@ -1,18 +1,6 @@
class ProtectedTag::CreateAccessLevel < ActiveRecord::Base
include ProtectedTagAccess
- validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
- Gitlab::Access::DEVELOPER,
- Gitlab::Access::NO_ACCESS] }
-
- def self.human_access_levels
- {
- Gitlab::Access::MASTER => "Masters",
- Gitlab::Access::DEVELOPER => "Developers + Masters",
- Gitlab::Access::NO_ACCESS => "No one"
- }.with_indifferent_access
- end
-
def check_access(user)
return false if access_level == Gitlab::Access::NO_ACCESS
diff --git a/app/models/repository.rb b/app/models/repository.rb
index d725c65081d..165dafd83fd 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -15,9 +15,8 @@ class Repository
].freeze
include Gitlab::ShellAdapter
- include RepositoryMirroring
- attr_accessor :full_path, :disk_path, :project
+ attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository
@@ -34,7 +33,8 @@ class Repository
CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide
changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count
- tag_count avatar exists? empty? root_ref has_visible_content?).freeze
+ tag_count avatar exists? empty? root_ref has_visible_content?
+ issue_template_names merge_request_template_names).freeze
# Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze
@@ -50,7 +50,9 @@ class Repository
gitignore: :gitignore,
koding: :koding_yml,
gitlab_ci: :gitlab_ci_yml,
- avatar: :avatar
+ avatar: :avatar,
+ issue_template: :issue_template_names,
+ merge_request_template: :merge_request_template_names
}.freeze
# Wraps around the given method and caches its output in Redis and an instance
@@ -69,10 +71,12 @@ class Repository
end
end
- def initialize(full_path, project, disk_path: nil)
+ def initialize(full_path, project, disk_path: nil, is_wiki: false)
@full_path = full_path
@disk_path = disk_path || full_path
@project = project
+ @commit_cache = {}
+ @is_wiki = is_wiki
end
def ==(other)
@@ -100,18 +104,17 @@ class Repository
def commit(ref = 'HEAD')
return nil unless exists?
+ return ref if ref.is_a?(::Commit)
- commit =
- if ref.is_a?(Gitlab::Git::Commit)
- ref
- else
- Gitlab::Git::Commit.find(raw_repository, ref)
- end
+ find_commit(ref)
+ end
- commit = ::Commit.new(commit, @project) if commit
- commit
- rescue Rugged::OdbError, Rugged::TreeError
- nil
+ # Finding a commit by the passed SHA
+ # Also takes care of caching, based on the SHA
+ def commit_by(oid:)
+ return @commit_cache[oid] if @commit_cache.key?(oid)
+
+ @commit_cache[oid] = find_commit(oid)
end
def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
@@ -129,7 +132,8 @@ class Repository
commits = Gitlab::Git::Commit.where(options)
commits = Commit.decorate(commits, @project) if commits.present?
- commits
+
+ CommitCollection.new(project, commits, ref)
end
def commits_between(from, to)
@@ -145,11 +149,14 @@ class Repository
end
raw_repository.gitaly_migrate(:commits_by_message) do |is_enabled|
- if is_enabled
- find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
- else
- find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
- end
+ commits =
+ if is_enabled
+ find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
+ else
+ find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
+ end
+
+ CommitCollection.new(project, commits, ref)
end
end
@@ -210,11 +217,7 @@ class Repository
def branch_exists?(branch_name)
return false unless raw_repository
- @branch_exists_memo ||= Hash.new do |hash, key|
- hash[key] = raw_repository.branch_exists?(key)
- end
-
- @branch_exists_memo[branch_name]
+ branch_names.include?(branch_name)
end
def ref_exists?(ref)
@@ -228,7 +231,7 @@ class Repository
# branches or tags, but we want to keep some of these commits around, for
# example if they have comments or CI builds.
def keep_around(sha)
- return unless sha && commit(sha)
+ return unless sha && commit_by(oid: sha)
return if kept_around?(sha)
@@ -239,6 +242,7 @@ class Repository
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 #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
end
end
@@ -465,13 +469,16 @@ class Repository
end
def blob_at(sha, path)
- unless Gitlab::Git.blank_ref?(sha)
- Blob.decorate(Gitlab::Git::Blob.find(self, sha, path), project)
- end
+ Blob.decorate(raw_repository.blob_at(sha, path), project)
rescue Gitlab::Git::Repository::NoRepository
nil
end
+ # items is an Array like: [[oid, path], [oid1, path1]]
+ def blobs_at(items)
+ raw_repository.batch_blobs(items).map { |blob| Blob.decorate(blob, project) }
+ end
+
def root_ref
if raw_repository
raw_repository.root_ref
@@ -535,6 +542,16 @@ class Repository
end
cache_method :avatar
+ def issue_template_names
+ Gitlab::Template::IssueTemplate.dropdown_names(project)
+ end
+ cache_method :issue_template_names, fallback: []
+
+ def merge_request_template_names
+ Gitlab::Template::MergeRequestTemplate.dropdown_names(project)
+ end
+ cache_method :merge_request_template_names, fallback: []
+
def readme
if readme = tree(:head)&.readme
ReadmeBlob.new(readme, self)
@@ -651,6 +668,7 @@ class Repository
def next_branch(name, opts = {})
branch_ids = self.branch_names.map do |n|
next 1 if n == name
+
result = n.match(/\A#{name}-([0-9]+)\z/)
result[1].to_i if result
end.compact
@@ -851,22 +869,12 @@ class Repository
end
def ff_merge(user, source, target_branch, merge_request: nil)
- our_commit = rugged.branches[target_branch].target
- their_commit =
- if source.is_a?(Gitlab::Git::Commit)
- source.raw_commit
- else
- rugged.lookup(source)
- end
+ their_commit_id = commit(source)&.id
+ raise 'Invalid merge source' if their_commit_id.nil?
- raise 'Invalid merge target' if our_commit.nil?
- raise 'Invalid merge source' if their_commit.nil?
+ merge_request&.update(in_progress_merge_commit_sha: their_commit_id)
- with_branch(user, target_branch) do |start_commit|
- merge_request&.update(in_progress_merge_commit_sha: their_commit.oid)
-
- their_commit.oid
- end
+ with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }
end
def revert(
@@ -901,26 +909,21 @@ class Repository
end
end
- def resolve_conflicts(user, branch_name, params)
- with_branch(user, branch_name) do
- committer = user_to_committer(user)
+ def merged_to_root_ref?(branch_or_name)
+ branch = Gitlab::Git::Branch.find(self, branch_or_name)
- create_commit(params.merge(author: committer, committer: committer))
- end
- end
-
- def merged_to_root_ref?(branch_name)
- branch_commit = commit(branch_name)
- root_ref_commit = commit(root_ref)
-
- if branch_commit
- same_head = branch_commit.id == root_ref_commit.id
- !same_head && ancestor?(branch_commit.id, root_ref_commit.id)
+ if branch
+ @root_ref_sha ||= commit(root_ref).sha
+ same_head = branch.target == @root_ref_sha
+ merged = ancestor?(branch.target, @root_ref_sha)
+ !same_head && merged
else
nil
end
end
+ delegate :merged_branch_names, to: :raw_repository
+
def merge_base(first_commit_id, second_commit_id)
first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
second_commit_id = commit(second_commit_id).try(:id) || second_commit_id
@@ -963,25 +966,29 @@ class Repository
run_git(args).first.lines.map(&:strip)
end
- def add_remote(name, url)
- raw_repository.remote_add(name, url)
- rescue Rugged::ConfigError
- raw_repository.remote_update(name, url: url)
+ def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil)
+ unless remote_name
+ remote_name = "tmp-#{SecureRandom.hex}"
+ tmp_remote_name = true
+ end
+
+ add_remote(remote_name, url)
+ set_remote_as_mirror(remote_name, refmap: refmap)
+ fetch_remote(remote_name, forced: forced)
+ ensure
+ remove_remote(remote_name) if tmp_remote_name
end
- def remove_remote(name)
- raw_repository.remote_delete(name)
- true
- rescue Rugged::ConfigError
- false
+ def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false)
+ gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)
end
- def fetch_remote(remote, forced: false, no_tags: false)
- gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags)
+ def fetch_source_branch!(source_repository, source_branch, local_ref)
+ raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref)
end
- def fetch_source_branch(source_repository, source_branch, local_ref)
- raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref)
+ def remote_exists?(name)
+ raw_repository.remote_exists?(name)
end
def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
@@ -997,10 +1004,6 @@ class Repository
raw_repository.ls_files(actual_ref)
end
- def gitattribute(path, name)
- raw_repository.attributes(path)[name]
- end
-
def copy_gitattributes(ref)
actual_ref = ref || root_ref
begin
@@ -1028,6 +1031,10 @@ class Repository
if instance_variable_defined?(ivar)
instance_variable_get(ivar)
else
+ # If the repository doesn't exist and a fallback was specified we return
+ # that value inmediately. This saves us Rugged/gRPC invocations.
+ return fallback unless fallback.nil? || exists?
+
begin
value =
if memoize_only
@@ -1037,8 +1044,9 @@ class Repository
end
instance_variable_set(ivar, value)
rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
- # if e.g. HEAD or the entire repository doesn't exist we want to
- # gracefully handle this and not cache anything.
+ # Even if the above `#exists?` check passes these errors might still
+ # occur (for example because of a non-existing HEAD). We want to
+ # gracefully handle this and not cache anything
fallback
end
end
@@ -1064,8 +1072,28 @@ class Repository
blob_data_at(sha, path)
end
+ def fetch_ref(source_repository, source_ref:, target_ref:)
+ raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
+ end
+
+ def repository_storage_path
+ @project.repository_storage_path
+ end
+
private
+ # TODO Generice finder, later split this on finders by Ref or Oid
+ # gitlab-org/gitlab-ce#39239
+ def find_commit(oid_or_ref)
+ commit = if oid_or_ref.is_a?(Gitlab::Git::Commit)
+ oid_or_ref
+ else
+ Gitlab::Git::Commit.find(raw_repository, oid_or_ref)
+ end
+
+ ::Commit.new(commit, @project) if commit
+ end
+
def blob_data_at(sha, path)
blob = blob_at(sha, path)
return unless blob
@@ -1104,25 +1132,21 @@ class Repository
def last_commit_for_path_by_gitaly(sha, path)
c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path)
- commit(c)
+ commit_by(oid: c)
end
def last_commit_for_path_by_rugged(sha, path)
sha = last_commit_id_for_path_by_shelling_out(sha, path)
- commit(sha)
+ commit_by(oid: sha)
end
def last_commit_id_for_path_by_shelling_out(sha, path)
args = %W(rev-list --max-count=1 #{sha} -- #{path})
- run_git(args).first.strip
- end
-
- def repository_storage_path
- @project.repository_storage_path
+ raw_repository.run_git_with_timeout(args, Gitlab::Git::Popen::FAST_GIT_PROCESS_TIMEOUT).first.strip
end
def initialize_raw_repository
- Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false))
+ Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki))
end
def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 298569cb7a6..6e311806be1 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -53,13 +53,17 @@ class SentNotification < ActiveRecord::Base
end
def unsubscribable?
- !for_commit?
+ !(for_commit? || for_snippet?)
end
def for_commit?
noteable_type == "Commit"
end
+ def for_snippet?
+ noteable_type.end_with?('Snippet')
+ end
+
def noteable
if for_commit?
project.commit(commit_id) rescue nil
diff --git a/app/models/service.rb b/app/models/service.rb
index 6b64079215f..fdd2605e3e3 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -238,6 +238,7 @@ class Service < ActiveRecord::Base
kubernetes
mattermost_slash_commands
mattermost
+ packagist
pipelines_email
pivotaltracker
prometheus
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 9533aa7f555..05a16f11b59 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -9,6 +9,7 @@ class Snippet < ActiveRecord::Base
include Mentionable
include Spammable
include Editable
+ include Gitlab::SQL::Pattern
extend Gitlab::CurrentSettings
@@ -75,11 +76,11 @@ class Snippet < ActiveRecord::Base
@link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end
- def to_reference(from_project = nil, full: false)
+ def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{id}"
if project.present?
- "#{project.to_reference(from_project, full: full)}#{reference}"
+ "#{project.to_reference(from, full: full)}#{reference}"
else
reference
end
@@ -135,10 +136,7 @@ class Snippet < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def search(query)
- t = arel_table
- pattern = "%#{query}%"
-
- where(t[:title].matches(pattern).or(t[:file_name].matches(pattern)))
+ fuzzy_search(query, [:title, :file_name])
end
# Searches for snippets with matching content.
@@ -149,10 +147,7 @@ class Snippet < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def search_code(query)
- table = Snippet.arel_table
- pattern = "%#{query}%"
-
- where(table[:content].matches(pattern))
+ fuzzy_search(query, [:content])
end
end
end
diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb
index f025f40994e..fae1b64961a 100644
--- a/app/models/storage/hashed_project.rb
+++ b/app/models/storage/hashed_project.rb
@@ -4,7 +4,6 @@ module Storage
delegate :gitlab_shell, :repository_storage_path, to: :project
ROOT_PATH_PREFIX = '@hashed'.freeze
- STORAGE_VERSION = 1
def initialize(project)
@project = project
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 1f9f8d7286b..29035480371 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -1,4 +1,14 @@
class SystemNoteMetadata < ActiveRecord::Base
+ # These notes's action text might contain a reference that is external.
+ # We should always force a deep validation upon references that are found
+ # in this note type.
+ # Other notes can always be safely shown as all its references are
+ # in the same project (i.e. with the same permissions)
+ TYPES_WITH_CROSS_REFERENCES = %w[
+ commit cross_reference
+ close duplicate
+ ].freeze
+
ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved
diff --git a/app/models/user.rb b/app/models/user.rb
index 533a776bc65..14941fd7f98 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -21,8 +21,8 @@ class User < ActiveRecord::Base
ignore_column :external_email
ignore_column :email_provider
+ ignore_column :authentication_token
- add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token
add_authentication_token_field :rss_token
@@ -146,7 +146,7 @@ class User < ActiveRecord::Base
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username,
- dynamic_path: true,
+ user_path: true,
presence: true,
uniqueness: { case_sensitive: false }
@@ -163,12 +163,14 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed?
before_validation :set_public_email, if: :public_email_changed?
- before_save :ensure_authentication_token, :ensure_incoming_email_token
- before_save :ensure_user_rights_and_limits, if: :external_changed?
+ before_save :ensure_incoming_email_token
+ before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
after_save :ensure_namespace_correct
+ after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
+ after_destroy :remove_key_cache
after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') }
after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
@@ -182,15 +184,8 @@ class User < ActiveRecord::Base
enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos]
# User's Project preference
- #
- # Note: When adding an option, it MUST go on the end of the hash with a
- # number higher than the current max. We cannot move options and/or change
- # their numbers.
- #
- # We skip 0 because this was used by an option that has since been removed.
- enum project_view: { activity: 1, files: 2 }
-
- alias_attribute :private_token, :authentication_token
+ # Note: When adding an option, it MUST go on the end of the array.
+ enum project_view: [:readme, :activity, :files]
delegate :path, to: :namespace, allow_nil: true, prefix: true
@@ -273,18 +268,22 @@ class User < ActiveRecord::Base
end
end
+ def for_github_id(id)
+ joins(:identities).merge(Identity.with_extern_uid(:github, id))
+ end
+
# Find a User by their primary email or any associated secondary email
def find_by_any_email(email)
- sql = 'SELECT *
- FROM users
- WHERE id IN (
- SELECT id FROM users WHERE email = :email
- UNION
- SELECT emails.user_id FROM emails WHERE email = :email
- )
- LIMIT 1;'
+ by_any_email(email).take
+ end
- User.find_by_sql([sql, { email: email }]).first
+ # Returns a relation containing all the users for the given Email address
+ def by_any_email(email)
+ users = where(email: email)
+ emails = joins(:emails).where(emails: { email: email })
+ union = Gitlab::SQL::Union.new([users, emails])
+
+ from("(#{union.to_sql}) #{table_name}")
end
def filter(filter_name)
@@ -314,9 +313,6 @@ class User < ActiveRecord::Base
#
# Returns an ActiveRecord::Relation.
def search(query)
- table = arel_table
- pattern = User.to_pattern(query)
-
order = <<~SQL
CASE
WHEN users.name = %{query} THEN 0
@@ -326,11 +322,8 @@ class User < ActiveRecord::Base
END
SQL
- where(
- table[:name].matches(pattern)
- .or(table[:email].matches(pattern))
- .or(table[:username].matches(pattern))
- ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
+ fuzzy_search(query, [:name, :email, :username])
+ .reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
end
# searches user by given pattern
@@ -338,16 +331,16 @@ class User < ActiveRecord::Base
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
def search_with_secondary_emails(query)
- table = arel_table
email_table = Email.arel_table
- pattern = "%#{query}%"
- matched_by_emails_user_ids = email_table.project(email_table[:user_id]).where(email_table[:email].matches(pattern))
+ matched_by_emails_user_ids = email_table
+ .project(email_table[:user_id])
+ .where(Email.fuzzy_arel_match(:email, query))
where(
- table[:name].matches(pattern)
- .or(table[:email].matches(pattern))
- .or(table[:username].matches(pattern))
- .or(table[:id].in(matched_by_emails_user_ids))
+ fuzzy_arel_match(:name, query)
+ .or(fuzzy_arel_match(:email, query))
+ .or(fuzzy_arel_match(:username, query))
+ .or(arel_table[:id].in(matched_by_emails_user_ids))
)
end
@@ -438,7 +431,7 @@ class User < ActiveRecord::Base
username
end
- def to_reference(_from_project = nil, target_project: nil, full: nil)
+ def to_reference(_from = nil, target_project: nil, full: nil)
"#{self.class.reference_prefix}#{username}"
end
@@ -446,6 +439,10 @@ class User < ActiveRecord::Base
skip_confirmation! if bool
end
+ def skip_reconfirmation=(bool)
+ skip_reconfirmation! if bool
+ end
+
def generate_reset_token
@reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token)
@@ -625,21 +622,39 @@ class User < ActiveRecord::Base
end
def require_ssh_key?
- keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh')
+ count = Users::KeysCountService.new(self).count
+
+ count.zero? && Gitlab::ProtocolAccess.allowed?('ssh')
+ end
+
+ def require_password_creation_for_web?
+ allow_password_authentication_for_web? && password_automatically_set?
end
- def require_password_creation?
- password_automatically_set? && allow_password_authentication?
+ def require_password_creation_for_git?
+ allow_password_authentication_for_git? && password_automatically_set?
end
def require_personal_access_token_creation_for_git_auth?
- return false if current_application_settings.password_authentication_enabled? || ldap_user?
+ return false if allow_password_authentication_for_git? || ldap_user?
PersonalAccessTokensFinder.new(user: self, impersonation: false, state: 'active').execute.none?
end
+ def require_extra_setup_for_git_auth?
+ require_password_creation_for_git? || require_personal_access_token_creation_for_git_auth?
+ end
+
def allow_password_authentication?
- !ldap_user? && current_application_settings.password_authentication_enabled?
+ allow_password_authentication_for_web? || allow_password_authentication_for_git?
+ end
+
+ def allow_password_authentication_for_web?
+ current_application_settings.password_authentication_enabled_for_web? && !ldap_user?
+ end
+
+ def allow_password_authentication_for_git?
+ current_application_settings.password_authentication_enabled_for_git? && !ldap_user?
end
def can_change_username?
@@ -878,11 +893,19 @@ class User < ActiveRecord::Base
end
end
+ def username_changed_hook
+ system_hook_service.execute_hooks_for(self, :rename)
+ end
+
def post_destroy_hook
log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy)
end
+ def remove_key_cache
+ Users::KeysCountService.new(self).delete_cache
+ end
+
def delete_async(deleted_by:, params: {})
block if params[:hard_delete]
DeleteUserWorker.perform_async(deleted_by.id, id, params)
@@ -918,7 +941,16 @@ class User < ActiveRecord::Base
end
def manageable_namespaces
- @manageable_namespaces ||= [namespace] + owned_groups + masters_groups
+ @manageable_namespaces ||= [namespace] + manageable_groups
+ end
+
+ def manageable_groups
+ union = Gitlab::SQL::Union.new([owned_groups.select(:id),
+ masters_groups.select(:id)])
+ arel_union = Arel::Nodes::SqlLiteral.new(union.to_sql)
+ owned_and_master_groups = Group.where(Group.arel_table[:id].in(arel_union))
+
+ Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
end
def namespaces
@@ -1107,6 +1139,7 @@ class User < ActiveRecord::Base
# override, from Devise::Validatable
def password_required?
return false if internal?
+
super
end
@@ -1124,6 +1157,7 @@ class User < ActiveRecord::Base
# Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
def send_devise_notification(notification, *args)
return true unless can?(:receive_notifications)
+
devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
end
@@ -1141,8 +1175,9 @@ class User < ActiveRecord::Base
self.can_create_group = false
self.projects_limit = 0
else
- self.can_create_group = gitlab_config.default_can_create_group
- self.projects_limit = current_application_settings.default_projects_limit
+ # Only revert these back to the default if they weren't specifically changed in this update.
+ self.can_create_group = gitlab_config.default_can_create_group unless can_create_group_changed?
+ self.projects_limit = current_application_settings.default_projects_limit unless projects_limit_changed?
end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 5f710961f95..bdfef677ef3 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -127,19 +127,24 @@ class WikiPage
@version ||= @page.version
end
- # Returns an array of Gitlab Commit instances.
- def versions
+ def versions(options = {})
return [] unless persisted?
- wiki.wiki.page_versions(@page.path)
+ wiki.wiki.page_versions(@page.path, options)
end
- def commit
- versions.first
+ def count_versions
+ return [] unless persisted?
+
+ wiki.wiki.count_page_versions(@page.path)
+ end
+
+ def last_version
+ @last_version ||= versions(limit: 1).first
end
def last_commit_sha
- commit&.sha
+ last_version&.sha
end
# Returns the Date that this latest version was
@@ -151,7 +156,7 @@ class WikiPage
# Returns boolean True or False if this instance
# is an old version of the page.
def historical?
- @page.historical? && versions.first.sha != version.sha
+ @page.historical? && last_version.sha != version.sha
end
# Returns boolean True or False if this instance
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 984e5482288..1ab391a5a9d 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -10,6 +10,15 @@ module Ci
end
end
- rule { protected_ref }.prevent :update_build
+ condition(:owner_of_job) do
+ can?(:developer_access) && @subject.triggered_by?(@user)
+ end
+
+ rule { protected_ref }.policy do
+ prevent :update_build
+ prevent :erase_build
+ end
+
+ rule { can?(:master_access) | owner_of_job }.enable :erase_build
end
end
diff --git a/app/policies/clusters/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb
new file mode 100644
index 00000000000..1f7c13072b9
--- /dev/null
+++ b/app/policies/clusters/cluster_policy.rb
@@ -0,0 +1,12 @@
+module Clusters
+ class ClusterPolicy < BasePolicy
+ alias_method :cluster, :subject
+
+ delegate { cluster.project }
+
+ rule { can?(:master_access) }.policy do
+ enable :update_cluster
+ enable :admin_cluster
+ end
+ end
+end
diff --git a/app/policies/gcp/cluster_policy.rb b/app/policies/gcp/cluster_policy.rb
deleted file mode 100644
index e77173ea6e1..00000000000
--- a/app/policies/gcp/cluster_policy.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-module Gcp
- class ClusterPolicy < BasePolicy
- alias_method :cluster, :subject
-
- delegate { @subject.project }
-
- rule { can?(:master_access) }.policy do
- enable :update_cluster
- enable :admin_cluster
- end
- end
-end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 8af9738d75c..a2518bc1080 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -34,6 +34,8 @@ class GroupPolicy < BasePolicy
rule { admin } .enable :read_group
rule { has_projects } .enable :read_group
+ rule { has_access }.enable :read_namespace
+
rule { developer }.enable :admin_milestones
rule { reporter }.enable :admin_label
diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb
index 92213f0155e..eb01218eb0a 100644
--- a/app/policies/namespace_policy.rb
+++ b/app/policies/namespace_policy.rb
@@ -8,6 +8,7 @@ class NamespacePolicy < BasePolicy
rule { owner | admin }.policy do
enable :create_projects
enable :admin_namespace
+ enable :read_namespace
end
rule { personal_project & ~can_create_personal_project }.prevent :create_projects
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
new file mode 100644
index 00000000000..01cb59d0d44
--- /dev/null
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -0,0 +1,9 @@
+module Clusters
+ class ClusterPresenter < Gitlab::View::Presenter::Delegated
+ presents :cluster
+
+ def gke_cluster_url
+ "https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?
+ end
+ end
+end
diff --git a/app/presenters/gcp/cluster_presenter.rb b/app/presenters/gcp/cluster_presenter.rb
deleted file mode 100644
index f7908f92a37..00000000000
--- a/app/presenters/gcp/cluster_presenter.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Gcp
- class ClusterPresenter < Gitlab::View::Presenter::Delegated
- presents :cluster
-
- def gke_cluster_url
- "https://console.cloud.google.com/kubernetes/clusters/details/#{gcp_cluster_zone}/#{gcp_cluster_name}"
- end
- end
-end
diff --git a/app/serializers/base_serializer.rb b/app/serializers/base_serializer.rb
index 4e6c15f673b..8cade280b0c 100644
--- a/app/serializers/base_serializer.rb
+++ b/app/serializers/base_serializer.rb
@@ -1,6 +1,9 @@
class BaseSerializer
- def initialize(parameters = {})
- @request = EntityRequest.new(parameters)
+ attr_reader :params
+
+ def initialize(params = {})
+ @params = params
+ @request = EntityRequest.new(params)
end
def represent(resource, opts = {}, entity_class = nil)
diff --git a/app/serializers/blob_entity.rb b/app/serializers/blob_entity.rb
index 56f173e5a27..ad039a2623d 100644
--- a/app/serializers/blob_entity.rb
+++ b/app/serializers/blob_entity.rb
@@ -3,10 +3,6 @@ class BlobEntity < Grape::Entity
expose :id, :path, :name, :mode
- expose :last_commit do |blob|
- request.project.repository.last_commit_for_path(blob.commit_id, blob.path)
- end
-
expose :icon do |blob|
IconsHelper.file_type_icon_class('file', blob.mode, blob.name)
end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 8c89eea607f..69d46f5ec14 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -6,7 +6,7 @@ class BuildDetailsEntity < JobEntity
expose :pipeline, using: PipelineEntity
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
- expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :update_build, project) } do |build|
+ expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)
end
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
new file mode 100644
index 00000000000..3f9a275ad08
--- /dev/null
+++ b/app/serializers/cluster_application_entity.rb
@@ -0,0 +1,5 @@
+class ClusterApplicationEntity < Grape::Entity
+ expose :name
+ expose :status_name, as: :status
+ expose :status_reason
+end
diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb
index 08a113c4d8a..7e5b0997878 100644
--- a/app/serializers/cluster_entity.rb
+++ b/app/serializers/cluster_entity.rb
@@ -3,4 +3,5 @@ class ClusterEntity < Grape::Entity
expose :status_name, as: :status
expose :status_reason
+ expose :applications, using: ClusterApplicationEntity
end
diff --git a/app/serializers/cluster_serializer.rb b/app/serializers/cluster_serializer.rb
index 2c87202a105..2e13c1501e7 100644
--- a/app/serializers/cluster_serializer.rb
+++ b/app/serializers/cluster_serializer.rb
@@ -2,6 +2,6 @@ class ClusterSerializer < BaseSerializer
entity ClusterEntity
def represent_status(resource)
- represent(resource, { only: [:status, :status_reason] })
+ represent(resource, { only: [:status, :status_reason, :applications] })
end
end
diff --git a/app/serializers/concerns/with_pagination.rb b/app/serializers/concerns/with_pagination.rb
new file mode 100644
index 00000000000..d29e22d6740
--- /dev/null
+++ b/app/serializers/concerns/with_pagination.rb
@@ -0,0 +1,22 @@
+module WithPagination
+ attr_accessor :paginator
+
+ def with_pagination(request, response)
+ tap { self.paginator = Gitlab::Serializer::Pagination.new(request, response) }
+ end
+
+ def paginated?
+ paginator.present?
+ end
+
+ # super is `BaseSerializer#represent` here.
+ #
+ # we shouldn't try to paginate single resources
+ def represent(resource, opts = {})
+ if paginated? && resource.respond_to?(:page)
+ super(@paginator.paginate(resource), opts)
+ else
+ super(resource, opts)
+ end
+ end
+end
diff --git a/app/serializers/container_tag_entity.rb b/app/serializers/container_tag_entity.rb
index ec1fc349586..8f1488e6cbb 100644
--- a/app/serializers/container_tag_entity.rb
+++ b/app/serializers/container_tag_entity.rb
@@ -1,10 +1,10 @@
class ContainerTagEntity < Grape::Entity
include RequestAwareEntity
- expose :name, :location, :revision, :total_size, :created_at
+ expose :name, :location, :revision, :short_revision, :total_size, :created_at
expose :destroy_path, if: -> (*) { can_destroy? } do |tag|
- project_registry_repository_tag_path(project, tag.repository, tag.name, format: :json)
+ project_registry_repository_tag_path(project, tag.repository, tag.name)
end
private
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
index 88842a9aa75..84722f33f59 100644
--- a/app/serializers/environment_serializer.rb
+++ b/app/serializers/environment_serializer.rb
@@ -1,4 +1,6 @@
class EnvironmentSerializer < BaseSerializer
+ include WithPagination
+
Item = Struct.new(:name, :size, :latest)
entity EnvironmentEntity
@@ -7,18 +9,10 @@ class EnvironmentSerializer < BaseSerializer
tap { @itemize = true }
end
- def with_pagination(request, response)
- tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
- end
-
def itemized?
@itemize
end
- def paginated?
- @paginator.present?
- end
-
def represent(resource, opts = {})
if itemized?
itemize(resource).map do |item|
@@ -27,8 +21,6 @@ class EnvironmentSerializer < BaseSerializer
latest: super(item.latest, opts) }
end
else
- resource = @paginator.paginate(resource) if paginated?
-
super(resource, opts)
end
end
diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb
new file mode 100644
index 00000000000..37240bfb0b1
--- /dev/null
+++ b/app/serializers/group_child_entity.rb
@@ -0,0 +1,77 @@
+class GroupChildEntity < Grape::Entity
+ include ActionView::Helpers::NumberHelper
+ include RequestAwareEntity
+
+ expose :id, :name, :description, :visibility, :full_name,
+ :created_at, :updated_at, :avatar_url
+
+ expose :type do |instance|
+ type
+ end
+
+ expose :can_edit do |instance|
+ return false unless request.respond_to?(:current_user)
+
+ can?(request.current_user, "admin_#{type}", instance)
+ end
+
+ expose :edit_path do |instance|
+ # We know `type` will be one either `project` or `group`.
+ # The `edit_polymorphic_path` helper would try to call the path helper
+ # with a plural: `edit_groups_path(instance)` or `edit_projects_path(instance)`
+ # while our methods are `edit_group_path` or `edit_group_path`
+ public_send("edit_#{type}_path", instance) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ expose :relative_path do |instance|
+ polymorphic_path(instance)
+ end
+
+ expose :permission do |instance|
+ membership&.human_access
+ end
+
+ # Project only attributes
+ expose :star_count,
+ if: lambda { |_instance, _options| project? }
+
+ # Group only attributes
+ expose :children_count, :parent_id, :project_count, :subgroup_count,
+ unless: lambda { |_instance, _options| project? }
+
+ expose :leave_path, unless: lambda { |_instance, _options| project? } do |instance|
+ leave_group_members_path(instance)
+ end
+
+ expose :can_leave, unless: lambda { |_instance, _options| project? } do |instance|
+ if membership
+ can?(request.current_user, :destroy_group_member, membership)
+ else
+ false
+ end
+ end
+
+ expose :number_projects_with_delimiter, unless: lambda { |_instance, _options| project? } do |instance|
+ number_with_delimiter(instance.project_count)
+ end
+
+ expose :number_users_with_delimiter, unless: lambda { |_instance, _options| project? } do |instance|
+ number_with_delimiter(instance.member_count)
+ end
+
+ private
+
+ def membership
+ return unless request.current_user
+
+ @membership ||= request.current_user.members.find_by(source: object)
+ end
+
+ def project?
+ object.is_a?(Project)
+ end
+
+ def type
+ object.class.name.downcase
+ end
+end
diff --git a/app/serializers/group_child_serializer.rb b/app/serializers/group_child_serializer.rb
new file mode 100644
index 00000000000..2baef0a5703
--- /dev/null
+++ b/app/serializers/group_child_serializer.rb
@@ -0,0 +1,51 @@
+class GroupChildSerializer < BaseSerializer
+ include WithPagination
+
+ attr_reader :hierarchy_root, :should_expand_hierarchy
+
+ entity GroupChildEntity
+
+ def expand_hierarchy(hierarchy_root = nil)
+ @hierarchy_root = hierarchy_root
+ @should_expand_hierarchy = true
+
+ self
+ end
+
+ def represent(resource, opts = {}, entity_class = nil)
+ if should_expand_hierarchy
+ paginator.paginate(resource) if paginated?
+ represent_hierarchies(resource, opts)
+ else
+ super(resource, opts)
+ end
+ end
+
+ protected
+
+ def represent_hierarchies(children, opts)
+ if children.is_a?(GroupDescendant)
+ represent_hierarchy(children.hierarchy(hierarchy_root), opts).first
+ else
+ hierarchies = GroupDescendant.build_hierarchy(children, hierarchy_root)
+ # When an array was passed, we always want to represent an array.
+ # Even if the hierarchy only contains one element
+ represent_hierarchy(Array.wrap(hierarchies), opts)
+ end
+ end
+
+ def represent_hierarchy(hierarchy, opts)
+ serializer = self.class.new(params)
+
+ if hierarchy.is_a?(Hash)
+ hierarchy.map do |parent, children|
+ serializer.represent(parent, opts)
+ .merge(children: Array.wrap(serializer.represent_hierarchy(children, opts)))
+ end
+ elsif hierarchy.is_a?(Array)
+ hierarchy.flat_map { |child| serializer.represent_hierarchy(child, opts) }
+ else
+ serializer.represent(hierarchy, opts)
+ end
+ end
+end
diff --git a/app/serializers/group_entity.rb b/app/serializers/group_entity.rb
index 7c872a3e986..6d8466da902 100644
--- a/app/serializers/group_entity.rb
+++ b/app/serializers/group_entity.rb
@@ -45,6 +45,6 @@ class GroupEntity < Grape::Entity
end
expose :avatar_url do |group|
- group_icon(group)
+ group_icon_url(group)
end
end
diff --git a/app/serializers/group_serializer.rb b/app/serializers/group_serializer.rb
index 26e8566828b..8cf7eb63bcf 100644
--- a/app/serializers/group_serializer.rb
+++ b/app/serializers/group_serializer.rb
@@ -1,19 +1,5 @@
class GroupSerializer < BaseSerializer
- entity GroupEntity
-
- def with_pagination(request, response)
- tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
- end
+ include WithPagination
- def paginated?
- @paginator.present?
- end
-
- def represent(resource, opts = {})
- if paginated?
- super(@paginator.paginate(resource), opts)
- else
- super(resource, opts)
- end
- end
+ entity GroupEntity
end
diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb
index 61c7a428745..3b5a4fd4f79 100644
--- a/app/serializers/issuable_entity.rb
+++ b/app/serializers/issuable_entity.rb
@@ -1,20 +1,16 @@
class IssuableEntity < Grape::Entity
+ include RequestAwareEntity
+
expose :id
expose :iid
expose :author_id
expose :description
expose :lock_version
expose :milestone_id
- expose :state
expose :title
expose :updated_by_id
expose :created_at
expose :updated_at
- expose :deleted_at
- expose :time_estimate
- 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/issuable_sidebar_entity.rb b/app/serializers/issuable_sidebar_entity.rb
new file mode 100644
index 00000000000..ff23d8bf0c7
--- /dev/null
+++ b/app/serializers/issuable_sidebar_entity.rb
@@ -0,0 +1,16 @@
+class IssuableSidebarEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :participants, using: ::API::Entities::UserBasic do |issuable|
+ issuable.participants(request.current_user)
+ end
+
+ expose :subscribed do |issuable|
+ issuable.subscribed?(request.current_user, issuable.project)
+ end
+
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
+end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 10d3ad0214b..9d52b8d9752 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -1,7 +1,8 @@
class IssueEntity < IssuableEntity
- include RequestAwareEntity
+ include TimeTrackableEntity
- expose :branch_name
+ expose :state
+ expose :deleted_at
expose :confidential
expose :discussion_locked
expose :assignees, using: API::Entities::UserBasic
diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb
index 4fff54a9126..2555595379b 100644
--- a/app/serializers/issue_serializer.rb
+++ b/app/serializers/issue_serializer.rb
@@ -1,3 +1,16 @@
class IssueSerializer < BaseSerializer
- entity IssueEntity
+ # This overrided method takes care of which entity should be used
+ # to serialize the `issue` based on `basic` key in `opts` param.
+ # Hence, `entity` doesn't need to be declared on the class scope.
+ def represent(merge_request, opts = {})
+ entity =
+ case opts[:serializer]
+ when 'sidebar'
+ IssueSidebarEntity
+ else
+ IssueEntity
+ end
+
+ super(merge_request, opts, entity)
+ end
end
diff --git a/app/serializers/issue_sidebar_entity.rb b/app/serializers/issue_sidebar_entity.rb
new file mode 100644
index 00000000000..6c823dbfe95
--- /dev/null
+++ b/app/serializers/issue_sidebar_entity.rb
@@ -0,0 +1,3 @@
+class IssueSidebarEntity < IssuableSidebarEntity
+ expose :assignees, using: API::Entities::UserBasic
+end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
index 8461f158bb5..d54a6516aed 100644
--- a/app/serializers/merge_request_basic_entity.rb
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -1,11 +1,7 @@
-class MergeRequestBasicEntity < Grape::Entity
+class MergeRequestBasicEntity < IssuableSidebarEntity
expose :assignee_id
expose :merge_status
expose :merge_error
expose :state
expose :source_branch_exists?, as: :source_branch_exists
- expose :time_estimate
- expose :total_time_spent
- expose :human_time_estimate
- expose :human_total_time_spent
end
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 297a459e394..b53a49fe59e 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -1,6 +1,8 @@
class MergeRequestEntity < IssuableEntity
- include RequestAwareEntity
+ include TimeTrackableEntity
+ expose :state
+ expose :deleted_at
expose :in_progress_merge_commit_sha
expose :merge_commit_sha
expose :merge_error
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index f67034ce47a..e9d98d8baca 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -3,7 +3,14 @@ class MergeRequestSerializer < BaseSerializer
# to serialize the `merge_request` based on `basic` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope.
def represent(merge_request, opts = {})
- entity = opts[:basic] ? MergeRequestBasicEntity : MergeRequestEntity
+ entity =
+ case opts[:serializer]
+ when 'basic', 'sidebar'
+ MergeRequestBasicEntity
+ else
+ MergeRequestEntity
+ end
+
super(merge_request, opts, entity)
end
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index 661bf17983c..7181f8a6b04 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -1,16 +1,10 @@
class PipelineSerializer < BaseSerializer
+ include WithPagination
+
InvalidResourceError = Class.new(StandardError)
entity PipelineDetailsEntity
- def with_pagination(request, response)
- tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
- end
-
- def paginated?
- @paginator.present?
- end
-
def represent(resource, opts = {})
if resource.is_a?(ActiveRecord::Relation)
diff --git a/app/serializers/submodule_entity.rb b/app/serializers/submodule_entity.rb
index 9a7eb5e7880..ed1f1ae0ef0 100644
--- a/app/serializers/submodule_entity.rb
+++ b/app/serializers/submodule_entity.rb
@@ -7,7 +7,7 @@ class SubmoduleEntity < Grape::Entity
'archive'
end
- expose :project_url do |blob|
+ expose :url do |blob|
submodule_links(blob, request).first
end
diff --git a/app/serializers/time_trackable_entity.rb b/app/serializers/time_trackable_entity.rb
new file mode 100644
index 00000000000..e81cd7bec72
--- /dev/null
+++ b/app/serializers/time_trackable_entity.rb
@@ -0,0 +1,11 @@
+module TimeTrackableEntity
+ extend ActiveSupport::Concern
+ extend Grape
+
+ included do
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
+ end
+end
diff --git a/app/serializers/tree_entity.rb b/app/serializers/tree_entity.rb
index 555e5cf83bd..9f1b485347f 100644
--- a/app/serializers/tree_entity.rb
+++ b/app/serializers/tree_entity.rb
@@ -3,10 +3,6 @@ class TreeEntity < Grape::Entity
expose :id, :path, :name, :mode
- expose :last_commit do |tree|
- request.project.repository.last_commit_for_path(tree.commit_id, tree.path)
- end
-
expose :icon do |tree|
IconsHelper.file_type_icon_class('folder', tree.mode, tree.name)
end
diff --git a/app/serializers/tree_root_entity.rb b/app/serializers/tree_root_entity.rb
index 69702ae1493..496f070ddbd 100644
--- a/app/serializers/tree_root_entity.rb
+++ b/app/serializers/tree_root_entity.rb
@@ -18,4 +18,8 @@ class TreeRootEntity < Grape::Entity
project_tree_path(request.project, File.join(request.ref, parent_tree_path))
end
+
+ expose :last_commit_path do |tree|
+ logs_file_project_ref_path(request.project, request.ref, tree.path)
+ end
end
diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb
index 9c00ea789ec..46e19230328 100644
--- a/app/services/access_token_validation_service.rb
+++ b/app/services/access_token_validation_service.rb
@@ -39,11 +39,8 @@ class AccessTokenValidationService
token_scopes = token.scopes.map(&:to_sym)
required_scopes.any? do |scope|
- if scope.respond_to?(:sufficient?)
- scope.sufficient?(token_scopes, request)
- else
- API::Scope.new(scope).sufficient?(token_scopes, request)
- end
+ scope = API::Scope.new(scope) unless scope.is_a?(API::Scope)
+ scope.sufficient?(token_scopes, request)
end
end
end
diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb
new file mode 100644
index 00000000000..35d45f25a71
--- /dev/null
+++ b/app/services/applications/create_service.rb
@@ -0,0 +1,13 @@
+module Applications
+ class CreateService
+ def initialize(current_user, params)
+ @current_user = current_user
+ @params = params
+ @ip_address = @params.delete(:ip_address)
+ end
+
+ def execute(request = nil)
+ Doorkeeper::Application.create(@params)
+ end
+ end
+end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 9a636346899..f40cd2b06c8 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -56,11 +56,22 @@ module Auth
def process_scope(scope)
type, name, actions = scope.split(':', 3)
actions = actions.split(',')
- path = ContainerRegistry::Path.new(name)
- return unless type == 'repository'
+ case type
+ when 'registry'
+ process_registry_access(type, name, actions)
+ when 'repository'
+ path = ContainerRegistry::Path.new(name)
+ process_repository_access(type, path, actions)
+ end
+ end
+
+ def process_registry_access(type, name, actions)
+ return unless current_user&.admin?
+ return unless name == 'catalog'
+ return unless actions == ['*']
- process_repository_access(type, path, actions)
+ { type: type, name: name, actions: ['*'] }
end
def process_repository_access(type, path, actions)
diff --git a/app/services/base_count_service.rb b/app/services/base_count_service.rb
new file mode 100644
index 00000000000..99cc9a196e6
--- /dev/null
+++ b/app/services/base_count_service.rb
@@ -0,0 +1,34 @@
+# Base class for services that count a single resource such as the number of
+# issues for a project.
+class BaseCountService
+ def relation_for_count
+ raise(
+ NotImplementedError,
+ '"relation_for_count" must be implemented and return an ActiveRecord::Relation'
+ )
+ end
+
+ def count
+ Rails.cache.fetch(cache_key, raw: raw?) { uncached_count }.to_i
+ end
+
+ def refresh_cache
+ Rails.cache.write(cache_key, uncached_count, raw: raw?)
+ end
+
+ def uncached_count
+ relation_for_count.count
+ end
+
+ def delete_cache
+ Rails.cache.delete(cache_key)
+ end
+
+ def raw?
+ false
+ end
+
+ def cache_key
+ raise NotImplementedError, 'cache_key must be implemented and return a String'
+ end
+end
diff --git a/app/services/base_renderer.rb b/app/services/base_renderer.rb
new file mode 100644
index 00000000000..d6e30bd7008
--- /dev/null
+++ b/app/services/base_renderer.rb
@@ -0,0 +1,7 @@
+class BaseRenderer
+ attr_reader :current_user
+
+ def initialize(current_user = nil)
+ @current_user = current_user
+ end
+end
diff --git a/app/services/ci/create_cluster_service.rb b/app/services/ci/create_cluster_service.rb
deleted file mode 100644
index f7ee0e468e2..00000000000
--- a/app/services/ci/create_cluster_service.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module Ci
- class CreateClusterService < BaseService
- def execute(access_token)
- params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE
-
- cluster_params =
- params.merge(user: current_user,
- gcp_token: access_token)
-
- project.create_cluster(cluster_params).tap do |cluster|
- ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
- end
- end
- end
-end
diff --git a/app/services/ci/ensure_stage_service.rb b/app/services/ci/ensure_stage_service.rb
new file mode 100644
index 00000000000..dc2f49e8db1
--- /dev/null
+++ b/app/services/ci/ensure_stage_service.rb
@@ -0,0 +1,39 @@
+module Ci
+ ##
+ # We call this service everytime we persist a CI/CD job.
+ #
+ # In most cases a job should already have a stage assigned, but in cases it
+ # doesn't have we need to either find existing one or create a brand new
+ # stage.
+ #
+ class EnsureStageService < BaseService
+ def execute(build)
+ @build = build
+
+ return if build.stage_id.present?
+ return if build.invalid?
+
+ ensure_stage.tap do |stage|
+ build.stage_id = stage.id
+
+ yield stage if block_given?
+ end
+ end
+
+ private
+
+ def ensure_stage
+ find_stage || create_stage
+ end
+
+ def find_stage
+ @build.pipeline.stages.find_by(name: @build.stage)
+ end
+
+ def create_stage
+ Ci::Stage.create!(name: @build.stage,
+ pipeline: @build.pipeline,
+ project: @build.project)
+ end
+ end
+end
diff --git a/app/services/ci/fetch_gcp_operation_service.rb b/app/services/ci/fetch_gcp_operation_service.rb
deleted file mode 100644
index 0b68e4d6ea9..00000000000
--- a/app/services/ci/fetch_gcp_operation_service.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module Ci
- class FetchGcpOperationService
- def execute(cluster)
- api_client =
- GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
-
- operation = api_client.projects_zones_operations(
- cluster.gcp_project_id,
- cluster.gcp_cluster_zone,
- cluster.gcp_operation_id)
-
- yield(operation) if block_given?
- rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
- return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
- end
- end
-end
diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb
index 44da87cb00c..e73c6ad6780 100644
--- a/app/services/ci/fetch_kubernetes_token_service.rb
+++ b/app/services/ci/fetch_kubernetes_token_service.rb
@@ -34,6 +34,7 @@ module Ci
kubeclient.get_secrets.as_json
rescue KubeException => err
raise err unless err.error_code == 404
+
[]
end
diff --git a/app/services/ci/finalize_cluster_creation_service.rb b/app/services/ci/finalize_cluster_creation_service.rb
deleted file mode 100644
index 347875c5697..00000000000
--- a/app/services/ci/finalize_cluster_creation_service.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module Ci
- class FinalizeClusterCreationService
- def execute(cluster)
- api_client =
- GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
-
- begin
- gke_cluster = api_client.projects_zones_clusters_get(
- cluster.gcp_project_id,
- cluster.gcp_cluster_zone,
- cluster.gcp_cluster_name)
- rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
- return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
- end
-
- endpoint = gke_cluster.endpoint
- api_url = 'https://' + endpoint
- ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate)
- username = gke_cluster.master_auth.username
- password = gke_cluster.master_auth.password
-
- kubernetes_token = Ci::FetchKubernetesTokenService.new(
- api_url, ca_cert, username, password).execute
-
- unless kubernetes_token
- return cluster.make_errored!('Failed to get a default token of kubernetes')
- end
-
- Ci::IntegrateClusterService.new.execute(
- cluster, endpoint, ca_cert, kubernetes_token, username, password)
- end
- end
-end
diff --git a/app/services/ci/integrate_cluster_service.rb b/app/services/ci/integrate_cluster_service.rb
deleted file mode 100644
index d123ce8d26b..00000000000
--- a/app/services/ci/integrate_cluster_service.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module Ci
- class IntegrateClusterService
- def execute(cluster, endpoint, ca_cert, token, username, password)
- Gcp::Cluster.transaction do
- cluster.update!(
- enabled: true,
- endpoint: endpoint,
- ca_cert: ca_cert,
- kubernetes_token: token,
- username: username,
- password: password,
- service: cluster.project.find_or_initialize_service('kubernetes'),
- status_event: :make_created)
-
- cluster.service.update!(
- active: true,
- api_url: cluster.api_url,
- ca_pem: ca_cert,
- namespace: cluster.project_namespace,
- token: token)
- end
- rescue ActiveRecord::RecordInvalid => e
- cluster.make_errored!("Failed to integrate cluster into kubernetes_service: #{e.message}")
- end
- end
-end
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index 120af8c1e61..a9813d774bb 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -1,5 +1,7 @@
module Ci
class PipelineTriggerService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
def execute
if trigger_from_token
create_pipeline_from_trigger(trigger_from_token)
@@ -26,9 +28,9 @@ module Ci
end
def trigger_from_token
- return @trigger if defined?(@trigger)
-
- @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ strong_memoize(:trigger) do
+ Ci::Trigger.find_by_token(params[:token].to_s)
+ end
end
def create_pipeline_variables!(pipeline)
diff --git a/app/services/ci/provision_cluster_service.rb b/app/services/ci/provision_cluster_service.rb
deleted file mode 100644
index 52d80b01813..00000000000
--- a/app/services/ci/provision_cluster_service.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module Ci
- class ProvisionClusterService
- def execute(cluster)
- api_client =
- GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
-
- begin
- operation = api_client.projects_zones_clusters_create(
- cluster.gcp_project_id,
- cluster.gcp_cluster_zone,
- cluster.gcp_cluster_name,
- cluster.gcp_cluster_size,
- machine_type: cluster.gcp_machine_type)
- rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
- return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
- end
-
- unless operation.status == 'RUNNING' || operation.status == 'PENDING'
- return cluster.make_errored!("Operation status is unexpected; #{operation.status_message}")
- end
-
- cluster.gcp_operation_id = api_client.parse_operation_id(operation.self_link)
-
- unless cluster.gcp_operation_id
- return cluster.make_errored!('Can not find operation_id from self_link')
- end
-
- if cluster.make_creating
- WaitForClusterCreationWorker.perform_in(
- WaitForClusterCreationWorker::INITIAL_INTERVAL, cluster.id)
- else
- return cluster.make_errored!("Failed to update cluster record; #{cluster.errors}")
- end
- end
- end
-end
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index d67b9f5cc56..c552193e66b 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -28,6 +28,8 @@ module Ci
attributes.push([:user, current_user])
+ build.retried = true
+
Ci::Build.transaction do
# mark all other builds of that name as retried
build.pipeline.builds.latest
diff --git a/app/services/ci/update_cluster_service.rb b/app/services/ci/update_cluster_service.rb
deleted file mode 100644
index 70d88fca660..00000000000
--- a/app/services/ci/update_cluster_service.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module Ci
- class UpdateClusterService < BaseService
- def execute(cluster)
- Gcp::Cluster.transaction do
- cluster.update!(params)
-
- if params['enabled'] == 'true'
- cluster.service.update!(
- active: true,
- api_url: cluster.api_url,
- ca_pem: cluster.ca_cert,
- namespace: cluster.project_namespace,
- token: cluster.kubernetes_token)
- else
- cluster.service.update!(active: false)
- end
- end
- rescue ActiveRecord::RecordInvalid => e
- cluster.errors.add(:base, e.message)
- end
- end
-end
diff --git a/app/services/clusters/applications/base_helm_service.rb b/app/services/clusters/applications/base_helm_service.rb
new file mode 100644
index 00000000000..9a4ce31cb39
--- /dev/null
+++ b/app/services/clusters/applications/base_helm_service.rb
@@ -0,0 +1,29 @@
+module Clusters
+ module Applications
+ class BaseHelmService
+ attr_accessor :app
+
+ def initialize(app)
+ @app = app
+ end
+
+ protected
+
+ def cluster
+ app.cluster
+ end
+
+ def kubeclient
+ cluster.kubeclient
+ end
+
+ def helm_api
+ @helm_api ||= Gitlab::Kubernetes::Helm.new(kubeclient)
+ end
+
+ def install_command
+ @install_command ||= app.install_command
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb
new file mode 100644
index 00000000000..bde090eaeec
--- /dev/null
+++ b/app/services/clusters/applications/check_installation_progress_service.rb
@@ -0,0 +1,65 @@
+module Clusters
+ module Applications
+ class CheckInstallationProgressService < BaseHelmService
+ def execute
+ return unless app.installing?
+
+ case installation_phase
+ when Gitlab::Kubernetes::Pod::SUCCEEDED
+ on_success
+ when Gitlab::Kubernetes::Pod::FAILED
+ on_failed
+ else
+ check_timeout
+ end
+ rescue KubeException => ke
+ app.make_errored!("Kubernetes error: #{ke.message}") unless app.errored?
+ end
+
+ private
+
+ def on_success
+ app.make_installed!
+ ensure
+ remove_installation_pod
+ end
+
+ def on_failed
+ app.make_errored!(installation_errors || 'Installation silently failed')
+ ensure
+ remove_installation_pod
+ end
+
+ def check_timeout
+ if timeouted?
+ begin
+ app.make_errored!('Installation timeouted')
+ ensure
+ remove_installation_pod
+ end
+ else
+ ClusterWaitForAppInstallationWorker.perform_in(
+ ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
+ end
+ end
+
+ def timeouted?
+ Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
+ end
+
+ def remove_installation_pod
+ helm_api.delete_installation_pod!(install_command.pod_name)
+ rescue
+ # no-op
+ end
+
+ def installation_phase
+ helm_api.installation_status(install_command.pod_name)
+ end
+
+ def installation_errors
+ helm_api.installation_log(install_command.pod_name)
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb
new file mode 100644
index 00000000000..8ceeec687cd
--- /dev/null
+++ b/app/services/clusters/applications/install_service.rb
@@ -0,0 +1,21 @@
+module Clusters
+ module Applications
+ class InstallService < BaseHelmService
+ def execute
+ return unless app.scheduled?
+
+ begin
+ app.make_installing!
+ helm_api.install(install_command)
+
+ ClusterWaitForAppInstallationWorker.perform_in(
+ ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
+ rescue KubeException => ke
+ app.make_errored!("Kubernetes error: #{ke.message}")
+ rescue StandardError
+ app.make_errored!("Can't start installation process")
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/schedule_installation_service.rb b/app/services/clusters/applications/schedule_installation_service.rb
new file mode 100644
index 00000000000..eb8caa68ef7
--- /dev/null
+++ b/app/services/clusters/applications/schedule_installation_service.rb
@@ -0,0 +1,22 @@
+module Clusters
+ module Applications
+ class ScheduleInstallationService < ::BaseService
+ def execute
+ application_class.find_or_create_by!(cluster: cluster).try do |application|
+ application.make_scheduled!
+ ClusterInstallAppWorker.perform_async(application.name, application.id)
+ end
+ end
+
+ private
+
+ def application_class
+ params[:application_class]
+ end
+
+ def cluster
+ params[:cluster]
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
new file mode 100644
index 00000000000..1d407739b21
--- /dev/null
+++ b/app/services/clusters/create_service.rb
@@ -0,0 +1,29 @@
+module Clusters
+ class CreateService < BaseService
+ attr_reader :access_token
+
+ def execute(access_token)
+ @access_token = access_token
+
+ create_cluster.tap do |cluster|
+ ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
+ end
+ end
+
+ private
+
+ def create_cluster
+ Clusters::Cluster.create(cluster_params)
+ end
+
+ def cluster_params
+ return @cluster_params if defined?(@cluster_params)
+
+ params[:provider_gcp_attributes].try do |provider|
+ provider[:access_token] = access_token
+ end
+
+ @cluster_params = params.merge(user: current_user, projects: [project])
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/fetch_operation_service.rb b/app/services/clusters/gcp/fetch_operation_service.rb
new file mode 100644
index 00000000000..a4cd3ca5c11
--- /dev/null
+++ b/app/services/clusters/gcp/fetch_operation_service.rb
@@ -0,0 +1,16 @@
+module Clusters
+ module Gcp
+ class FetchOperationService
+ def execute(provider)
+ operation = provider.api_client.projects_zones_operations(
+ provider.gcp_project_id,
+ provider.zone,
+ provider.operation_id)
+
+ yield(operation) if block_given?
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
new file mode 100644
index 00000000000..cea56f4e849
--- /dev/null
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -0,0 +1,56 @@
+module Clusters
+ module Gcp
+ class FinalizeCreationService
+ attr_reader :provider
+
+ def execute(provider)
+ @provider = provider
+
+ configure_provider
+ configure_kubernetes
+
+ cluster.save!
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ rescue ActiveRecord::RecordInvalid => e
+ provider.make_errored!("Failed to configure GKE Cluster: #{e.message}")
+ end
+
+ private
+
+ def configure_provider
+ provider.endpoint = gke_cluster.endpoint
+ provider.status_event = :make_created
+ end
+
+ def configure_kubernetes
+ cluster.platform_type = :kubernetes
+ cluster.build_platform_kubernetes(
+ api_url: 'https://' + gke_cluster.endpoint,
+ ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
+ username: gke_cluster.master_auth.username,
+ password: gke_cluster.master_auth.password,
+ token: request_kuberenetes_token)
+ end
+
+ def request_kuberenetes_token
+ Ci::FetchKubernetesTokenService.new(
+ 'https://' + gke_cluster.endpoint,
+ Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
+ gke_cluster.master_auth.username,
+ gke_cluster.master_auth.password).execute
+ end
+
+ def gke_cluster
+ @gke_cluster ||= provider.api_client.projects_zones_clusters_get(
+ provider.gcp_project_id,
+ provider.zone,
+ cluster.name)
+ end
+
+ def cluster
+ @cluster ||= provider.cluster
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb
new file mode 100644
index 00000000000..8beea5a8cfb
--- /dev/null
+++ b/app/services/clusters/gcp/provision_service.rb
@@ -0,0 +1,47 @@
+module Clusters
+ module Gcp
+ class ProvisionService
+ attr_reader :provider
+
+ def execute(provider)
+ @provider = provider
+
+ get_operation_id do |operation_id|
+ if provider.make_creating(operation_id)
+ WaitForClusterCreationWorker.perform_in(
+ Clusters::Gcp::VerifyProvisionStatusService::INITIAL_INTERVAL,
+ provider.cluster_id)
+ else
+ provider.make_errored!("Failed to update provider record; #{provider.errors}")
+ end
+ end
+ end
+
+ private
+
+ def get_operation_id
+ operation = provider.api_client.projects_zones_clusters_create(
+ provider.gcp_project_id,
+ provider.zone,
+ provider.cluster.name,
+ provider.num_nodes,
+ machine_type: provider.machine_type)
+
+ unless operation.status == 'PENDING' || operation.status == 'RUNNING'
+ return provider.make_errored!("Operation status is unexpected; #{operation.status_message}")
+ end
+
+ operation_id = provider.api_client.parse_operation_id(operation.self_link)
+
+ unless operation_id
+ return provider.make_errored!('Can not find operation_id from self_link')
+ end
+
+ yield(operation_id)
+
+ rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
+ provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb
new file mode 100644
index 00000000000..bc33756f27c
--- /dev/null
+++ b/app/services/clusters/gcp/verify_provision_status_service.rb
@@ -0,0 +1,48 @@
+module Clusters
+ module Gcp
+ class VerifyProvisionStatusService
+ attr_reader :provider
+
+ INITIAL_INTERVAL = 2.minutes
+ EAGER_INTERVAL = 10.seconds
+ TIMEOUT = 20.minutes
+
+ def execute(provider)
+ @provider = provider
+
+ request_operation do |operation|
+ case operation.status
+ when 'PENDING', 'RUNNING'
+ continue_creation(operation)
+ when 'DONE'
+ finalize_creation
+ else
+ return provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
+ end
+ end
+ end
+
+ private
+
+ def continue_creation(operation)
+ if elapsed_time_from_creation(operation) < TIMEOUT
+ WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id)
+ else
+ provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}")
+ end
+ end
+
+ def elapsed_time_from_creation(operation)
+ Time.now.utc - operation.start_time.to_time.utc
+ end
+
+ def finalize_creation
+ Clusters::Gcp::FinalizeCreationService.new.execute(provider)
+ end
+
+ def request_operation(&blk)
+ Clusters::Gcp::FetchOperationService.new.execute(provider, &blk)
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/update_service.rb b/app/services/clusters/update_service.rb
new file mode 100644
index 00000000000..989218e32a2
--- /dev/null
+++ b/app/services/clusters/update_service.rb
@@ -0,0 +1,7 @@
+module Clusters
+ class UpdateService < BaseService
+ def execute(cluster)
+ cluster.update(params)
+ end
+ end
+end
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index 53f16a236d2..1db91c3c90c 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -1,17 +1,17 @@
require 'securerandom'
-# Compare 2 branches for one repo or between repositories
+# Compare 2 refs for one repo or between repositories
# and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService
- attr_reader :start_project, :start_branch_name
+ attr_reader :start_project, :start_ref_name
- def initialize(new_start_project, new_start_branch_name)
+ def initialize(new_start_project, new_start_ref_name)
@start_project = new_start_project
- @start_branch_name = new_start_branch_name
+ @start_ref_name = new_start_ref_name
end
- def execute(target_project, target_branch, straight: false)
- raw_compare = target_project.repository.compare_source_branch(target_branch, start_project.repository, start_branch_name, straight: straight)
+ def execute(target_project, target_ref, straight: false)
+ raw_compare = target_project.repository.compare_source_branch(target_ref, start_project.repository, start_ref_name, straight: straight)
Compare.new(raw_compare, target_project, straight: straight) if raw_compare
end
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
index 077268b2388..cb235a85daf 100644
--- a/app/services/delete_merged_branches_service.rb
+++ b/app/services/delete_merged_branches_service.rb
@@ -13,7 +13,7 @@ class DeleteMergedBranchesService < BaseService
# Prevent deletion of branches relevant to open merge requests
branches -= merge_request_branch_names
# Prevent deletion of protected branches
- branches = branches.reject { |branch| project.protected_for?(branch) }
+ branches = branches.reject { |branch| ProtectedBranch.protected?(project, branch) }
branches.each do |branch|
DeleteBranchService.new(project, current_user).execute(branch)
diff --git a/app/services/events/render_service.rb b/app/services/events/render_service.rb
new file mode 100644
index 00000000000..0b62d8aedf1
--- /dev/null
+++ b/app/services/events/render_service.rb
@@ -0,0 +1,21 @@
+module Events
+ class RenderService < BaseRenderer
+ def execute(events, atom_request: false)
+ events.map(&:note).compact.group_by(&:project).each do |project, notes|
+ render_notes(notes, project, atom_request)
+ end
+ end
+
+ private
+
+ def render_notes(notes, project, atom_request)
+ Notes::RenderService.new(current_user).execute(notes, project, render_options(atom_request))
+ end
+
+ def render_options(atom_request)
+ return {} unless atom_request
+
+ { only_path: false, xhtml: true }
+ end
+ end
+end
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
new file mode 100644
index 00000000000..3da21bd8b8f
--- /dev/null
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -0,0 +1,93 @@
+module Issuable
+ class CommonSystemNotesService < ::BaseService
+ attr_reader :issuable
+
+ def execute(issuable, old_labels)
+ @issuable = issuable
+
+ if issuable.previous_changes.include?('title')
+ create_title_change_note(issuable.previous_changes['title'].first)
+ end
+
+ handle_description_change_note
+
+ handle_time_tracking_note if issuable.is_a?(TimeTrackable)
+ create_labels_note(old_labels) if issuable.labels != old_labels
+ create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked')
+ create_milestone_note if issuable.previous_changes.include?('milestone_id')
+ end
+
+ private
+
+ def handle_time_tracking_note
+ if issuable.previous_changes.include?('time_estimate')
+ create_time_estimate_note
+ end
+
+ if issuable.time_spent?
+ create_time_spent_note
+ end
+ end
+
+ def handle_description_change_note
+ if issuable.previous_changes.include?('description')
+ if issuable.tasks? && issuable.updated_tasks.any?
+ create_task_status_note
+ else
+ # TODO: Show this note if non-task content was modified.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
+ create_description_change_note
+ end
+ end
+ end
+
+ def create_wip_note(old_title)
+ return unless issuable.is_a?(MergeRequest)
+
+ if MergeRequest.work_in_progress?(old_title) != issuable.work_in_progress?
+ SystemNoteService.handle_merge_request_wip(issuable, issuable.project, current_user)
+ end
+ end
+
+ def create_labels_note(old_labels)
+ added_labels = issuable.labels - old_labels
+ removed_labels = old_labels - issuable.labels
+
+ SystemNoteService.change_label(issuable, issuable.project, current_user, added_labels, removed_labels)
+ end
+
+ def create_title_change_note(old_title)
+ create_wip_note(old_title)
+
+ if issuable.wipless_title_changed(old_title)
+ SystemNoteService.change_title(issuable, issuable.project, current_user, old_title)
+ end
+ end
+
+ def create_description_change_note
+ SystemNoteService.change_description(issuable, issuable.project, current_user)
+ end
+
+ def create_task_status_note
+ issuable.updated_tasks.each do |task|
+ SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
+ end
+ end
+
+ def create_time_estimate_note
+ SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
+ end
+
+ def create_time_spent_note
+ SystemNoteService.change_time_spent(issuable, issuable.project, issuable.time_spent_user)
+ end
+
+ def create_milestone_note
+ SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone)
+ end
+
+ def create_discussion_lock_note
+ SystemNoteService.discussion_lock(issuable, current_user)
+ end
+ end
+end
diff --git a/app/services/issuable/destroy_service.rb b/app/services/issuable/destroy_service.rb
new file mode 100644
index 00000000000..0610b401213
--- /dev/null
+++ b/app/services/issuable/destroy_service.rb
@@ -0,0 +1,9 @@
+module Issuable
+ class DestroyService < IssuableBaseService
+ def execute(issuable)
+ if issuable.destroy
+ issuable.update_project_counter_caches
+ end
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index f83ece7098f..2c51ac13815 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -1,56 +1,10 @@
class IssuableBaseService < BaseService
private
- def create_milestone_note(issuable)
- SystemNoteService.change_milestone(
- issuable, issuable.project, current_user, issuable.milestone)
- end
-
- def create_labels_note(issuable, old_labels)
- added_labels = issuable.labels - old_labels
- removed_labels = old_labels - issuable.labels
-
- SystemNoteService.change_label(
- issuable, issuable.project, current_user, added_labels, removed_labels)
- end
-
- def create_title_change_note(issuable, old_title)
- SystemNoteService.change_title(
- issuable, issuable.project, current_user, old_title)
- end
-
- def create_description_change_note(issuable)
- SystemNoteService.change_description(issuable, issuable.project, current_user)
- end
-
- def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
- SystemNoteService.change_branch(
- issuable, issuable.project, current_user, branch_type,
- old_branch, new_branch)
- end
-
- def create_task_status_note(issuable)
- issuable.updated_tasks.each do |task|
- SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
- end
- end
-
- def create_time_estimate_note(issuable)
- SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
- end
-
- def create_time_spent_note(issuable)
- SystemNoteService.change_time_spent(issuable, issuable.project, current_user)
- end
-
- def create_discussion_lock_note(issuable)
- SystemNoteService.discussion_lock(issuable, current_user)
- end
-
def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}"
- unless can?(current_user, ability_name, project)
+ unless can?(current_user, ability_name, issuable)
params.delete(:milestone_id)
params.delete(:labels)
params.delete(:add_label_ids)
@@ -215,9 +169,7 @@ class IssuableBaseService < BaseService
change_todo(issuable)
toggle_award(issuable)
filter_params(issuable)
- old_labels = issuable.labels.to_a
- old_mentioned_users = issuable.mentioned_users.to_a
- old_assignees = issuable.assignees.to_a
+ old_associations = associations_before_update(issuable)
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
@@ -233,29 +185,27 @@ class IssuableBaseService < BaseService
# We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save.
- update_project_counters = issuable.update_project_counter_caches?
+ update_project_counters = issuable.project && update_project_counter_caches?(issuable)
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
- handle_common_system_notes(issuable, old_labels: old_labels)
+ Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_associations[:labels])
end
- change_discussion_lock(issuable)
- handle_changes(
- issuable,
- old_labels: old_labels,
- old_mentioned_users: old_mentioned_users,
- old_assignees: old_assignees
- )
+ handle_changes(issuable, old_associations: old_associations)
new_assignees = issuable.assignees.to_a
- affected_assignees = (old_assignees + new_assignees) - (old_assignees & new_assignees)
+ affected_assignees = (old_associations[:assignees] + new_assignees) - (old_associations[:assignees] & new_assignees)
invalidate_cache_counts(issuable, users: affected_assignees.compact)
after_update(issuable)
issuable.create_new_cross_references!(current_user)
- execute_hooks(issuable, 'update')
+ execute_hooks(
+ issuable,
+ 'update',
+ old_associations: old_associations
+ )
issuable.update_project_counter_caches if update_project_counters
end
@@ -300,12 +250,6 @@ class IssuableBaseService < BaseService
end
end
- def change_discussion_lock(issuable)
- if issuable.previous_changes.include?('discussion_locked')
- create_discussion_lock_note(issuable)
- end
- end
-
def toggle_award(issuable)
award = params.delete(:emoji_award)
if award
@@ -314,6 +258,18 @@ class IssuableBaseService < BaseService
end
end
+ def associations_before_update(issuable)
+ associations =
+ {
+ labels: issuable.labels.to_a,
+ mentioned_users: issuable.mentioned_users.to_a,
+ assignees: issuable.assignees.to_a
+ }
+ associations[:total_time_spent] = issuable.total_time_spent if issuable.respond_to?(:total_time_spent)
+
+ associations
+ end
+
def has_changes?(issuable, old_labels: [], old_assignees: [])
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
@@ -328,35 +284,21 @@ class IssuableBaseService < BaseService
attrs_changed || labels_changed || assignees_changed
end
- def handle_common_system_notes(issuable, old_labels: [])
- if issuable.previous_changes.include?('title')
- create_title_change_note(issuable, issuable.previous_changes['title'].first)
- end
-
- if issuable.previous_changes.include?('description')
- if issuable.tasks? && issuable.updated_tasks.any?
- create_task_status_note(issuable)
- else
- # TODO: Show this note if non-task content was modified.
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
- create_description_change_note(issuable)
- end
- end
-
- if issuable.previous_changes.include?('time_estimate')
- create_time_estimate_note(issuable)
+ 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
+ end
- if issuable.time_spent?
- create_time_spent_note(issuable)
- end
+ # override if needed
+ def handle_changes(issuable, options)
+ end
- create_labels_note(issuable, old_labels) if issuable.labels != old_labels
+ # override if needed
+ def execute_hooks(issuable, action = 'open', params = {})
end
- 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
+ def update_project_counter_caches?(issuable)
+ issuable.state_changed?
end
end
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 4c198fc96ea..9f6cfc0f6d3 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -1,10 +1,10 @@
module Issues
class BaseService < ::IssuableBaseService
- def hook_data(issue, action)
- issue_data = issue.to_hook_data(current_user)
- issue_url = Gitlab::UrlBuilder.build(issue)
- issue_data[:object_attributes].merge!(url: issue_url, action: action)
- issue_data
+ def hook_data(issue, action, old_associations: {})
+ hook_data = issue.to_hook_data(current_user, old_associations: old_associations)
+ hook_data[:object_attributes][:action] = action
+
+ hook_data
end
def reopen_service
@@ -22,8 +22,8 @@ module Issues
issue, issue.project, current_user, old_assignees)
end
- def execute_hooks(issue, action = 'open')
- issue_data = hook_data(issue, action)
+ def execute_hooks(issue, action = 'open', old_associations: {})
+ issue_data = hook_data(issue, action, old_associations: old_associations)
hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
issue.project.execute_hooks(issue_data, hooks_scope)
issue.project.execute_services(issue_data, hooks_scope)
@@ -45,5 +45,9 @@ module Issues
params.delete(:assignee_ids)
end
end
+
+ def update_project_counter_caches?(issue)
+ super || issue.confidential_changed?
+ end
end
end
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index 35de4337b15..62b4b4b6a1e 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -9,6 +9,7 @@ module Issues
notification_service.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue, users: issue.assignees)
+ issue.update_project_counter_caches
end
issue
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index b4ca3966505..d7aa7e2347e 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -14,9 +14,10 @@ module Issues
end
def handle_changes(issue, options)
- old_labels = options[:old_labels] || []
- old_mentioned_users = options[:old_mentioned_users] || []
- old_assignees = options[:old_assignees] || []
+ old_associations = options.fetch(:old_associations, {})
+ old_labels = old_associations.fetch(:labels, [])
+ old_mentioned_users = old_associations.fetch(:mentioned_users, [])
+ old_assignees = old_associations.fetch(:assignees, [])
if has_changes?(issue, old_labels: old_labels, old_assignees: old_assignees)
todo_service.mark_pending_todos_as_done(issue, current_user)
@@ -27,14 +28,10 @@ module Issues
todo_service.update_issue(issue, current_user, old_mentioned_users)
end
- if issue.previous_changes.include?('milestone_id')
- create_milestone_note(issue)
- end
-
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
notification_service.reassigned_issue(issue, current_user, old_assignees)
- todo_service.reassigned_issue(issue, current_user)
+ todo_service.reassigned_issue(issue, current_user, old_assignees)
end
if issue.previous_changes.include?('confidential')
diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb
index 545832d0bd4..f78791932a7 100644
--- a/app/services/keys/base_service.rb
+++ b/app/services/keys/base_service.rb
@@ -4,6 +4,7 @@ module Keys
def initialize(user, params)
@user, @params = user, params
+ @ip_address = @params.delete(:ip_address)
end
def notification_service
diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb
index 43b539ded53..997d247be46 100644
--- a/app/services/labels/promote_service.rb
+++ b/app/services/labels/promote_service.rb
@@ -19,6 +19,7 @@ module Labels
# We skipped validations during creation. Let's run them now, after deleting conflicting labels
raise ActiveRecord::RecordInvalid.new(new_label) unless new_label.valid?
+
new_label
end
end
diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb
index 727768b1a39..6805b2f7d1c 100644
--- a/app/services/merge_requests/add_todo_when_build_fails_service.rb
+++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb
@@ -3,7 +3,7 @@ module MergeRequests
# Adds a todo to the parent merge_request when a CI build fails
#
def execute(commit_status)
- return if commit_status.allow_failure?
+ return if commit_status.allow_failure? || commit_status.retried?
commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_failed(merge_request)
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 35ccff26262..6b32d65a74b 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -4,33 +4,19 @@ module MergeRequests
SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, state, nil)
end
- def create_title_change_note(issuable, old_title)
- removed_wip = MergeRequest.work_in_progress?(old_title) && !issuable.work_in_progress?
- added_wip = !MergeRequest.work_in_progress?(old_title) && issuable.work_in_progress?
- changed_title = MergeRequest.wipless_title(old_title) != issuable.wipless_title
-
- if removed_wip
- SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user)
- elsif added_wip
- SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user)
- end
-
- super if changed_title
- end
-
- def hook_data(merge_request, action, oldrev = nil)
- hook_data = merge_request.to_hook_data(current_user)
- hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request)
+ def hook_data(merge_request, action, old_rev: nil, old_associations: {})
+ hook_data = merge_request.to_hook_data(current_user, old_associations: old_associations)
hook_data[:object_attributes][:action] = action
- if oldrev && !Gitlab::Git.blank_ref?(oldrev)
- hook_data[:object_attributes][:oldrev] = oldrev
+ if old_rev && !Gitlab::Git.blank_ref?(old_rev)
+ hook_data[:object_attributes][:oldrev] = old_rev
end
+
hook_data
end
- def execute_hooks(merge_request, action = 'open', oldrev = nil)
+ def execute_hooks(merge_request, action = 'open', old_rev: nil, old_associations: {})
if merge_request.project
- merge_data = hook_data(merge_request, action, oldrev)
+ merge_data = hook_data(merge_request, action, old_rev: old_rev, old_associations: old_associations)
merge_request.project.execute_hooks(merge_data, :merge_request_hooks)
merge_request.project.execute_services(merge_data, :merge_request_hooks)
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index bc0e7ad4e39..c2fb01466df 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -1,6 +1,8 @@
module MergeRequests
class BuildService < MergeRequests::BaseService
def execute
+ @issue_iid = params.delete(:issue_iid)
+
self.merge_request = MergeRequest.new(params)
merge_request.compare_commits = []
merge_request.source_project = find_source_project
@@ -18,7 +20,17 @@ module MergeRequests
attr_accessor :merge_request
- delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request
+ delegate :target_branch,
+ :target_branch_ref,
+ :target_project,
+ :source_branch,
+ :source_branch_ref,
+ :source_project,
+ :compare_commits,
+ :wip_title,
+ :description,
+ :errors,
+ to: :merge_request
def find_source_project
return source_project if source_project.present? && can?(current_user, :read_project, source_project)
@@ -28,6 +40,7 @@ module MergeRequests
def find_target_project
return target_project if target_project.present? && can?(current_user, :read_project, target_project)
+
project.default_merge_request_target
end
@@ -53,10 +66,10 @@ module MergeRequests
def compare_branches
compare = CompareService.new(
source_project,
- source_branch
+ source_branch_ref
).execute(
target_project,
- target_branch
+ target_branch_ref
)
if compare
@@ -105,37 +118,53 @@ module MergeRequests
# more than one commit in the MR
#
def assign_title_and_description
- if match = source_branch.match(/\A(\d+)-/)
- iid = match[1]
- end
+ assign_title_and_description_from_single_commit
+ assign_title_from_issue
- commits = compare_commits
- if commits && commits.count == 1
- commit = commits.first
- merge_request.title = commit.title
- merge_request.description ||= commit.description.try(:strip)
- elsif iid && issue = target_project.get_issue(iid, current_user)
- case issue
- when Issue
- merge_request.title = "Resolve \"#{issue.title}\""
- when ExternalIssue
- merge_request.title = "Resolve #{issue.title}"
- end
+ merge_request.title ||= source_branch.titleize.humanize
+ merge_request.title = wip_title if compare_commits.empty?
+
+ append_closes_description
+ end
+
+ def append_closes_description
+ return unless issue_iid
+
+ closes_issue = "Closes ##{issue_iid}"
+
+ if description.present?
+ merge_request.description += closes_issue.prepend("\n\n")
else
- merge_request.title = source_branch.titleize.humanize
+ merge_request.description = closes_issue
end
+ end
+
+ def assign_title_and_description_from_single_commit
+ commits = compare_commits
+
+ return unless commits&.count == 1
+
+ commit = commits.first
+ merge_request.title ||= commit.title
+ merge_request.description ||= commit.description.try(:strip)
+ end
- if iid
- closes_issue = "Closes ##{iid}"
+ def assign_title_from_issue
+ return unless issue
- if description.present?
- merge_request.description += closes_issue.prepend("\n\n")
- else
- merge_request.description = closes_issue
+ merge_request.title =
+ case issue
+ when Issue then "Resolve \"#{issue.title}\""
+ when ExternalIssue then "Resolve #{issue.title}"
end
- end
+ end
+
+ def issue_iid
+ @issue_iid ||= source_branch.match(/\A(\d+)-/).try(:[], 1)
+ end
- merge_request.title = wip_title if commits.empty?
+ def issue
+ @issue ||= target_project.get_issue(issue_iid, current_user)
end
end
end
diff --git a/app/services/merge_requests/conflicts/list_service.rb b/app/services/merge_requests/conflicts/list_service.rb
index 9835606812c..0f677a996f7 100644
--- a/app/services/merge_requests/conflicts/list_service.rb
+++ b/app/services/merge_requests/conflicts/list_service.rb
@@ -23,13 +23,13 @@ module MergeRequests
# when there are no conflict files.
conflicts.files.each(&:lines)
@conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
- rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing
+ rescue Rugged::OdbError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
@conflicts_can_be_resolved_in_ui = false
end
end
def conflicts
- @conflicts ||= Gitlab::Conflict::FileCollection.read_only(merge_request)
+ @conflicts ||= Gitlab::Conflict::FileCollection.new(merge_request)
end
end
end
diff --git a/app/services/merge_requests/conflicts/resolve_service.rb b/app/services/merge_requests/conflicts/resolve_service.rb
index 6b6e231f4f9..27cafd2d7d9 100644
--- a/app/services/merge_requests/conflicts/resolve_service.rb
+++ b/app/services/merge_requests/conflicts/resolve_service.rb
@@ -1,54 +1,10 @@
module MergeRequests
module Conflicts
class ResolveService < MergeRequests::Conflicts::BaseService
- MissingFiles = Class.new(Gitlab::Conflict::ResolutionError)
-
def execute(current_user, params)
- rugged = merge_request.source_project.repository.rugged
-
- Gitlab::Conflict::FileCollection.for_resolution(merge_request) do |conflicts_for_resolution|
- merge_index = conflicts_for_resolution.merge_index
-
- params[:files].each do |file_params|
- conflict_file = conflicts_for_resolution.file_for_path(file_params[:old_path], file_params[:new_path])
-
- write_resolved_file_to_index(merge_index, rugged, conflict_file, file_params)
- end
-
- unless merge_index.conflicts.empty?
- missing_files = merge_index.conflicts.map { |file| file[:ours][:path] }
-
- raise MissingFiles, "Missing resolutions for the following files: #{missing_files.join(', ')}"
- end
-
- commit_params = {
- message: params[:commit_message] || conflicts_for_resolution.default_commit_message,
- parents: [conflicts_for_resolution.our_commit, conflicts_for_resolution.their_commit].map(&:oid),
- tree: merge_index.write_tree(rugged)
- }
-
- conflicts_for_resolution
- .project
- .repository
- .resolve_conflicts(current_user, merge_request.source_branch, commit_params)
- end
- end
-
- private
-
- def write_resolved_file_to_index(merge_index, rugged, file, params)
- if params[:sections]
- new_file = file.resolve_lines(params[:sections]).map(&:text).join("\n")
-
- new_file << "\n" if file.our_blob.data.ends_with?("\n")
- elsif params[:content]
- new_file = file.resolve_content(params[:content])
- end
-
- our_path = file.our_path
+ conflicts = Gitlab::Conflict::FileCollection.new(merge_request)
- merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
- merge_index.conflict_remove(our_path)
+ conflicts.resolve(current_user, params[:commit_message], params[:files])
end
end
end
diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb
index da39a380451..89dab1dd028 100644
--- a/app/services/merge_requests/create_from_issue_service.rb
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -1,7 +1,18 @@
module MergeRequests
class CreateFromIssueService < MergeRequests::CreateService
+ def initialize(project, user, params)
+ # branch - the name of new branch
+ # ref - the source of new branch.
+
+ @branch_name = params[:branch_name]
+ @issue_iid = params[:issue_iid]
+ @ref = params[:ref]
+
+ super(project, user)
+ end
+
def execute
- return error('Invalid issue iid') unless issue_iid.present? && issue.present?
+ return error('Invalid issue iid') unless @issue_iid.present? && issue.present?
params[:label_ids] = issue.label_ids if issue.label_ids.any?
@@ -21,20 +32,16 @@ module MergeRequests
private
- def issue_iid
- @isssue_iid ||= params.delete(:issue_iid)
- end
-
def issue
- @issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: issue_iid)
+ @issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: @issue_iid)
end
def branch_name
- @branch_name ||= issue.to_branch_name
+ @branch ||= @branch_name || issue.to_branch_name
end
def ref
- project.default_branch || 'master'
+ @ref || project.default_branch || 'master'
end
def merge_request
@@ -43,6 +50,7 @@ module MergeRequests
def merge_request_params
{
+ issue_iid: @issue_iid,
source_project_id: project.id,
source_branch: branch_name,
target_project_id: project.id,
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index a110abf8256..cedfcb50e09 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -10,6 +10,8 @@ module MergeRequests
attr_reader :merge_request, :source
+ delegate :merge_jid, :state, to: :@merge_request
+
def execute(merge_request)
if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
FfMergeService.new(project, current_user, params).execute(merge_request)
@@ -18,15 +20,7 @@ module MergeRequests
@merge_request = merge_request
- unless @merge_request.mergeable?
- return handle_merge_error(log_message: 'Merge request is not mergeable', save_message_on_model: true)
- end
-
- @source = find_merge_source
-
- unless @source
- return handle_merge_error(log_message: 'No source for merge', save_message_on_model: true)
- end
+ error_check!
merge_request.in_locked_state do
if commit
@@ -35,16 +29,32 @@ module MergeRequests
success
end
end
+ log_info("Merge process finished on JID #{merge_jid} with state #{state}")
rescue MergeError => e
handle_merge_error(log_message: e.message, save_message_on_model: true)
end
private
+ def error_check!
+ error =
+ if @merge_request.should_be_rebased?
+ 'Only fast-forward merge is allowed for your project. Please update your source branch'
+ elsif !@merge_request.mergeable?
+ 'Merge request is not mergeable'
+ elsif !source
+ 'No source for merge'
+ end
+
+ raise MergeError, error if error
+ end
+
def commit
message = params[:commit_message] || merge_request.merge_commit_message
+ log_info("Git merge started on JID #{merge_jid}")
commit_id = repository.merge(current_user, source, merge_request, message)
+ log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
raise MergeError, 'Conflicts detected during merge' unless commit_id
@@ -58,15 +68,13 @@ module MergeRequests
end
def after_merge
+ log_info("Post merge started on JID #{merge_jid} with state #{state}")
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
+ log_info("Post merge finished on JID #{merge_jid} with state #{state}")
- if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch?
- # Verify again that the source branch can be removed, since branch may be protected,
- # or the source branch may have been updated.
- if @merge_request.can_remove_source_branch?(branch_deletion_user)
- DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
- .execute(merge_request.source_branch)
- end
+ if delete_source_branch?
+ DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
+ .execute(merge_request.source_branch)
end
end
@@ -78,24 +86,30 @@ module MergeRequests
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
- # Logs merge error message and cleans `MergeRequest#merge_jid`.
+ # Verify again that the source branch can be removed, since branch may be protected,
+ # or the source branch may have been updated, or the user may not have permission
#
+ def delete_source_branch?
+ params.fetch('should_remove_source_branch', @merge_request.force_remove_source_branch?) &&
+ @merge_request.can_remove_source_branch?(branch_deletion_user)
+ end
+
def handle_merge_error(log_message:, save_message_on_model: false)
Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}")
+ @merge_request.update(merge_error: log_message) if save_message_on_model
+ end
- if save_message_on_model
- @merge_request.update(merge_error: log_message, merge_jid: nil)
- else
- clean_merge_jid
- end
+ def log_info(message)
+ @logger ||= Rails.logger
+ @logger.info("#{merge_request_info} - #{message}")
end
def merge_request_info
merge_request.to_reference(full: true)
end
- def find_merge_source
- merge_request.diff_head_sha
+ def source
+ @source ||= @merge_request.diff_head_sha
end
end
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index bc4a13cf4bc..bf3d4855122 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -35,7 +35,7 @@ module MergeRequests
# target branch manually
def close_merge_requests
commit_ids = @commits.map(&:id)
- merge_requests = @project.merge_requests.preload(:merge_request_diff).opened.where(target_branch: @branch_name).to_a
+ merge_requests = @project.merge_requests.preload(:latest_merge_request_diff).opened.where(target_branch: @branch_name).to_a
merge_requests = merge_requests.select(&:diff_head_commit)
merge_requests = merge_requests.select do |merge_request|
@@ -166,7 +166,7 @@ module MergeRequests
# Call merge request webhook with update branches
def execute_mr_web_hooks
merge_requests_for_source_branch.each do |merge_request|
- execute_hooks(merge_request, 'update', @oldrev)
+ execute_hooks(merge_request, 'update', old_rev: @oldrev)
end
end
diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb
index b9c65be36ec..c599a90f9fe 100644
--- a/app/services/merge_requests/reopen_service.rb
+++ b/app/services/merge_requests/reopen_service.rb
@@ -11,6 +11,7 @@ module MergeRequests
merge_request.reload_diff(current_user)
merge_request.mark_as_unchecked
invalidate_cache_counts(merge_request, users: merge_request.assignees)
+ merge_request.update_project_counter_caches
end
merge_request
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 2832d893e95..c153872c874 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -22,8 +22,9 @@ module MergeRequests
end
def handle_changes(merge_request, options)
- old_labels = options[:old_labels] || []
- old_mentioned_users = options[:old_mentioned_users] || []
+ old_associations = options.fetch(:old_associations, {})
+ old_labels = old_associations.fetch(:labels, [])
+ old_mentioned_users = old_associations.fetch(:mentioned_users, [])
if has_changes?(merge_request, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
@@ -40,10 +41,6 @@ module MergeRequests
merge_request.target_branch)
end
- if merge_request.previous_changes.include?('milestone_id')
- create_milestone_note(merge_request)
- end
-
if merge_request.previous_changes.include?('assignee_id')
create_assignee_note(merge_request)
notification_service.reassigned_merge_request(merge_request, current_user)
@@ -111,5 +108,11 @@ module MergeRequests
end
end
end
+
+ def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
+ SystemNoteService.change_branch(
+ issuable, issuable.project, current_user, branch_type,
+ old_branch, new_branch)
+ end
end
end
diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb
index a02eee4961b..6b3939aeba5 100644
--- a/app/services/metrics_service.rb
+++ b/app/services/metrics_service.rb
@@ -6,8 +6,7 @@ class MetricsService
Gitlab::HealthChecks::Redis::RedisCheck,
Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck,
- Gitlab::HealthChecks::Redis::SharedStateCheck,
- Gitlab::HealthChecks::FsShardsCheck
+ Gitlab::HealthChecks::Redis::SharedStateCheck
].freeze
def prometheus_metrics_text
diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb
new file mode 100644
index 00000000000..2187f26d1ed
--- /dev/null
+++ b/app/services/milestones/promote_service.rb
@@ -0,0 +1,85 @@
+module Milestones
+ class PromoteService < Milestones::BaseService
+ PromoteMilestoneError = Class.new(StandardError)
+
+ def execute(milestone)
+ check_project_milestone!(milestone)
+
+ Milestone.transaction do
+ group_milestone = clone_project_milestone(milestone)
+
+ move_children_to_group_milestone(group_milestone)
+
+ # Destroy all milestones with same title across projects
+ destroy_old_milestones(milestone)
+
+ # Rollback if milestone is not valid
+ unless group_milestone.valid?
+ raise_error(group_milestone.errors.full_messages.to_sentence)
+ end
+
+ group_milestone
+ end
+ end
+
+ private
+
+ def milestone_ids_for_merge(group_milestone)
+ # Pluck need to be used here instead of select so the array of ids
+ # is persistent after old milestones gets deleted.
+ @milestone_ids_for_merge ||= begin
+ search_params = { title: group_milestone.title, project_ids: group_project_ids, state: 'all' }
+ milestones = MilestonesFinder.new(search_params).execute
+ milestones.pluck(:id)
+ end
+ end
+
+ def move_children_to_group_milestone(group_milestone)
+ milestone_ids_for_merge(group_milestone).in_groups_of(100, false) do |milestone_ids|
+ update_children(group_milestone, milestone_ids)
+ end
+ end
+
+ def check_project_milestone!(milestone)
+ raise_error('Only project milestones can be promoted.') unless milestone.project_milestone?
+ end
+
+ def clone_project_milestone(milestone)
+ params = milestone.slice(:title, :description, :start_date, :due_date, :state_event)
+
+ create_service = CreateService.new(group, current_user, params)
+
+ milestone = create_service.execute
+
+ # milestone won't be valid here because of duplicated title
+ milestone.save(validate: false)
+
+ milestone
+ end
+
+ def update_children(group_milestone, milestone_ids)
+ issues = Issue.where(project_id: group_project_ids, milestone_id: milestone_ids)
+ merge_requests = MergeRequest.where(source_project_id: group_project_ids, milestone_id: milestone_ids)
+
+ [issues, merge_requests].each do |issuable_collection|
+ issuable_collection.update_all(milestone_id: group_milestone.id)
+ end
+ end
+
+ def group
+ @group ||= parent.group || raise_error('Project does not belong to a group.')
+ end
+
+ def destroy_old_milestones(milestone)
+ Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all
+ end
+
+ def group_project_ids
+ @group_project_ids ||= group.projects.pluck(:id)
+ end
+
+ def raise_error(message)
+ raise PromoteMilestoneError, "Promotion failed - #{message}"
+ end
+ end
+end
diff --git a/app/services/notes/render_service.rb b/app/services/notes/render_service.rb
new file mode 100644
index 00000000000..a77e98c2b07
--- /dev/null
+++ b/app/services/notes/render_service.rb
@@ -0,0 +1,21 @@
+module Notes
+ class RenderService < BaseRenderer
+ # Renders a collection of Note instances.
+ #
+ # notes - The notes to render.
+ # project - The project to use for redacting.
+ # user - The user viewing the notes.
+
+ # Possible options:
+ # requested_path - The request path.
+ # project_wiki - The project's wiki.
+ # ref - The current Git reference.
+ # only_path - flag to turn relative paths into absolute ones.
+ # xhtml - flag to save the html in XHTML
+ def execute(notes, project, **opts)
+ renderer = Banzai::ObjectRenderer.new(project, current_user, **opts)
+
+ renderer.render(notes, :note)
+ end
+ end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 8d5da459882..be3b4b2ba07 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -390,7 +390,7 @@ class NotificationService
end
def relabeled_resource_email(target, labels, current_user, method)
- recipients = labels.flat_map { |l| l.subscribers(target.project) }
+ recipients = labels.flat_map { |l| l.subscribers(target.project) }.uniq
recipients = notifiable_users(
recipients, :subscription,
target: target,
diff --git a/app/services/projects/count_service.rb b/app/services/projects/count_service.rb
index aa034315280..7e575b2d6f3 100644
--- a/app/services/projects/count_service.rb
+++ b/app/services/projects/count_service.rb
@@ -1,7 +1,7 @@
module Projects
# Base class for the various service classes that count project data (e.g.
# issues or forks).
- class CountService
+ class CountService < BaseCountService
# The version of the cache format. This should be bumped whenever the
# underlying logic changes. This removes the need for explicitly flushing
# all caches.
@@ -11,29 +11,6 @@ module Projects
@project = project
end
- def relation_for_count
- raise(
- NotImplementedError,
- '"relation_for_count" must be implemented and return an ActiveRecord::Relation'
- )
- end
-
- def count
- Rails.cache.fetch(cache_key) { uncached_count }
- end
-
- def refresh_cache
- Rails.cache.write(cache_key, uncached_count)
- end
-
- def uncached_count
- relation_for_count.count
- end
-
- def delete_cache
- Rails.cache.delete(cache_key)
- end
-
def cache_key_name
raise(
NotImplementedError,
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 19d75ff2efa..81972df9b3c 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -51,7 +51,7 @@ module Projects
end
def wiki_path
- repo_path + '.wiki'
+ project.wiki.disk_path
end
def trash_repositories!
diff --git a/app/services/projects/forks_count_service.rb b/app/services/projects/forks_count_service.rb
index 3a0fa84b868..d9bdf3a8ad7 100644
--- a/app/services/projects/forks_count_service.rb
+++ b/app/services/projects/forks_count_service.rb
@@ -1,6 +1,6 @@
module Projects
# Service class for getting and caching the number of forks of a project.
- class ForksCountService < CountService
+ class ForksCountService < Projects::CountService
def relation_for_count
@project.forks
end
diff --git a/app/services/projects/group_links/create_service.rb b/app/services/projects/group_links/create_service.rb
new file mode 100644
index 00000000000..35624577024
--- /dev/null
+++ b/app/services/projects/group_links/create_service.rb
@@ -0,0 +1,15 @@
+module Projects
+ module GroupLinks
+ class CreateService < BaseService
+ def execute(group)
+ return false unless group
+
+ project.project_group_links.create(
+ group: group,
+ group_access: params[:link_group_access],
+ expires_at: params[:expires_at]
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb
new file mode 100644
index 00000000000..e3a20b4c1e4
--- /dev/null
+++ b/app/services/projects/group_links/destroy_service.rb
@@ -0,0 +1,11 @@
+module Projects
+ module GroupLinks
+ class DestroyService < BaseService
+ def execute(group_link)
+ return false unless group_link
+
+ group_link.destroy
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage/migrate_attachments_service.rb b/app/services/projects/hashed_storage/migrate_attachments_service.rb
new file mode 100644
index 00000000000..f8aaec8a9c0
--- /dev/null
+++ b/app/services/projects/hashed_storage/migrate_attachments_service.rb
@@ -0,0 +1,54 @@
+module Projects
+ module HashedStorage
+ AttachmentMigrationError = Class.new(StandardError)
+
+ class MigrateAttachmentsService < BaseService
+ attr_reader :logger, :old_path, :new_path
+
+ def initialize(project, logger = nil)
+ @project = project
+ @logger = logger || Rails.logger
+ end
+
+ def execute
+ @old_path = project.full_path
+ @new_path = project.disk_path
+
+ origin = FileUploader.dynamic_path_segment(project)
+ project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:attachments]
+ target = FileUploader.dynamic_path_segment(project)
+
+ result = move_folder!(origin, target)
+ project.save!
+
+ if result && block_given?
+ yield
+ end
+
+ result
+ end
+
+ private
+
+ def move_folder!(old_path, new_path)
+ unless File.directory?(old_path)
+ logger.info("Skipped attachments migration from '#{old_path}' to '#{new_path}', source path doesn't exist or is not a directory (PROJECT_ID=#{project.id})")
+ return
+ end
+
+ if File.exist?(new_path)
+ logger.error("Cannot migrate attachments from '#{old_path}' to '#{new_path}', target path already exist (PROJECT_ID=#{project.id})")
+ raise AttachmentMigrationError, "Target path '#{new_path}' already exist"
+ end
+
+ # Create hashed storage base path folder
+ FileUtils.mkdir_p(File.dirname(new_path))
+
+ FileUtils.mv(old_path, new_path)
+ logger.info("Migrated project attachments from '#{old_path}' to '#{new_path}' (PROJECT_ID=#{project.id})")
+
+ true
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
new file mode 100644
index 00000000000..7212e7524ab
--- /dev/null
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -0,0 +1,70 @@
+module Projects
+ module HashedStorage
+ class MigrateRepositoryService < BaseService
+ include Gitlab::ShellAdapter
+
+ attr_reader :old_disk_path, :new_disk_path, :old_wiki_disk_path, :old_storage_version, :logger
+
+ def initialize(project, logger = nil)
+ @project = project
+ @logger = logger || Rails.logger
+ end
+
+ def execute
+ @old_disk_path = project.disk_path
+ has_wiki = project.wiki.repository_exists?
+
+ @old_storage_version = project.storage_version
+ project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
+ project.ensure_storage_path_exists
+
+ @new_disk_path = project.disk_path
+
+ result = move_repository(@old_disk_path, @new_disk_path)
+
+ if has_wiki
+ @old_wiki_disk_path = "#{@old_disk_path}.wiki"
+ result &&= move_repository("#{@old_wiki_disk_path}", "#{@new_disk_path}.wiki")
+ end
+
+ unless result
+ rollback_folder_move
+ project.storage_version = nil
+ end
+
+ project.repository_read_only = false
+ project.save!
+
+ if result && block_given?
+ yield
+ end
+
+ result
+ end
+
+ private
+
+ def move_repository(from_name, to_name)
+ from_exists = gitlab_shell.exists?(project.repository_storage_path, "#{from_name}.git")
+ to_exists = gitlab_shell.exists?(project.repository_storage_path, "#{to_name}.git")
+
+ # If we don't find the repository on either original or target we should log that as it could be an issue if the
+ # project was not originally empty.
+ if !from_exists && !to_exists
+ logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
+ return false
+ elsif !from_exists
+ # Repository have been moved already.
+ return true
+ end
+
+ gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
+ end
+
+ def rollback_folder_move
+ move_repository(@new_disk_path, @old_disk_path)
+ move_repository("#{@new_disk_path}.wiki", "#{@old_disk_path}.wiki")
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage_migration_service.rb b/app/services/projects/hashed_storage_migration_service.rb
index 41259de3a16..662702c1db5 100644
--- a/app/services/projects/hashed_storage_migration_service.rb
+++ b/app/services/projects/hashed_storage_migration_service.rb
@@ -1,68 +1,22 @@
module Projects
class HashedStorageMigrationService < BaseService
- include Gitlab::ShellAdapter
-
- attr_reader :old_disk_path, :new_disk_path
+ attr_reader :logger
def initialize(project, logger = nil)
@project = project
- @logger ||= Rails.logger
+ @logger = logger || Rails.logger
end
def execute
- return if project.hashed_storage?
-
- @old_disk_path = project.disk_path
- has_wiki = project.wiki.repository_exists?
-
- project.storage_version = Storage::HashedProject::STORAGE_VERSION
- project.ensure_storage_path_exists
-
- @new_disk_path = project.disk_path
-
- result = move_repository(@old_disk_path, @new_disk_path)
-
- if has_wiki
- result &&= move_repository("#{@old_disk_path}.wiki", "#{@new_disk_path}.wiki")
- end
-
- unless result
- rollback_folder_move
- return
+ # Migrate repository from Legacy to Hashed Storage
+ unless project.hashed_storage?(:repository)
+ return unless HashedStorage::MigrateRepositoryService.new(project, logger).execute
end
- project.repository_read_only = false
- project.save!
-
- block_given? ? yield : result
- end
-
- private
-
- def move_repository(from_name, to_name)
- from_exists = gitlab_shell.exists?(project.repository_storage_path, "#{from_name}.git")
- to_exists = gitlab_shell.exists?(project.repository_storage_path, "#{to_name}.git")
-
- # If we don't find the repository on either original or target we should log that as it could be an issue if the
- # project was not originally empty.
- if !from_exists && !to_exists
- logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
- return false
- elsif !from_exists
- # Repository have been moved already.
- return true
+ # Migrate attachments from Legacy to Hashed Storage
+ unless project.hashed_storage?(:attachments)
+ HashedStorage::MigrateAttachmentsService.new(project, logger).execute
end
-
- gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
- end
-
- def rollback_folder_move
- move_repository(@new_disk_path, @old_disk_path)
- move_repository("#{@new_disk_path}.wiki", "#{@old_disk_path}.wiki")
- end
-
- def logger
- @logger
end
end
end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index c3bf0031409..f2d676af5c3 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -4,8 +4,18 @@ module Projects
Error = Class.new(StandardError)
+ # Returns true if this importer is supposed to perform its work in the
+ # background.
+ #
+ # This method will only return `true` if async importing is explicitly
+ # supported by an importer class (`Gitlab::GithubImport::ParallelImporter`
+ # for example).
+ def async?
+ has_importer? && !!importer_class.try(:async?)
+ end
+
def execute
- add_repository_to_project unless project.gitlab_project_import?
+ add_repository_to_project
import_data
@@ -17,6 +27,14 @@ module Projects
private
def add_repository_to_project
+ if project.external_import? && !unknown_url?
+ raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
+ end
+
+ # We should skip the repository for a GitHub import or GitLab project import,
+ # because these importers fetch the project repositories for us.
+ return if has_importer? && importer_class.try(:imports_repository?)
+
if unknown_url?
# In this case, we only want to import issues, not a repository.
create_repository
@@ -32,19 +50,16 @@ module Projects
end
def import_repository
- raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
-
- # We should return early for a GitHub import because the new GitHub
- # importer fetch the project repositories for us.
- return if project.github_import?
-
begin
- if project.gitea_import?
- fetch_repository
+ refmap = importer_class.try(:refmap) if has_importer?
+
+ if refmap
+ project.ensure_repository
+ project.repository.fetch_as_mirror(project.import_url, refmap: refmap)
else
- clone_repository
+ gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, project.import_url)
end
- rescue Gitlab::Shell::Error => e
+ rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e
# Expire cache to prevent scenarios such as:
# 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
# 2. Retried import, repo is broken or not imported but +exists?+ still returns true
@@ -54,17 +69,6 @@ module Projects
end
end
- def clone_repository
- gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, project.import_url)
- end
-
- def fetch_repository
- project.ensure_repository
- project.repository.add_remote(project.import_type, project.import_url)
- project.repository.set_remote_as_mirror(project.import_type)
- project.repository.fetch_remote(project.import_type, forced: true)
- end
-
def import_data
return unless has_importer?
@@ -75,12 +79,16 @@ module Projects
end
end
+ def importer_class
+ @importer_class ||= Gitlab::ImportSources.importer(project.import_type)
+ end
+
def has_importer?
Gitlab::ImportSources.importer_names.include?(project.import_type)
end
def importer
- Gitlab::ImportSources.importer(project.import_type).new(project)
+ importer_class.new(project)
end
def unknown_url?
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
index 3c0d186a73c..25de97325e2 100644
--- a/app/services/projects/open_issues_count_service.rb
+++ b/app/services/projects/open_issues_count_service.rb
@@ -1,7 +1,7 @@
module Projects
# Service class for counting and caching the number of open issues of a
# project.
- class OpenIssuesCountService < CountService
+ class OpenIssuesCountService < Projects::CountService
def relation_for_count
# We don't include confidential issues in this number since this would
# expose the number of confidential issues to non project members.
diff --git a/app/services/projects/open_merge_requests_count_service.rb b/app/services/projects/open_merge_requests_count_service.rb
index 2a90f78b90d..77e6448fd5e 100644
--- a/app/services/projects/open_merge_requests_count_service.rb
+++ b/app/services/projects/open_merge_requests_count_service.rb
@@ -1,7 +1,7 @@
module Projects
# Service class for counting and caching the number of open merge requests of
# a project.
- class OpenMergeRequestsCountService < CountService
+ class OpenMergeRequestsCountService < Projects::CountService
def relation_for_count
@project.merge_requests.opened
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 5957f612e84..e5cd6fcdfe3 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -60,21 +60,14 @@ module Projects
# Notifications
project.send_move_instructions(@old_path)
- # Move main repository
- # TODO: check storage type and NOOP when not using Legacy
- unless move_repo_folder(@old_path, @new_path)
- raise TransferError.new('Cannot move project')
- end
-
- # Move wiki repo also if present
- # TODO: check storage type and NOOP when not using Legacy
- move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki")
+ # Directories on disk
+ move_project_folders(project)
# Move missing group labels to project
Labels::TransferService.new(current_user, @old_group, project).execute
# Move uploads
- Gitlab::UploadsTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
+ move_project_uploads(project)
# Move pages
Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
@@ -131,5 +124,30 @@ module Projects
def execute_system_hooks
SystemHooksService.new.execute_hooks_for(project, :transfer)
end
+
+ def move_project_folders(project)
+ return if project.hashed_storage?(:repository)
+
+ # Move main repository
+ unless move_repo_folder(@old_path, @new_path)
+ raise TransferError.new("Cannot move project")
+ end
+
+ # Disk path is changed; we need to ensure we reload it
+ project.reload_repository!
+
+ # Move wiki repo also if present
+ move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki")
+ end
+
+ def move_project_uploads(project)
+ return if project.hashed_storage?(:attachments)
+
+ Gitlab::UploadsTransfer.new.move_project(
+ project.path,
+ @old_namespace.full_path,
+ @new_namespace.full_path
+ )
+ end
end
end
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index abe414d0c05..c499f384426 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -3,20 +3,26 @@ module Projects
def execute
return unless @project.forked?
- @project.forked_from_project.lfs_objects.find_each do |lfs_object|
- lfs_object.projects << @project
+ if fork_source = @project.fork_source
+ fork_source.lfs_objects.find_each do |lfs_object|
+ lfs_object.projects << @project
+ end
+
+ refresh_forks_count(fork_source)
end
- merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project)
+ merge_requests = @project.fork_network
+ .merge_requests
+ .opened
+ .where.not(target_project: @project)
+ .from_project(@project)
merge_requests.each do |mr|
::MergeRequests::CloseService.new(@project, @current_user).execute(mr)
end
- refresh_forks_count(@project.forked_from_project)
-
- @project.forked_project_link.destroy
@project.fork_network_member.destroy
+ @project.forked_project_link.destroy
end
def refresh_forks_count(project)
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 13e292a18bf..72eecc61c96 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -15,7 +15,7 @@ module Projects
return error("Could not set the default branch") unless project.change_head(params[:default_branch])
end
- if project.update_attributes(params.except(:default_branch))
+ if project.update_attributes(update_params)
if project.previous_changes.include?('path')
project.rename_repo
else
@@ -31,8 +31,16 @@ module Projects
end
end
+ def run_auto_devops_pipeline?
+ params.dig(:run_auto_devops_pipeline_explicit) == 'true' || params.dig(:run_auto_devops_pipeline_implicit) == 'true'
+ end
+
private
+ def update_params
+ params.except(:default_branch, :run_auto_devops_pipeline_explicit, :run_auto_devops_pipeline_implicit)
+ end
+
def renaming_project_with_container_registry_tags?
new_path = params[:path]
diff --git a/app/services/protected_branches/api_create_service.rb b/app/services/protected_branches/api_create_service.rb
deleted file mode 100644
index f2040dfa03a..00000000000
--- a/app/services/protected_branches/api_create_service.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
-# flags for backward compatibility, and so performs translation between that format and the
-# internal data model (separate access levels). The translation code is non-trivial, and so
-# lives in this service.
-module ProtectedBranches
- class ApiCreateService < BaseService
- def execute
- push_access_level =
- if params.delete(:developers_can_push)
- Gitlab::Access::DEVELOPER
- else
- Gitlab::Access::MASTER
- end
-
- merge_access_level =
- if params.delete(:developers_can_merge)
- Gitlab::Access::DEVELOPER
- else
- Gitlab::Access::MASTER
- end
-
- @params.merge!(push_access_levels_attributes: [{ access_level: push_access_level }],
- merge_access_levels_attributes: [{ access_level: merge_access_level }])
-
- service = ProtectedBranches::CreateService.new(@project, @current_user, @params)
- service.execute
- end
- end
-end
diff --git a/app/services/protected_branches/api_update_service.rb b/app/services/protected_branches/api_update_service.rb
deleted file mode 100644
index bdb0e0cc8bf..00000000000
--- a/app/services/protected_branches/api_update_service.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
-# flags for backward compatibility, and so performs translation between that format and the
-# internal data model (separate access levels). The translation code is non-trivial, and so
-# lives in this service.
-module ProtectedBranches
- class ApiUpdateService < BaseService
- def execute(protected_branch)
- @developers_can_push = params.delete(:developers_can_push)
- @developers_can_merge = params.delete(:developers_can_merge)
-
- @protected_branch = protected_branch
-
- protected_branch.transaction do
- delete_redundant_access_levels
-
- case @developers_can_push
- when true
- params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
- when false
- params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }]
- end
-
- case @developers_can_merge
- when true
- params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
- when false
- params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }]
- end
-
- service = ProtectedBranches::UpdateService.new(@project, @current_user, @params)
- service.execute(protected_branch)
- end
- end
-
- private
-
- def delete_redundant_access_levels
- unless @developers_can_merge.nil?
- @protected_branch.merge_access_levels.destroy_all
- end
-
- unless @developers_can_push.nil?
- @protected_branch.push_access_levels.destroy_all
- end
- end
- end
-end
diff --git a/app/services/protected_branches/legacy_api_create_service.rb b/app/services/protected_branches/legacy_api_create_service.rb
new file mode 100644
index 00000000000..e358fd0374e
--- /dev/null
+++ b/app/services/protected_branches/legacy_api_create_service.rb
@@ -0,0 +1,29 @@
+# The branches#protect API still uses the `developers_can_push` and `developers_can_merge`
+# flags for backward compatibility, and so performs translation between that format and the
+# internal data model (separate access levels). The translation code is non-trivial, and so
+# lives in this service.
+module ProtectedBranches
+ class LegacyApiCreateService < BaseService
+ def execute
+ push_access_level =
+ if params.delete(:developers_can_push)
+ Gitlab::Access::DEVELOPER
+ else
+ Gitlab::Access::MASTER
+ end
+
+ merge_access_level =
+ if params.delete(:developers_can_merge)
+ Gitlab::Access::DEVELOPER
+ else
+ Gitlab::Access::MASTER
+ end
+
+ @params.merge!(push_access_levels_attributes: [{ access_level: push_access_level }],
+ merge_access_levels_attributes: [{ access_level: merge_access_level }])
+
+ service = ProtectedBranches::CreateService.new(@project, @current_user, @params)
+ service.execute
+ end
+ end
+end
diff --git a/app/services/protected_branches/legacy_api_update_service.rb b/app/services/protected_branches/legacy_api_update_service.rb
new file mode 100644
index 00000000000..33176253ca2
--- /dev/null
+++ b/app/services/protected_branches/legacy_api_update_service.rb
@@ -0,0 +1,47 @@
+# The branches#protect API still uses the `developers_can_push` and `developers_can_merge`
+# flags for backward compatibility, and so performs translation between that format and the
+# internal data model (separate access levels). The translation code is non-trivial, and so
+# lives in this service.
+module ProtectedBranches
+ class LegacyApiUpdateService < BaseService
+ def execute(protected_branch)
+ @developers_can_push = params.delete(:developers_can_push)
+ @developers_can_merge = params.delete(:developers_can_merge)
+
+ @protected_branch = protected_branch
+
+ protected_branch.transaction do
+ delete_redundant_access_levels
+
+ case @developers_can_push
+ when true
+ params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
+ when false
+ params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }]
+ end
+
+ case @developers_can_merge
+ when true
+ params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }]
+ when false
+ params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }]
+ end
+
+ service = ProtectedBranches::UpdateService.new(@project, @current_user, @params)
+ service.execute(protected_branch)
+ end
+ end
+
+ private
+
+ def delete_redundant_access_levels
+ unless @developers_can_merge.nil?
+ @protected_branch.merge_access_levels.destroy_all
+ end
+
+ unless @developers_can_push.nil?
+ @protected_branch.push_access_levels.destroy_all
+ end
+ end
+ end
+end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index a077b3584b0..06ac86cd5a9 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -381,7 +381,7 @@ module QuickActions
end
desc 'Add or substract spent time'
- explanation do |time_spent|
+ explanation do |time_spent, time_spent_date|
if time_spent
if time_spent > 0
verb = 'Adds'
@@ -394,16 +394,20 @@ module QuickActions
"#{verb} #{Gitlab::TimeTrackingFormatter.output(value)} spent time."
end
end
- params '<1h 30m | -1h 30m>'
+ params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>'
condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
- parse_params do |raw_duration|
- Gitlab::TimeTrackingFormatter.parse(raw_duration)
+ parse_params do |raw_time_date|
+ Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute
end
- command :spend do |time_spent|
+ command :spend do |time_spent, time_spent_date|
if time_spent
- @updates[:spend_time] = { duration: time_spent, user: current_user }
+ @updates[:spend_time] = {
+ duration: time_spent,
+ user: current_user,
+ spent_at: time_spent_date
+ }
end
end
@@ -458,7 +462,7 @@ module QuickActions
target_branch_param.strip
end
command :target_branch do |branch_name|
- @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
+ @updates[:target_branch] = branch_name if project.repository.branch_exists?(branch_name)
end
desc 'Move issue from one column of the board to another'
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index a1c2f8d0180..911cc919bb8 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -35,24 +35,22 @@ class SystemHooksService
data[:old_path_with_namespace] = model.old_path_with_namespace
end
when User
- data.merge!({
- name: model.name,
- email: model.email,
- user_id: model.id,
- username: model.username
- })
+ data.merge!(user_data(model))
+
+ if event == :rename
+ data[:old_username] = model.username_was
+ end
when ProjectMember
data.merge!(project_member_data(model))
when Group
- owner = model.owner
+ data.merge!(group_data(model))
- data.merge!(
- name: model.name,
- path: model.path,
- group_id: model.id,
- owner_name: owner.respond_to?(:name) ? owner.name : nil,
- owner_email: owner.respond_to?(:email) ? owner.email : nil
- )
+ if event == :rename
+ data.merge!(
+ old_path: model.path_was,
+ old_full_path: model.full_path_was
+ )
+ end
when GroupMember
data.merge!(group_member_data(model))
end
@@ -83,7 +81,7 @@ class SystemHooksService
project_id: model.id,
owner_name: owner.name,
owner_email: owner.respond_to?(:email) ? owner.email : "",
- project_visibility: Project.visibility_levels.key(model.visibility_level_value).downcase
+ project_visibility: model.visibility.downcase
}
end
@@ -104,6 +102,19 @@ class SystemHooksService
}
end
+ def group_data(model)
+ owner = model.owner
+
+ {
+ name: model.name,
+ path: model.path,
+ full_path: model.full_path,
+ group_id: model.id,
+ owner_name: owner.try(:name),
+ owner_email: owner.try(:email)
+ }
+ end
+
def group_member_data(model)
{
group_name: model.group.name,
@@ -116,4 +127,13 @@ class SystemHooksService
group_access: model.human_access
}
end
+
+ def user_data(model)
+ {
+ name: model.name,
+ email: model.email,
+ user_id: model.id,
+ username: model.username
+ }
+ end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 7b32e215c7f..30a5aab13bf 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -162,7 +162,6 @@ module SystemNoteService
# "changed time estimate to 3d 5h"
#
# Returns the created Note object
-
def change_time_estimate(noteable, project, author)
parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
body = if noteable.time_estimate == 0
@@ -188,16 +187,17 @@ module SystemNoteService
# "added 2h 30m of time spent"
#
# Returns the created Note object
-
def change_time_spent(noteable, project, author)
time_spent = noteable.time_spent
if time_spent == :reset
body = "removed time spent"
else
+ spent_at = noteable.spent_at
parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
action = time_spent > 0 ? 'added' : 'subtracted'
body = "#{action} #{parsed_time} of time spent"
+ body << " at #{spent_at}" if spent_at
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
@@ -241,14 +241,10 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
end
- def remove_merge_request_wip(noteable, project, author)
- body = 'unmarked as a **Work In Progress**'
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
- end
+ def handle_merge_request_wip(noteable, project, author)
+ prefix = noteable.work_in_progress? ? "marked" : "unmarked"
- def add_merge_request_wip(noteable, project, author)
- body = 'marked as a **Work In Progress**'
+ body = "#{prefix} as a **Work In Progress**"
create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
@@ -451,10 +447,6 @@ module SystemNoteService
end
end
- def cross_reference?(note_text)
- note_text =~ /\A#{cross_reference_note_prefix}/i
- end
-
# Check if a cross-reference is disallowed
#
# This method prevents adding a "mentioned in !1" note on every single commit
@@ -484,19 +476,8 @@ module SystemNoteService
# mentioner - Mentionable object
#
# Returns Boolean
-
def cross_reference_exists?(noteable, mentioner)
- # Initial scope should be system notes of this noteable type
- notes = Note.system.where(noteable_type: noteable.class)
-
- notes =
- if noteable.is_a?(Commit)
- # Commits have non-integer IDs, so they're stored in `commit_id`
- notes.where(commit_id: noteable.id)
- else
- notes.where(noteable_id: noteable.id)
- end
-
+ notes = noteable.notes.system
notes_for_mentioner(mentioner, noteable, notes).exists?
end
@@ -593,11 +574,15 @@ module SystemNoteService
def discussion_lock(issuable, author)
action = issuable.discussion_locked? ? 'locked' : 'unlocked'
- body = "#{action} this issue"
+ body = "#{action} this #{issuable.class.to_s.titleize.downcase}"
create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action))
end
+ def cross_reference?(note_text)
+ note_text =~ /\A#{cross_reference_note_prefix}/i
+ end
+
private
def notes_for_mentioner(mentioner, noteable, notes)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 6ee96d6a0f8..575853fd66b 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -31,20 +31,20 @@ class TodoService
mark_pending_todos_as_done(issue, current_user)
end
- # When we destroy an issue we should:
+ # When we destroy an issuable we should:
#
# * refresh the todos count cache for the current user
#
- def destroy_issue(issue, current_user)
- destroy_issuable(issue, current_user)
+ def destroy_issuable(issuable, user)
+ user.update_todos_count_cache
end
# When we reassign an issue we should:
#
# * create a pending todo for new assignee if issue is assigned
#
- def reassigned_issue(issue, current_user)
- create_assignment_todo(issue, current_user)
+ def reassigned_issue(issue, current_user, old_assignees = [])
+ create_assignment_todo(issue, current_user, old_assignees)
end
# When create a merge request we should:
@@ -72,14 +72,6 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user)
end
- # When we destroy a merge request we should:
- #
- # * refresh the todos count cache for the current user
- #
- def destroy_merge_request(merge_request, current_user)
- destroy_issuable(merge_request, current_user)
- end
-
# When we reassign a merge request we should:
#
# * creates a pending todo for new assignee if merge request is assigned
@@ -216,6 +208,7 @@ class TodoService
def create_todos(users, attributes)
Array(users).map do |user|
next if pending_todos(user, attributes).exists?
+
todo = Todo.create(attributes.merge(user_id: user.id))
user.update_todos_count_cache
todo
@@ -234,10 +227,6 @@ class TodoService
create_mention_todos(issuable.project, issuable, author, nil, skip_users)
end
- def destroy_issuable(issuable, user)
- user.update_todos_count_cache
- end
-
def toggling_tasks?(issuable)
issuable.previous_changes.include?('description') &&
issuable.tasks? && issuable.updated_tasks.any?
@@ -254,10 +243,11 @@ class TodoService
create_mention_todos(project, target, author, note, skip_users)
end
- def create_assignment_todo(issuable, author)
+ def create_assignment_todo(issuable, author, old_assignees = [])
if issuable.assignees.any?
+ assignees = issuable.assignees - old_assignees
attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED)
- create_todos(issuable.assignees, attributes)
+ create_todos(assignees, attributes)
end
end
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index 6f05500adea..61f1568f366 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -34,7 +34,7 @@ module Users
private
def can_create_user?
- (current_user.nil? && current_application_settings.signup_enabled?) || current_user&.admin?
+ (current_user.nil? && current_application_settings.allow_signup?) || current_user&.admin?
end
# Allowed params for creating a user (admins only)
diff --git a/app/services/users/keys_count_service.rb b/app/services/users/keys_count_service.rb
new file mode 100644
index 00000000000..f82d27eded9
--- /dev/null
+++ b/app/services/users/keys_count_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Users
+ # Service class for getting the number of SSH keys that belong to a user.
+ class KeysCountService < BaseCountService
+ attr_reader :user
+
+ # user - The User for which to get the number of SSH keys.
+ def initialize(user)
+ @user = user
+ end
+
+ def relation_for_count
+ user.keys
+ end
+
+ def raw?
+ # Since we're storing simple integers we don't need all of the additional
+ # Marshal data Rails includes by default.
+ true
+ end
+
+ def cache_key
+ "users/key-count-service/#{user.id}"
+ end
+ end
+end
diff --git a/app/services/users/last_push_event_service.rb b/app/services/users/last_push_event_service.rb
index f2bfb60604f..57e446d7f30 100644
--- a/app/services/users/last_push_event_service.rb
+++ b/app/services/users/last_push_event_service.rb
@@ -16,8 +16,8 @@ module Users
user_cache_key
]
- if event.project.forked?
- keys << project_cache_key(event.project.forked_from_project)
+ if forked_from = event.project.forked_from_project
+ keys << project_cache_key(forked_from)
end
keys.each { |key| set_key(key, event.id) }
diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb
index 3a9c151cf9b..976017dfa82 100644
--- a/app/services/users/migrate_to_ghost_user_service.rb
+++ b/app/services/users/migrate_to_ghost_user_service.rb
@@ -25,7 +25,7 @@ module Users
user.block
# Reverse the user block if record migration fails
- if !migrate_records && transition
+ if !migrate_records_in_transaction && transition
transition.rollback
user.save!
end
@@ -36,18 +36,22 @@ module Users
private
- def migrate_records
+ def migrate_records_in_transaction
user.transaction(requires_new: true) do
@ghost_user = User.ghost
- migrate_issues
- migrate_merge_requests
- migrate_notes
- migrate_abuse_reports
- migrate_award_emojis
+ migrate_records
end
end
+ def migrate_records
+ migrate_issues
+ migrate_merge_requests
+ migrate_notes
+ migrate_abuse_reports
+ migrate_award_emojis
+ end
+
def migrate_issues
user.issues.update_all(author_id: ghost_user.id)
Issue.where(last_edited_by_id: user.id).update_all(last_edited_by_id: ghost_user.id)
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 7027ac4b5db..71658df5b41 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -26,11 +26,22 @@ class FileUploader < GitlabUploader
# This is used to build Upload paths dynamically based on the model's current
# namespace and path, allowing us to ignore renames or transfers.
#
- # model - Object that responds to `path_with_namespace`
+ # model - Object that responds to `full_path` and `disk_path`
#
# Returns a String without a trailing slash
- def self.dynamic_path_segment(model)
- File.join(CarrierWave.root, base_dir, model.full_path)
+ def self.dynamic_path_segment(project)
+ if project.hashed_storage?(:attachments)
+ dynamic_path_builder(project.disk_path)
+ else
+ dynamic_path_builder(project.full_path)
+ end
+ end
+
+ # Auxiliary method to build dynamic path segment when not using a project model
+ #
+ # Prefer to use the `.dynamic_path_segment` as it includes Hashed Storage specific logic
+ def self.dynamic_path_builder(path)
+ File.join(CarrierWave.root, base_dir, path)
end
attr_accessor :model
diff --git a/app/validators/abstract_path_validator.rb b/app/validators/abstract_path_validator.rb
new file mode 100644
index 00000000000..adbccb65a84
--- /dev/null
+++ b/app/validators/abstract_path_validator.rb
@@ -0,0 +1,38 @@
+class AbstractPathValidator < ActiveModel::EachValidator
+ extend Gitlab::EncodingHelper
+
+ def self.path_regex
+ raise NotImplementedError
+ end
+
+ def self.format_regex
+ raise NotImplementedError
+ end
+
+ def self.format_error_message
+ raise NotImplementedError
+ end
+
+ def self.full_path(record, value)
+ value
+ end
+
+ def self.valid_path?(path)
+ encode!(path)
+ "#{path}/" =~ path_regex
+ end
+
+ def validate_each(record, attribute, value)
+ unless value =~ self.class.format_regex
+ record.errors.add(attribute, self.class.format_error_message)
+ return
+ end
+
+ full_path = self.class.full_path(record, value)
+ return unless full_path
+
+ unless self.class.valid_path?(full_path)
+ record.errors.add(attribute, "#{value} is a reserved name")
+ end
+ end
+end
diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb
index 098b16017d2..8c7bb750339 100644
--- a/app/validators/certificate_key_validator.rb
+++ b/app/validators/certificate_key_validator.rb
@@ -17,6 +17,7 @@ class CertificateKeyValidator < ActiveModel::EachValidator
def valid_private_key_pem?(value)
return false unless value
+
pkey = OpenSSL::PKey::RSA.new(value)
pkey.private?
rescue OpenSSL::PKey::PKeyError
diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb
index e3d18097f71..5239e70a326 100644
--- a/app/validators/certificate_validator.rb
+++ b/app/validators/certificate_validator.rb
@@ -17,6 +17,7 @@ class CertificateValidator < ActiveModel::EachValidator
def valid_certificate_pem?(value)
return false unless value
+
OpenSSL::X509::Certificate.new(value).present?
rescue OpenSSL::X509::CertificateError
false
diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb
new file mode 100644
index 00000000000..13ec342f399
--- /dev/null
+++ b/app/validators/cluster_name_validator.rb
@@ -0,0 +1,24 @@
+# ClusterNameValidator
+#
+# Custom validator for ClusterName.
+class ClusterNameValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if record.user?
+ unless value.present?
+ record.errors.add(attribute, " has to be present")
+ end
+ elsif record.gcp?
+ if record.persisted? && record.name_changed?
+ record.errors.add(attribute, " can not be changed because it's synchronized with provider")
+ end
+
+ unless value.length >= 1 && value.length <= 63
+ record.errors.add(attribute, " is invalid syntax")
+ end
+
+ unless value =~ Gitlab::Regex.kubernetes_namespace_regex
+ record.errors.add(attribute, Gitlab::Regex.kubernetes_namespace_regex_message)
+ end
+ end
+ end
+end
diff --git a/app/validators/dynamic_path_validator.rb b/app/validators/dynamic_path_validator.rb
deleted file mode 100644
index 4688aabc2a8..00000000000
--- a/app/validators/dynamic_path_validator.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# DynamicPathValidator
-#
-# Custom validator for GitLab path values.
-# These paths are assigned to `Namespace` (& `Group` as a subclass) & `Project`
-#
-# Values are checked for formatting and exclusion from a list of illegal path
-# names.
-class DynamicPathValidator < ActiveModel::EachValidator
- extend Gitlab::EncodingHelper
-
- class << self
- def valid_user_path?(path)
- encode!(path)
- "#{path}/" =~ Gitlab::PathRegex.root_namespace_path_regex
- end
-
- def valid_group_path?(path)
- encode!(path)
- "#{path}/" =~ Gitlab::PathRegex.full_namespace_path_regex
- end
-
- def valid_project_path?(path)
- encode!(path)
- "#{path}/" =~ Gitlab::PathRegex.full_project_path_regex
- end
- end
-
- def path_valid_for_record?(record, value)
- full_path = record.respond_to?(:build_full_path) ? record.build_full_path : value
-
- return true unless full_path
-
- case record
- when Project
- self.class.valid_project_path?(full_path)
- when Group
- self.class.valid_group_path?(full_path)
- else # User or non-Group Namespace
- self.class.valid_user_path?(full_path)
- end
- end
-
- def validate_each(record, attribute, value)
- unless value =~ Gitlab::PathRegex.namespace_format_regex
- record.errors.add(attribute, Gitlab::PathRegex.namespace_format_message)
- return
- end
-
- unless path_valid_for_record?(record, value)
- record.errors.add(attribute, "#{value} is a reserved name")
- end
- end
-end
diff --git a/app/validators/namespace_path_validator.rb b/app/validators/namespace_path_validator.rb
new file mode 100644
index 00000000000..4a0aa64ae0c
--- /dev/null
+++ b/app/validators/namespace_path_validator.rb
@@ -0,0 +1,19 @@
+class NamespacePathValidator < AbstractPathValidator
+ extend Gitlab::EncodingHelper
+
+ def self.path_regex
+ Gitlab::PathRegex.full_namespace_path_regex
+ end
+
+ def self.format_regex
+ Gitlab::PathRegex.namespace_format_regex
+ end
+
+ def self.format_error_message
+ Gitlab::PathRegex.namespace_format_message
+ end
+
+ def self.full_path(record, value)
+ record.build_full_path
+ end
+end
diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb
new file mode 100644
index 00000000000..829b596ad3c
--- /dev/null
+++ b/app/validators/project_path_validator.rb
@@ -0,0 +1,19 @@
+class ProjectPathValidator < AbstractPathValidator
+ extend Gitlab::EncodingHelper
+
+ def self.path_regex
+ Gitlab::PathRegex.full_project_path_regex
+ end
+
+ def self.format_regex
+ Gitlab::PathRegex.project_path_format_regex
+ end
+
+ def self.format_error_message
+ Gitlab::PathRegex.project_path_format_message
+ end
+
+ def self.full_path(record, value)
+ record.build_full_path
+ end
+end
diff --git a/app/validators/user_path_validator.rb b/app/validators/user_path_validator.rb
new file mode 100644
index 00000000000..adf02901802
--- /dev/null
+++ b/app/validators/user_path_validator.rb
@@ -0,0 +1,15 @@
+class UserPathValidator < AbstractPathValidator
+ extend Gitlab::EncodingHelper
+
+ def self.path_regex
+ Gitlab::PathRegex.root_namespace_path_regex
+ end
+
+ def self.format_regex
+ Gitlab::PathRegex.namespace_format_regex
+ end
+
+ def self.format_error_message
+ Gitlab::PathRegex.namespace_format_message
+ end
+end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 935787d1a4a..4a2238fe277 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -43,7 +43,7 @@
= f.hidden_field :header_logo_cache
= f.file_field :header_logo, class: ""
.hint
- Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo
+ Maximum file size is 1MB. Pages are optimized for a 28px tall header logo
.form-actions
= f.submit 'Save', class: 'btn btn-save append-right-10'
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index dbaed1d09fb..a9d0503bc73 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -160,9 +160,22 @@
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
- = f.label :password_authentication_enabled do
- = f.check_box :password_authentication_enabled
- Sign-in enabled
+ = f.label :password_authentication_enabled_for_web do
+ = f.check_box :password_authentication_enabled_for_web
+ Password authentication enabled for web interface
+ .help-block
+ When disabled, an external authentication provider must be used.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :password_authentication_enabled_for_git do
+ = f.check_box :password_authentication_enabled_for_git
+ Password authentication enabled for Git over HTTP(S)
+ .help-block
+ When disabled, a Personal Access Token
+ - if Gitlab::LDAP::Config.enabled?
+ or LDAP password
+ must be used to authenticate.
- 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,6 +543,44 @@
= succeed "." do
= link_to "repository storages documentation", help_page_path("administration/repository_storages")
+ %fieldset
+ %legend Git Storage Circuitbreaker settings
+ .form-group
+ = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :circuitbreaker_access_retries, class: 'form-control'
+ .help-block
+ = circuitbreaker_access_retries_help_text
+ .form-group
+ = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
+ .help-block
+ = circuitbreaker_storage_timeout_help_text
+ .form-group
+ = f.label :circuitbreaker_backoff_threshold, _('Number of failures before backing off'), class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :circuitbreaker_backoff_threshold, class: 'form-control'
+ .help-block
+ = circuitbreaker_backoff_threshold_help_text
+ .form-group
+ = f.label :circuitbreaker_failure_wait_time, _('Seconds to wait after a storage failure'), class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :circuitbreaker_failure_wait_time, class: 'form-control'
+ .help-block
+ = circuitbreaker_failure_wait_time_help_text
+ .form-group
+ = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
+ .help-block
+ = circuitbreaker_failure_count_help_text
+ .form-group
+ = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
+ .help-block
+ = circuitbreaker_failure_reset_time_help_text
%fieldset
%legend Repository Checks
@@ -681,6 +732,30 @@
Number of Git pushes after which 'git gc' is run.
%fieldset
+ %legend Gitaly Timeouts
+ .form-group
+ = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :gitaly_timeout_default, class: 'form-control'
+ .help-block
+ Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
+ for git fetch/push operations or Sidekiq jobs.
+ .form-group
+ = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :gitaly_timeout_fast, class: 'form-control'
+ .help-block
+ Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
+ If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
+ can help maintain the stability of the GitLab instance.
+ .form-group
+ = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :gitaly_timeout_medium, class: 'form-control'
+ .help-block
+ Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
+
+ %fieldset
%legend Web terminal
.form-group
= f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
@@ -705,5 +780,56 @@
installations. Set to 0 to completely disable polling.
= link_to icon('question-circle'), help_page_path('administration/polling')
+ %fieldset
+ %legend User and IP Rate Limits
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :throttle_unauthenticated_enabled do
+ = f.check_box :throttle_unauthenticated_enabled
+ Enable unauthenticated request rate limit
+ %span.help-block
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :throttle_authenticated_api_enabled do
+ = f.check_box :throttle_authenticated_api_enabled
+ Enable authenticated API request rate limit
+ %span.help-block
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :throttle_authenticated_web_enabled do
+ = f.check_box :throttle_authenticated_web_enabled
+ Enable authenticated web request rate limit
+ %span.help-block
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 3ef8f2a3acb..f0cc4d7ee62 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -42,4 +42,4 @@
.panel.panel-default
- %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: none" }
+ %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" }
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 2f0143c7eff..a24516355bf 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -45,10 +45,10 @@
.well-segment.admin-well.admin-well-features
%h4 Features
- sign_up = "Sign up"
- %p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") }
+ %p{ "aria-label" => "#{sign_up}: status " + (allow_signup? ? "on" : "off") }
= sign_up
%span.light.pull-right
- = boolean_to_icon signup_enabled?
+ = boolean_to_icon allow_signup?
- ldap = "LDAP"
%p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") }
= ldap
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index e3a77dfdf10..47cc2d4d27e 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -20,7 +20,7 @@
= visibility_level_icon(group.visibility_level, fw: false)
.avatar-container.s40
- = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+ = group_icon(group, class: "avatar s40 hidden-xs")
.title
= link_to [:admin, group], class: 'group-name' do
= group.full_name
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 3e02f7b1e16..2545cecc721 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -16,7 +16,7 @@
%ul.well-list
%li
.avatar-container.s60
- = image_tag group_icon(@group), class: "avatar s60"
+ = group_icon(@group, class: "avatar s60")
%li
%span.light Name:
%strong= @group.name
diff --git a/app/views/admin/hook_logs/_index.html.haml b/app/views/admin/hook_logs/_index.html.haml
index 7dd9943190f..91a8c0c62fe 100644
--- a/app/views/admin/hook_logs/_index.html.haml
+++ b/app/views/admin/hook_logs/_index.html.haml
@@ -24,7 +24,7 @@
%td
= truncate(hook_log.url, length: 50)
%td.light
- #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%td.light
= time_ago_with_tooltip(hook_log.created_at)
%td
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 4d8754afdd2..c37d8ac45b9 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -14,7 +14,7 @@
= hidden_field_tag :namespace_id, params[:namespace_id]
- namespace = Namespace.find(params[:namespace_id])
- toggle_text = "#{namespace.kind}: #{namespace.full_path}"
- = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' })
+ = dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select.dropdown-menu-align-right
= dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace")
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index ab4165c0bf2..42f92079d85 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -115,7 +115,7 @@
= f.label :new_namespace_id, "Namespace", class: 'control-label'
.col-sm-10
.dropdown
- = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id', show_any: 'false' }, { toggle_class: 'js-namespace-select large' })
+ = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' })
.dropdown-menu.dropdown-select
= dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace")
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 43cea1358cc..4f60be698e9 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -52,22 +52,23 @@
%br
- if @runners.any?
- .table-holder
- %table.table
- %thead
- %tr
- %th Type
- %th Runner token
- %th Description
- %th Version
- %th Projects
- %th Jobs
- %th Tags
- %th Last contact
- %th
+ .runners-content
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th Type
+ %th Runner token
+ %th Description
+ %th Version
+ %th Projects
+ %th Jobs
+ %th Tags
+ %th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc'))
+ %th
- - @runners.each do |runner|
- = render "admin/runners/runner", runner: runner
- = paginate @runners, theme: "gitlab"
+ - @runners.each do |runner|
+ = render "admin/runners/runner", runner: runner
+ = paginate @runners, theme: "gitlab"
- else
.nothing-here-block No runners found
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index df2bf27be9d..6d8fad0eb8d 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -99,7 +99,7 @@
%td.build-link
- if project
- = link_to ci_status_path(build.pipeline) do
+ = link_to pipeline_path(build.pipeline) do
%strong= build.pipeline.short_sha
%td.timestamp
diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml
index 39c7fb0eba2..35a3563dff1 100644
--- a/app/views/ci/status/_badge.html.haml
+++ b/app/views/ci/status/_badge.html.haml
@@ -5,9 +5,9 @@
- if link && status.has_details?
= link_to status.details_path, class: css_classes, title: title do
- = custom_icon(status.icon)
+ = sprite_icon(status.icon)
= status.text
- else
%span{ class: css_classes, title: title }
- = custom_icon(status.icon)
+ = sprite_icon(status.icon)
= status.text
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index dcfb7f0c32d..c5b4439e273 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -7,13 +7,13 @@
- if status.has_details?
= 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{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name
- else
.menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } }
- %span{ class: klass }= custom_icon(status.icon)
+ %span{ class: klass }= sprite_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, container: 'body' } do
- = custom_icon(status.action_icon)
+ = 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
+ = sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}")
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 7981daa0705..cebdbab4e74 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -1,13 +1,13 @@
.top-area
%ul.nav-links
= nav_link(page: dashboard_groups_path) do
- = link_to dashboard_groups_path, title: 'Your groups' do
+ = link_to dashboard_groups_path, title: _("Your groups") do
Your groups
= nav_link(page: explore_groups_path) do
- = link_to explore_groups_path, title: 'Explore public groups' do
+ = link_to explore_groups_path, title: _("Explore public groups") do
Explore public groups
.nav-controls
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'
- if current_user.can_create_group?
- = link_to "New group", new_group_path, class: "btn btn-new"
+ = link_to _("New group"), new_group_path, class: "btn btn-new"
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index fd2ba9ac1ca..9038c4fbebd 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -6,13 +6,13 @@
.fade-right= icon('angle-right')
%ul.nav-links.scrolling-tabs
= nav_link(page: [dashboard_projects_path, root_path]) do
- = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
+ = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects
= nav_link(page: starred_dashboard_projects_path) do
- = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do
+ = link_to starred_dashboard_projects_path, data: {placement: 'right'} do
Starred projects
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
- = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do
+ = link_to explore_root_path, data: {placement: 'right'} do
Explore projects
.nav-controls
diff --git a/app/views/dashboard/groups/_empty_state.html.haml b/app/views/dashboard/groups/_empty_state.html.haml
deleted file mode 100644
index f5222fe631e..00000000000
--- a/app/views/dashboard/groups/_empty_state.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-.groups-empty-state
- = custom_icon("icon_empty_groups")
-
- .text-content
- %h4 A group is a collection of several projects.
- %p If you organize your projects under a group, it works like a folder.
- %p You can manage your group member’s permissions and access to each project in the group.
diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml
index 168e6272d8e..601b6a8b1a7 100644
--- a/app/views/dashboard/groups/_groups.html.haml
+++ b/app/views/dashboard/groups/_groups.html.haml
@@ -1,9 +1,2 @@
.js-groups-list-holder
- #dashboard-group-app{ data: { endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path } }
- .groups-list-loading
- = icon('spinner spin', 'v-show' => 'isLoading')
- %template{ 'v-if' => '!isLoading && isEmpty' }
- %div{ 'v-cloak' => true }
- = render 'empty_state'
- %template{ 'v-else-if' => '!isLoading && !isEmpty' }
- %groups-component{ ':groups' => 'state.groups', ':page-info' => 'state.pageInfo' }
+ #js-groups-tree{ data: { hide_projects: 'true', endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } }
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index 1cea8182733..25bf08c6c12 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -6,7 +6,7 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'groups'
-- if @groups.empty?
- = render 'empty_state'
+- if params[:filter].blank? && @groups.empty?
+ = render 'shared/groups/empty_state'
- else
= render 'groups'
diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
index 57544559824..573a4b93d67 100644
--- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
@@ -1,33 +1,41 @@
-.blank-state
- .blank-state-icon
- = custom_icon("add_new_user", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Add user
- %p.blank-state-text
- Add your team members and others to GitLab.
- = link_to new_admin_user_path, class: "btn btn-new" do
- New user
+.blank-state-row
+ = link_to new_project_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_project", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a project
+ %p.blank-state-text
+ Projects are where you store your code, access issues, wiki and other features of GitLab.
-.blank-state
- .blank-state-icon
- = custom_icon("configure_server", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Configure GitLab
- %p.blank-state-text
- Make adjustments to how your GitLab instance is set up.
- = link_to admin_root_path, class: "btn btn-new" do
- Configure
+ - if current_user.can_create_group?
+ = link_to admin_root_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_group", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a group
+ %p.blank-state-text
+ Groups are a great way to organize projects and people.
-- if current_user.can_create_group?
- .blank-state
- .blank-state-icon
- = custom_icon("add_new_group", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Create a group
- %p.blank-state-text
- Groups are a great way to organize projects and people.
- = link_to new_group_path, class: "btn btn-new" do
- New group
+ = link_to new_admin_user_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_user", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Add people
+ %p.blank-state-text
+ Add your team members and others to GitLab.
+
+ = link_to admin_root_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("configure_server", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Configure GitLab
+ %p.blank-state-text
+ Make adjustments to how your GitLab instance is set up.
diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml
index a93a3415ee1..8d5bddbb288 100644
--- a/app/views/dashboard/projects/_blank_state_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml
@@ -1,48 +1,58 @@
- public_project_count = ProjectsFinder.new(current_user: current_user).execute.count
-- if current_user.can_create_group?
- .blank-state
- .blank-state-icon
- = custom_icon("add_new_group", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Create a group for several dependent projects.
- %p.blank-state-text
- Groups are the best way to manage projects and members.
- = link_to new_group_path, class: "btn btn-new" do
- New group
+.blank-state-row
+ - if current_user.can_create_project?
+ = link_to new_project_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_project", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a project
+ %p.blank-state-text
+ Projects are where you store your code, access issues, wiki and other features of GitLab.
+ - else
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_project", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a project
+ %p.blank-state-text
+ If you are added to a project, it will be displayed here.
-.blank-state
- .blank-state-icon
- = custom_icon("add_new_project", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Create a project
- %p.blank-state-text
- - if current_user.can_create_project?
- You don't have access to any projects right now.
- You can create up to
- %strong= number_with_delimiter(current_user.projects_limit)
- = succeed "." do
- = "project".pluralize(current_user.projects_limit)
- - else
- If you are added to a project, it will be displayed here.
- - if current_user.can_create_project?
- = link_to new_project_path, class: "btn btn-new" do
- New project
+ - if current_user.can_create_group?
+ = link_to new_group_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("add_new_group", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Create a group
+ %p.blank-state-text
+ Groups are the best way to manage projects and members.
-- if public_project_count > 0
- .blank-state
- .blank-state-icon
- = custom_icon("globe", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Explore public projects
- %p.blank-state-text
- There are
- = number_with_delimiter(public_project_count)
- public projects on this server.
- Public projects are an easy way to allow
- everyone to have read-only access.
- = link_to trending_explore_projects_path, class: "btn btn-new" do
- Browse projects
+ - if public_project_count > 0
+ = link_to trending_explore_projects_path, class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("globe", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Explore public projects
+ %p.blank-state-text
+ There are
+ = number_with_delimiter(public_project_count)
+ public projects on this server.
+ Public projects are an easy way to allow
+ everyone to have read-only access.
+
+ = link_to "https://docs.gitlab.com/", class: "blank-state-link" do
+ .blank-state
+ .blank-state-icon
+ = custom_icon("lightbulb", size: 50)
+ .blank-state-body
+ %h3.blank-state-title
+ Learn more about GitLab
+ %p.blank-state-text
+ Take a look at the documentation to discover all of GitLab's capabilities.
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index ad3fac6d164..18a82feb189 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,12 +1,13 @@
-.row.blank-state-parent-container
+.blank-state-parent-container
.section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" }
.container.section-body
- .blank-state.blank-state-welcome
- %h2.blank-state-welcome-title
- Welcome to GitLab
- %p.blank-state-text
- Code, test, and deploy together
- - if current_user.admin?
- = render "blank_state_admin_welcome"
- - else
- = render "blank_state_welcome"
+ .row
+ .blank-state-welcome
+ %h2.blank-state-welcome-title
+ Welcome to GitLab
+ %p.blank-state-text
+ Code, test, and deploy together
+ - if current_user.admin?
+ = render "blank_state_admin_welcome"
+ - else
+ = render "blank_state_welcome"
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 38fd053ae65..efe1fb99efc 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -36,7 +36,7 @@
.todo-body
.todo-note
.md
- = event_note(todo.body, project: todo.project)
+ = first_line_in_markdown(todo, :body, 150, project: todo.project)
- if todo.pending?
.todo-actions
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index f62a0cd681e..a5686002328 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -8,7 +8,7 @@
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do
%span
- To do
+ Todos
%span.badge
= number_with_delimiter(todos_pending_count)
%li.todos-done{ class: active_when(params[:state] == 'done') }>
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index dd61dcf2a7b..34d4293bd45 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -6,15 +6,15 @@
- else
= render 'devise/shared/tabs_normal'
.tab-content
- - if password_authentication_enabled? || ldap_enabled? || crowd_enabled?
+ - if password_authentication_enabled_for_web? || ldap_enabled? || crowd_enabled?
= render 'devise/shared/signin_box'
-# Signup only makes sense if you can also sign-in
- - if password_authentication_enabled? && signup_enabled?
+ - if allow_signup?
= render 'devise/shared/signup_box'
-# Show a message if none of the mechanisms above are enabled
- - if !password_authentication_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
+ - if !password_authentication_enabled_for_web? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
%div
No authentication methods configured.
diff --git a/app/views/devise/shared/_links.erb b/app/views/devise/shared/_links.erb
index 49e99e25c1d..6e1cc244f26 100644
--- a/app/views/devise/shared/_links.erb
+++ b/app/views/devise/shared/_links.erb
@@ -2,7 +2,7 @@
<%= link_to "Sign in", new_session_path(resource_name), class: "btn" %><br />
<% end -%>
-<%- if devise_mapping.registerable? && controller_name != 'registrations' && gitlab_config.signup_enabled %>
+<%- if devise_mapping.registerable? && controller_name != 'registrations' && allow_signup? %>
<%= link_to "Sign up", new_registration_path(resource_name) %><br />
<% end -%>
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 3b06008febe..6087f4a0b37 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -7,12 +7,12 @@
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && !crowd_enabled?) }
.login-body
= render 'devise/sessions/new_ldap', server: server
- - if password_authentication_enabled?
+ - if password_authentication_enabled_for_web?
.login-box.tab-pane{ id: 'ldap-standard', role: 'tabpanel' }
.login-body
= render 'devise/sessions/new_base'
-- elsif password_authentication_enabled?
+- elsif password_authentication_enabled_for_web?
.login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
.login-body
= render 'devise/sessions/new_base'
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index 6d0243a325d..94f19ccd44c 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -5,9 +5,9 @@
- @ldap_servers.each_with_index do |server, i|
%li{ class: active_when(i.zero? && !crowd_enabled?) }
= link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
- - if password_authentication_enabled?
+ - if password_authentication_enabled_for_web?
%li
= link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
- - if password_authentication_enabled? && signup_enabled?
+ - if allow_signup?
%li
= link_to 'Register', '#register-pane', 'data-toggle' => 'tab'
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
index 212856c0676..1ba6d390875 100644
--- a/app/views/devise/shared/_tabs_normal.html.haml
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -1,6 +1,6 @@
%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' }
%a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
- - if password_authentication_enabled? && signup_enabled?
+ - if allow_signup?
%li{ role: 'presentation' }
%a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register
diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml
index 52279d0a870..4b6c4581eb3 100644
--- a/app/views/discussions/_diff_discussion.html.haml
+++ b/app/views/discussions/_diff_discussion.html.haml
@@ -7,4 +7,4 @@
%td.notes_line{ colspan: 2 }
%td.notes_content
.content{ class: ('hide' unless expanded) }
- = render partial: "discussions/notes", collection: discussions, as: :discussion
+ = render partial: "discussions/notes", collection: discussions, as: :discussion, locals: { disable_collapse_class: true }
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 636d06cab53..f9bfc01f213 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -24,4 +24,4 @@
= render partial: "projects/diffs/#{partial}", locals: { diff_file: diff_file, position: discussion.position.to_json, click_to_comment: false }
.note-container
- = render partial: "discussions/notes", locals: { discussion: discussion, show_toggle: false, show_image_comment_badge: true, disable_collapse: true }
+ = render partial: "discussions/notes", locals: { discussion: discussion, show_toggle: false, show_image_comment_badge: true, disable_collapse_class: true }
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 578e751ab47..0f03163a2e8 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -44,4 +44,4 @@
= render "discussions/diff_with_notes", discussion: discussion
- else
.panel.panel-default
- = render "discussions/notes", discussion: discussion
+ = render partial: "discussions/notes", locals: { discussion: discussion, disable_collapse_class: true }
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index 9efcfef690f..1cc227428e9 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -1,5 +1,5 @@
-- disable_collapse = local_assigns.fetch(:disable_collapse, false)
-- collapsed_class = 'collapsed' if discussion.resolved? && !disable_collapse
+- disable_collapse_class = local_assigns.fetch(:disable_collapse_class, false)
+- collapsed_class = 'collapsed' if discussion.resolved? && !disable_collapse_class
- badge_counter = discussion_counter + 1 if local_assigns[:discussion_counter]
- show_toggle = local_assigns.fetch(:show_toggle, true)
- show_image_comment_badge = local_assigns.fetch(:show_image_comment_badge, false)
diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml
index 253cd336882..079d9083dff 100644
--- a/app/views/discussions/_parallel_diff_discussion.html.haml
+++ b/app/views/discussions/_parallel_diff_discussion.html.haml
@@ -4,7 +4,7 @@
%td.notes_line.old
%td.notes_content.parallel.old
.content{ class: ('hide' unless discussions_left.any?(&:expanded?)) }
- = render partial: "discussions/notes", collection: discussions_left, as: :discussion, line_type: 'old'
+ = render partial: "discussions/notes", collection: discussions_left, as: :discussion, line_type: 'old', locals: { disable_collapse_class: true }
- else
%td.notes_line.old= ("")
%td.notes_content.parallel.old
@@ -14,7 +14,7 @@
%td.notes_line.new
%td.notes_content.parallel.new
.content{ class: ('hide' unless discussions_right.any?(&:expanded?)) }
- = render partial: "discussions/notes", collection: discussions_right, as: :discussion, line_type: 'new'
+ = render partial: "discussions/notes", collection: discussions_right, as: :discussion, line_type: 'new', locals: { disable_collapse_class: true }
- else
%td.notes_line.new= ("")
%td.notes_content.parallel.new
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index b3313c7c985..cf0e0de1ca4 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f|
+= form_for application, url: doorkeeper_submit_path(application), html: { role: 'form', class: 'doorkeeper-app-form' } do |f|
= form_errors(application)
.form-group
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 8ba88906714..6d9c6b5572a 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -1,5 +1,5 @@
%main{ :role => "main" }
- .modal-no-backdrop
+ .modal-no-backdrop.modal-doorkeepr-auth
.modal-content
.modal-header
%h3.page-title
@@ -16,14 +16,26 @@
%strong= @pre_auth.client.name
will allow them to interact with GitLab as an admin as well. Proceed with caution.
%p
- You are about to authorize
+ An application called
= link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
- to use your account.
- - if @pre_auth.scopes
+ is requesting access to your GitLab account.
+
+ - auth_app_owner = @pre_auth.client.application.owner
+ - if auth_app_owner
+ This application was created by
+ = succeed "." do
+ = link_to auth_app_owner.name, user_path(auth_app_owner)
+
+ Please note that this application is not provided by GitLab and you should verify its authenticity before
+ allowing access.
+ - if @pre_auth.scopes
+ %p
This application will be able to:
%ul
- @pre_auth.scopes.each do |scope|
- %li= t scope, scope: [:doorkeeper, :scopes]
+ %li
+ %strong= t scope, scope: [:doorkeeper, :scopes]
+ .scope-description= t scope, scope: [:doorkeeper, :scope_desc]
.form-actions.text-right
= form_tag oauth_authorization_path, method: :delete, class: 'inline' do
= hidden_field_tag :client_id, @pre_auth.client.uid
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index e2aec532a9d..38741fe6662 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -5,7 +5,12 @@ xml.entry do
xml.link href: event_feed_url(event)
xml.title truncate(event_feed_title(event), length: 80)
xml.updated event.updated_at.xmlschema
- xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
+
+ # We're deliberately re-using "event.author" here since this data is
+ # eager-loaded. This allows us to re-use the user object's Email address,
+ # instead of having to run additional queries to figure out what Email to use
+ # for the avatar.
+ xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author))
xml.author do
xml.username event.author_username
diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml
index 6fa2f9bd4db..7e264eb5575 100644
--- a/app/views/events/_event_note.atom.haml
+++ b/app/views/events/_event_note.atom.haml
@@ -1,2 +1,2 @@
%div{ xmlns: "http://www.w3.org/1999/xhtml" }
- = markdown(note.note, pipeline: :atom, project: note.project, author: note.author)
+ = markdown_field(note, :note)
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index df4b9562215..de6383e4097 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -10,7 +10,7 @@
.event-body
.event-note
.md
- = event_note(event.target.note, project: event.project)
+ = first_line_in_markdown(event.target, :note, 150, project: event.project)
- note = event.target
- if note.attachment.url
- if note.attachment.image?
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 53ebdd6d2ff..9a763887b30 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -19,8 +19,7 @@
- create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user)
- if event.commits_count > 1
%li.commits-stat
- - if event.commits_count > 2
- %span ... and #{event.commits_count - 2} more commits.
+ %span ... and #{pluralize(event.commits_count - 1, 'more commit')}.
- if event.md_ref?
- from = event.commit_from
diff --git a/app/views/explore/groups/_groups.html.haml b/app/views/explore/groups/_groups.html.haml
index 794c6d1d170..91149498248 100644
--- a/app/views/explore/groups/_groups.html.haml
+++ b/app/views/explore/groups/_groups.html.haml
@@ -1,6 +1,2 @@
.js-groups-list-holder
- %ul.content-list
- - @groups.each do |group|
- = render 'shared/groups/group', group: group
-
- = paginate @groups, theme: 'gitlab'
+ #js-groups-tree{ data: { hide_projects: 'true', endpoint: explore_groups_path(format: :json), path: explore_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } }
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 2651ef37e67..86abdf547cc 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -2,6 +2,9 @@
- page_title "Groups"
- header_title "Groups", dashboard_groups_path
+= webpack_bundle_tag 'common_vue'
+= webpack_bundle_tag 'groups'
+
- if current_user
= render 'dashboard/groups_head'
- else
@@ -17,7 +20,7 @@
%p Below you will find all the groups that are public.
%p You can easily contribute to them by requesting to join these groups.
-- if @groups.present?
- = render 'groups'
-- else
+- if params[:filter].blank? && @groups.empty?
.nothing-here-block No public groups
+- else
+ = render 'groups'
diff --git a/app/views/groups/_children.html.haml b/app/views/groups/_children.html.haml
new file mode 100644
index 00000000000..3afb6b2f849
--- /dev/null
+++ b/app/views/groups/_children.html.haml
@@ -0,0 +1,5 @@
+= webpack_bundle_tag 'common_vue'
+= webpack_bundle_tag 'groups'
+
+.js-groups-list-holder
+ #js-groups-tree{ data: { hide_projects: 'false', group_id: group.id, endpoint: group_children_path(group, format: :json), path: group_path(group), form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } }
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index 181c7bee702..a0760c2073b 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -1,7 +1,7 @@
.group-home-panel.text-center
%div{ class: container_class }
.avatar-container.s70.group-avatar
- = image_tag group_icon(@group), class: "avatar s70 avatar-tile"
+ = group_icon(@group, class: "avatar s70 avatar-tile")
%h1.group-title
= @group.name
%span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
diff --git a/app/views/groups/_show_nav.html.haml b/app/views/groups/_show_nav.html.haml
deleted file mode 100644
index 35b75bc0923..00000000000
--- a/app/views/groups/_show_nav.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-%ul.nav-links
- = nav_link(page: group_path(@group)) do
- = link_to group_path(@group) do
- Projects
- - if Group.supports_nested_groups?
- = nav_link(page: subgroups_group_path(@group)) do
- = link_to subgroups_group_path(@group) do
- Subgroups
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 15606dd30fd..16038ef2f79 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -10,7 +10,7 @@
.form-group
.col-sm-offset-2.col-sm-10
.avatar-container.s160
- = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160'
+ = group_icon(@group, alt: '', class: 'avatar group-avatar s160')
%p.light
- if @group.avatar?
You can change your group avatar here
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 07e64d9aeaf..00909982d59 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -19,13 +19,6 @@
= render 'shared/issuable/search_bar', type: :issues
- .row-content-block.second-block
- Only issues from the
- %strong= @group.name
- group are listed here.
- - if current_user
- To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
-
= render 'shared/issues'
- else
= render 'shared/empty_states/issues', project_select_button: true
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index e56dc1fb9c2..694292aa7c1 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -15,11 +15,4 @@
= render 'shared/issuable/search_bar', type: :merge_requests
- .row-content-block.second-block
- Only merge requests from
- %strong= @group.name
- group are listed here.
- - if current_user
- To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
-
= render 'shared/merge_requests'
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index 7f450cd9a93..a1be0d3220a 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -10,8 +10,8 @@
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
- = render layout: 'projects/md_preview', locals: { url: '' } do
- = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
+ = render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do
+ = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...', supports_autocomplete: false
.clearfix
.error-alert
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index cb4fc69d5b8..f5f621507b8 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -4,6 +4,7 @@
= render 'shared/milestones_filter', counts: @milestone_states
.nav-controls
+ = render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestones, @group)
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new"
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index eca7fb9ddb1..d758e314d41 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -1,6 +1,5 @@
- breadcrumb_title "Milestones"
- page_title "Milestones"
-- header_title group_title(@group, "Milestones", group_milestones_path(@group))
%h3.page-title
New Milestone
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 3ca63f9c3e0..7f9486d08d9 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,5 +1,6 @@
- @no_container = true
- breadcrumb_title "Details"
+- can_create_subgroups = can?(current_user, :create_subgroup, @group)
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
@@ -7,13 +8,38 @@
= render 'groups/home_panel'
.groups-header{ class: container_class }
- .top-area
- = render 'groups/show_nav'
- .nav-controls
- = render 'shared/projects/search_form'
- = render 'shared/projects/dropdown'
+ .group-nav-container
+ .nav-controls.clearfix
+ = render "shared/groups/search_form"
+ = render "shared/groups/dropdown", show_archive_options: true
- if can? current_user, :create_projects, @group
- = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
- New Project
+ - new_project_label = _("New project")
+ - new_subgroup_label = _("New subgroup")
+ - if can_create_subgroups
+ .btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } }
+ %input.btn.btn-success.dropdown-primary.js-new-group-child{ type: "button", value: new_project_label, data: { action: "new-project" } }
+ %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown" } }
+ = icon("caret-down", class: "dropdown-btn-icon")
+ %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } }
+ %li.droplab-item-selected{ role: "button", data: { value: "new-project", text: new_project_label } }
+ .menu-item
+ .icon-container
+ = icon("check", class: "list-item-checkmark")
+ .description
+ %strong= new_project_label
+ %span= s_("GroupsTree|Create a project in this group.")
+ %li.divider.droplap-item-ignore
+ %li{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } }
+ .menu-item
+ .icon-container
+ = icon("check", class: "list-item-checkmark")
+ .description
+ %strong= new_subgroup_label
+ %span= s_("GroupsTree|Create a subgroup in this group.")
+ - else
+ = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success"
- = render "projects", projects: @projects
+ - if params[:filter].blank? && !@has_children
+ = render "shared/groups/empty_state"
+ - else
+ = render "children", children: @children, group: @group
diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml
deleted file mode 100644
index 869b3b243c6..00000000000
--- a/app/views/groups/subgroups.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-- breadcrumb_title "Details"
-- @no_container = true
-
-= render 'groups/home_panel'
-
-.groups-header{ class: container_class }
- .top-area
- = render 'groups/show_nav'
- .nav-controls
- = form_tag request.path, method: :get do |f|
- = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name', class: 'form-control', spellcheck: false
- - if can?(current_user, :create_subgroup, @group)
- = link_to new_group_path(parent_id: @group.id), class: 'btn btn-new pull-right' do
- New Subgroup
-
- - if @nested_groups.present?
- %ul.content-list
- = render partial: 'shared/groups/group', collection: @nested_groups, locals: { full_name: false }
- - else
- .nothing-here-block
- There are no subgroups to show.
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index b18b3dd5766..29b23ae2e52 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -17,10 +17,6 @@
%th Global Shortcuts
%tr
%td.shortcut
- .key n
- %td Main Navigation
- %tr
- %td.shortcut
.key s
%td Focus Search
%tr
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index c25eae63eec..021de4f0caf 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -11,6 +11,7 @@
%span= Gitlab::VERSION
%small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION)
= version_status_badge
+
%p.slead
GitLab is open source software to collaborate on code.
%br
@@ -23,11 +24,12 @@
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}.
+ %p= link_to 'Check the current instance configuration ', help_instance_configuration_url
%hr
.row.prepend-top-default
.col-md-8
- .documentation-index
+ .documentation-index.wiki
= markdown(@help_index)
.col-md-4
.panel.panel-default
diff --git a/app/views/help/instance_configuration.html.haml b/app/views/help/instance_configuration.html.haml
new file mode 100644
index 00000000000..f09e3825a4b
--- /dev/null
+++ b/app/views/help/instance_configuration.html.haml
@@ -0,0 +1,17 @@
+- page_title 'Instance Configuration'
+.wiki.documentation
+ %h1 Instance Configuration
+
+ %p
+ In this page you will find information about the settings that are used in your current instance.
+
+ = render 'help/instance_configuration/ssh_info'
+ = render 'help/instance_configuration/gitlab_pages'
+ = render 'help/instance_configuration/gitlab_ci'
+ %p
+ %strong Table of contents
+
+ %ul
+ = content_for :table_content
+
+ = content_for :settings_content
diff --git a/app/views/help/instance_configuration/_gitlab_ci.html.haml b/app/views/help/instance_configuration/_gitlab_ci.html.haml
new file mode 100644
index 00000000000..7fa8bd086d4
--- /dev/null
+++ b/app/views/help/instance_configuration/_gitlab_ci.html.haml
@@ -0,0 +1,24 @@
+- content_for :table_content do
+ %li= link_to 'GitLab CI', '#gitlab-ci'
+
+- content_for :settings_content do
+ %h2#gitlab-ci
+ GitLab CI
+
+ %p
+ Below are the current settings regarding
+ = succeed('.') { link_to('GitLab CI', 'https://about.gitlab.com/gitlab-ci', target: '_blank') }
+
+ .table-responsive
+ %table
+ %thead
+ %tr
+ %th Setting
+ %th= instance_configuration_host(@instance_configuration.settings[:host])
+ %th Default
+ %tbody
+ %tr
+ - artifacts_size = @instance_configuration.settings[:gitlab_ci][:artifacts_max_size]
+ %td Artifacts maximum size
+ %td= instance_configuration_human_size_cell(artifacts_size[:value])
+ %td= instance_configuration_human_size_cell(artifacts_size[:default])
diff --git a/app/views/help/instance_configuration/_gitlab_pages.html.haml b/app/views/help/instance_configuration/_gitlab_pages.html.haml
new file mode 100644
index 00000000000..bdd77730dcc
--- /dev/null
+++ b/app/views/help/instance_configuration/_gitlab_pages.html.haml
@@ -0,0 +1,35 @@
+- gitlab_pages = @instance_configuration.settings[:gitlab_pages]
+- content_for :table_content do
+ %li= link_to 'GitLab Pages', '#gitlab-pages'
+
+- content_for :settings_content do
+ %h2#gitlab-pages
+ GitLab Pages
+
+ %p
+ Below are the settings for
+ = succeed('.') { link_to('Gitlab Pages', gitlab_pages[:url], target: '_blank') }
+ .table-responsive
+ %table
+ %thead
+ %tr
+ %th Setting
+ %th= instance_configuration_host(@instance_configuration.settings[:host])
+ %tbody
+ %tr
+ %td Domain Name
+ %td
+ %code= instance_configuration_cell_html(gitlab_pages[:host])
+ %tr
+ %td IP Address
+ %td
+ %code= instance_configuration_cell_html(gitlab_pages[:ip_address])
+ %tr
+ %td Port
+ %td
+ %code= instance_configuration_cell_html(gitlab_pages[:port])
+ %br
+
+ %p
+ The maximum size of your Pages site is regulated by the artifacts maximum
+ size which is part of #{succeed('.') { link_to('GitLab CI', '#gitlab-ci') }}
diff --git a/app/views/help/instance_configuration/_ssh_info.html.haml b/app/views/help/instance_configuration/_ssh_info.html.haml
new file mode 100644
index 00000000000..987cc61b3f6
--- /dev/null
+++ b/app/views/help/instance_configuration/_ssh_info.html.haml
@@ -0,0 +1,27 @@
+- ssh_info = @instance_configuration.settings[:ssh_algorithms_hashes]
+- if ssh_info.any?
+ - content_for :table_content do
+ %li= link_to 'SSH host keys fingerprints', '#ssh-host-keys-fingerprints'
+
+ - content_for :settings_content do
+ %h2#ssh-host-keys-fingerprints
+ SSH host keys fingerprints
+
+ %p
+ Below are the fingerprints for the current instance SSH host keys.
+
+ .table-responsive
+ %table
+ %thead
+ %tr
+ %th Algorithm
+ %th MD5
+ %th SHA256
+ %tbody
+ - ssh_info.each do |algorithm|
+ %tr
+ %td= algorithm[:name]
+ %td
+ %code= instance_configuration_cell_html(algorithm[:md5])
+ %td
+ %code= instance_configuration_cell_html(algorithm[:sha256])
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index baa8036de10..05ddd0ec733 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,10 +1,8 @@
.flash-container.flash-container-page
- - if alert
- .flash-alert
- %div{ class: (container_class) }
- %span= alert
-
- - elsif notice
- .flash-notice
- %div{ class: (container_class) }
- %span= notice
+ -# We currently only support `alert`, `notice`, `success`
+ - flash.each do |key, value|
+ -# Don't show a flash message if the message is nil
+ - if value
+ %div{ class: "flash-#{key}" }
+ %div{ class: (container_class) }
+ %span= value
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index f1b32274664..1597621fa78 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -37,7 +37,7 @@
- if content_for?(:library_javascripts)
= yield :library_javascripts
- = javascript_include_tag asset_path("locale/#{I18n.locale.to_s || I18n.default_locale.to_s}/app.js") unless I18n.locale == :en
+ = javascript_include_tag locale_path unless I18n.locale == :en
= webpack_bundle_tag "webpack_runtime"
= webpack_bundle_tag "common"
= webpack_bundle_tag "main"
diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml
index 983ed22a506..b50537438a9 100644
--- a/app/views/layouts/_mailer.html.haml
+++ b/app/views/layouts/_mailer.html.haml
@@ -10,6 +10,10 @@
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; }
+ .hidden {
+ display: none !important;
+ visibility: hidden !important;
+ }
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 29387d6627e..4c5cc249159 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -5,7 +5,7 @@
- if @group && @group.persisted? && @group.path
- group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) }
- if @project && @project.persisted?
- - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project) }
+ - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project), issues_disabled: !@project.issues_enabled? }
.search.search-form{ class: "#{'has-location-badge' if label.present?}" }
= form_tag search_path, method: :get, class: 'navbar-form' do |f|
.search-input-container
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 5ff6ac5fc00..e2407f6a428 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -7,7 +7,7 @@
= link_to root_path, title: 'Dashboard', id: 'logo' do
= brand_header_logo
%span.logo-text.hidden-xs
- = render 'shared/logo_type.svg'
+ = brand_header_logo_type
- if current_user
= render "layouts/nav/dashboard"
@@ -61,7 +61,7 @@
= link_to "Help", help_path
%li.divider
%li
- = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
+ = link_to "Sign out", destroy_user_session_path, 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
diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
index 610ff9001f7..ad0d51d28f9 100644
--- a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
+++ b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
@@ -4,7 +4,7 @@
%li.dropdown
%button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { toggle: "dropdown", container: "body" }, "aria-label": button_tooltip, title: button_tooltip }
= icon("ellipsis-h")
- = sprite_icon("angle-right", css_class: "breadcrumbs-list-angle")
+ = sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
.dropdown-menu
%ul
- @breadcrumb_dropdown_links[dropdown_location].each_with_index do |link, index|
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 8cba495f7e4..0bf318b0b66 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -6,7 +6,7 @@
.context-header
= link_to group_path(@group), title: @group.name do
.avatar-container.s40.group-avatar
- = image_tag group_icon(@group), class: "avatar s40 avatar-tile"
+ = group_icon(@group, class: "avatar s40 avatar-tile")
.sidebar-context-title
= @group.name
%ul.sidebar-top-level-items
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 458b5010d36..7e23f9c1f05 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -73,7 +73,7 @@
= link_to profile_emails_path do
%strong.fly-out-top-item-name
#{ _('Emails') }
- - unless current_user.ldap_user?
+ - if current_user.allow_password_authentication?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path do
.nav-icon-container
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 759d6ff68ea..66146e61263 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -272,6 +272,11 @@
= sprite_icon('users')
%span.nav-item-name
Members
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_project_members_path(@project) do
+ %strong.fly-out-top-item-name
+ #{ _('Members') }
= render 'shared/sidebar_toggle_button'
diff --git a/app/views/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml
index a80518f7986..3e36da31ea3 100644
--- a/app/views/notify/_note_email.html.haml
+++ b/app/views/notify/_note_email.html.haml
@@ -1,10 +1,15 @@
- discussion = @note.discussion if @note.part_of_discussion?
+- diff_discussion = discussion&.diff_discussion?
+- on_image = discussion.on_image? if diff_discussion
+
- if discussion
+ - phrase_end_char = on_image ? "." : ":"
+
%p.details
- = succeed ':' do
+ = succeed phrase_end_char do
= link_to @note.author_name, user_url(@note.author)
- - if discussion.diff_discussion?
+ - if diff_discussion
- if discussion.new_discussion?
started a new discussion
- else
@@ -21,7 +26,7 @@
%p.details
#{link_to @note.author_name, user_url(@note.author)} commented:
-- if discussion&.diff_discussion?
+- if diff_discussion && !on_image
= content_for :head do
= stylesheet_link_tag 'mailers/highlighted_diff_email'
diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml
index 6b9b42dcf37..00e1b5faae3 100644
--- a/app/views/notify/new_user_email.html.haml
+++ b/app/views/notify/new_user_email.html.haml
@@ -1,7 +1,7 @@
%p
Hi #{@user['name']}!
%p
- - if Gitlab.config.gitlab.signup_enabled
+ - if current_application_settings.allow_signup?
Your account has been created successfully.
- else
The Administrator created an account for you. Now you are a member of the company GitLab application.
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index b7a60938132..8eb3f2d5192 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -31,7 +31,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
= @pipeline.ref
@@ -42,7 +42,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= @pipeline.short_sha
@@ -60,7 +60,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
@@ -76,7 +76,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
@@ -100,7 +100,7 @@
triggered by
- if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
- %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 3f16885b8e3..574a8f2fa50 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -31,7 +31,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
= @pipeline.ref
@@ -42,7 +42,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= @pipeline.short_sha
@@ -60,7 +60,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
@@ -76,7 +76,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
@@ -100,7 +100,7 @@
triggered by
- if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
- %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name
diff --git a/app/views/peek/views/_gitaly.html.haml b/app/views/peek/views/_gitaly.html.haml
new file mode 100644
index 00000000000..a7d040d6821
--- /dev/null
+++ b/app/views/peek/views/_gitaly.html.haml
@@ -0,0 +1,7 @@
+- local_assigns.fetch(:view)
+
+%strong
+ %span{ data: { defer_to: "#{view.defer_key}-duration" } } ...
+ \/
+ %span{ data: { defer_to: "#{view.defer_key}-calls" } } ...
+ Gitaly
diff --git a/app/views/profiles/accounts/_reset_token.html.haml b/app/views/profiles/accounts/_reset_token.html.haml
deleted file mode 100644
index c31a4a8ecd4..00000000000
--- a/app/views/profiles/accounts/_reset_token.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- name = label.parameterize
-- attribute = name.underscore
-
-.reset-action
- %p.cgray
- = label_tag name, label, class: "label-light"
- = text_field_tag name, current_user.send(attribute), class: 'form-control', readonly: true, onclick: 'this.select()'
- %p.help-block
- = help_text
- .prepend-top-default
- = link_to button_label, [:reset, attribute, :profile], method: :put, data: { confirm: 'Are you sure?' }, class: 'btn btn-default private-token'
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 7f79168dfb3..ced58dffcdc 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -9,22 +9,6 @@
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
- Private Tokens
- %p
- Keep these tokens secret, anyone with access to them can interact with
- GitLab as if they were you.
- .col-lg-8.private-tokens-reset
- = render partial: 'reset_token', locals: { label: 'Private token', button_label: 'Reset private token', help_text: 'Your private token is used to access the API and Atom feeds without username/password authentication.' }
-
- = render partial: 'reset_token', locals: { label: 'RSS token', button_label: 'Reset RSS token', help_text: 'Your RSS token is used to create urls for personalized RSS feeds.' }
-
- - if incoming_email_token_enabled?
- = render partial: 'reset_token', locals: { label: 'Incoming email token', button_label: 'Reset incoming email token', help_text: 'Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.' }
-
-%hr
-.row.prepend-top-default
- .col-lg-4.profile-settings-sidebar
- %h4.prepend-top-0
Two-Factor Authentication
%p
Increase your account's security by enabling Two-Factor Authentication (2FA).
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 06bb72b9f0d..26c2e4c5936 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -30,3 +30,40 @@
= render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes
= render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens
+
+%hr
+.row.prepend-top-default
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ RSS token
+ %p
+ Your RSS token is used to authenticate you when your RSS reader loads a personalized RSS feed, and is included in your personal RSS feed URLs.
+ %p
+ It cannot be used to access any other data.
+ .col-lg-8.rss-token-reset
+ = label_tag :rss_token, 'RSS token', class: "label-light"
+ = text_field_tag :rss_token, current_user.rss_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+ %p.help-block
+ Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds as if they were you.
+ You should
+ = link_to 'reset it', [:reset, :rss_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS URLs currently in use will stop working.' }
+ if that ever happens.
+
+- if incoming_email_token_enabled?
+ %hr
+ .row.prepend-top-default
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ Incoming email token
+ %p
+ Your incoming email token is used to authenticate you when you create a new issue by email, and is included in your personal project-specific email addresses.
+ %p
+ It cannot be used to access any other data.
+ .col-lg-8.incoming-email-token-reset
+ = label_tag :incoming_email_token, 'Incoming email token', class: "label-light"
+ = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+ %p.help-block
+ Keep this token secret. Anyone who gets ahold of it can create issues as if they were you.
+ You should
+ = link_to 'reset it', [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: 'Are you sure? Any issue email addresses currently in use will stop working.' }
+ if that ever happens.
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 623d3bc91c6..c5b1897c492 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -3,7 +3,7 @@
- project = local_assigns.fetch(:project)
- expanded = Rails.env.test?
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Export project
@@ -11,7 +11,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
.bs-callout.bs-callout-info
%p.append-bottom-0
%p
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 619b632918e..1d644dda177 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,6 +1,5 @@
- empty_repo = @project.empty_repo?
- fork_network = @project.fork_network
-- forked_from_project = @project.forked_from_project || fork_network&.root_project
.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
.limit-container-width{ class: container_class }
.avatar-container.s70.project-avatar
@@ -16,13 +15,13 @@
- if @project.forked?
%p
- - if forked_from_project
+ - if @project.fork_source
#{ s_('ForkedFromProjectPath|Forked from') }
- = link_to project_path(forked_from_project) do
- = forked_from_project.full_name
+ = link_to project_path(@project.fork_source) do
+ = fork_source_name(@project)
- else
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
- = deleted_message % { project_name: fork_network.deleted_root_project_name }
+ = deleted_message % { project_name: fork_source_name(@project) }
.project-repo-buttons
.count-buttons
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 770608eddff..a9431cc4956 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -10,25 +10,23 @@
.md-area
.md-header
%ul.nav-links.clearfix
- %li.active
+ %li.md-header-tab.active
%a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 }
Write
- %li
+ %li.md-header-tab
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview
- %li.pull-right
- .toolbar-group
- = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" })
- = markdown_toolbar_button({ icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" })
- = markdown_toolbar_button({ icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
- = markdown_toolbar_button({ icon: "code fw", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
- = markdown_toolbar_button({ icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
- = markdown_toolbar_button({ icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
- = markdown_toolbar_button({ icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
- .toolbar-group
- %button.toolbar-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
- = icon("arrows-alt fw")
+ %li.md-header-toolbar
+ = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" })
+ = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" })
+ = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
+ = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
+ = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
+ = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
+ = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
+ %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
+ = sprite_icon("screen-full")
.md-write-holder
= yield
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
new file mode 100644
index 00000000000..a78a8e5d628
--- /dev/null
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -0,0 +1,41 @@
+- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
+
+.row{ id: project_name_id }
+ .form-group.project-path.col-sm-6
+ = f.label :namespace_id, class: 'label-light' do
+ %span
+ Project path
+ .input-group
+ - if current_user.can_select_namespace?
+ .input-group-addon
+ = root_url
+ = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1}
+
+ - else
+ .input-group-addon.static-namespace
+ #{user_url(current_user.username)}/
+ = f.hidden_field :namespace_id, value: current_user.namespace_id
+ .form-group.project-path.col-sm-6
+ = f.label :path, class: 'label-light' do
+ %span
+ Project name
+ = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true
+- if current_user.can_create_group?
+ .help-block
+ Want to house several dependent projects under the same namespace?
+ = link_to "Create a group", new_group_path
+
+.form-group
+ = f.label :description, class: 'label-light' do
+ Project description
+ %span (optional)
+ = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250
+
+.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' }
+ = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
+
+= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
+= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml
index 5638b7da1b0..d50175727be 100644
--- a/app/views/projects/_project_templates.html.haml
+++ b/app/views/projects/_project_templates.html.haml
@@ -1,10 +1,24 @@
-.project-templates-buttons.import-buttons{ data: { toggle: "buttons" } }
- .btn.blank-option.active
- %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: "blank", checked: "true", value: "" }
- = icon('file-o', class: 'btn-template-icon')
- Blank
+.project-templates-buttons.import-buttons
- Gitlab::ProjectTemplate.all.each do |template|
- .btn
- %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name }
+ .template-option
= custom_icon(template.logo)
- = template.title
+ .template-title= template.title
+ .template-description= template.description
+ %label.btn.btn-success.template-button.choose-template.append-right-10{ for: template.name }
+ %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name }
+ %span Use template
+ %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' } Preview
+
+ .project-fields-form
+ .form-group
+ %label.label-light
+ Template
+ .input-group.template-input-group
+ .input-group-addon
+ .selected-icon
+ - Gitlab::ProjectTemplate.all.each do |template|
+ = custom_icon(template.logo)
+ .selected-template
+ %button.btn.btn-default.change-template{ type: "button" } Change template
+
+ = render 'new_project_fields', f: f, project_name_id: "template-project-name"
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
new file mode 100644
index 00000000000..44aa9eb3826
--- /dev/null
+++ b/app/views/projects/_readme.html.haml
@@ -0,0 +1,23 @@
+- if (readme = @repository.readme) && readme.rich_viewer
+ %article.file-holder.readme-holder{ id: 'readme', class: ("limited-width-container" unless fluid_layout) }
+ .js-file-title.file-title
+ = blob_icon readme.mode, readme.name
+ = link_to project_blob_path(@project, tree_join(@ref, readme.path)) do
+ %strong
+ = readme.name
+ = render 'projects/blob/viewer', viewer: readme.rich_viewer, viewer_url: namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, readme.path), viewer: :rich, format: :json)
+
+- else
+ .row-content-block.second-block.center
+ %h3.page-title
+ This project does not have a README yet
+ - if can?(current_user, :push_code, @project)
+ %p
+ A
+ %code README
+ file contains information about other files in a repository and is commonly
+ distributed with computer software, forming part of its documentation.
+ %p
+ We recommend you to
+ = link_to "add a README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link'
+ file to the repository and GitLab will render it here instead of this message.
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 4b344b2edb9..7777f55ddd7 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -1,6 +1,6 @@
- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create'
-.file-holder.file.append-bottom-default
+.file-holder-bottom-radius.file-holder.file.append-bottom-default
.js-file-title.file-title.clearfix{ data: { current_action: action } }
.editor-ref
= icon('code-fork')
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 0be15cc179f..281363d2e01 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -2,7 +2,7 @@
.js-file-title.file-title-flex-parent
= render 'projects/blob/header_content', blob: blob
- .file-actions.hidden-xs
+ .file-actions
= render 'projects/blob/viewer_switcher', blob: blob unless blame
.btn-group{ role: "group" }<
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 49101d1efa4..6e02ae6c9cc 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -1,3 +1,4 @@
+- merged = local_assigns.fetch(:merged, false)
- commit = @repository.commit(branch.dereferenced_target)
- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0
- diverging_commit_counts = @repository.diverging_commit_counts(branch)
@@ -12,7 +13,7 @@
&nbsp;
- if branch.name == @repository.root_ref
%span.label.label-primary default
- - elsif @repository.merged_to_root_ref? branch.name
+ - elsif merged
%span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
= s_('Branches|merged')
@@ -47,7 +48,7 @@
target: "#modal-delete-branch",
delete_path: project_branch_path(@project, branch.name),
branch_name: branch.name,
- is_merged: ("true" if @repository.merged_to_root_ref?(branch.name)) } }
+ is_merged: ("true" if merged) } }
= icon("trash-o")
- else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 7d9645d79e6..fb770764364 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -38,7 +38,7 @@
- if @branches.any?
%ul.content-list.all-branches
- @branches.each do |branch|
- = render "projects/branches/branch", branch: branch
+ = render "projects/branches/branch", branch: branch, merged: @merged_branch_names.include?(branch.name)
= paginate @branches, theme: 'gitlab'
- else
.nothing-here-block
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index c82ae35a685..0a54c736761 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,10 +1,10 @@
- if current_user
= link_to toggle_star_project_path(@project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do
- if current_user.starred?(@project)
- = icon('star')
+ = sprite_icon('star')
%span.starred= _('Unstar')
- else
- = icon('star-o')
+ = sprite_icon('star-o')
%span= s_('StarProject|Star')
.count-with-arrow
%span.arrow
@@ -13,7 +13,7 @@
- else
= link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: _('You must sign in to star a project') do
- = icon('star')
+ = sprite_icon('star')
#{ s_('StarProject|Star') }
.count-with-arrow
%span.arrow
diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml
new file mode 100644
index 00000000000..97532f1e2bd
--- /dev/null
+++ b/app/views/projects/clusters/_advanced_settings.html.haml
@@ -0,0 +1,14 @@
+- if can?(current_user, :admin_cluster, @cluster)
+ .append-bottom-20
+ %label.append-bottom-10
+ = s_('ClusterIntegration|Google Container Engine')
+ %p
+ - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
+
+ .well.form-group
+ %label.text-danger
+ = s_('ClusterIntegration|Remove cluster integration')
+ %p
+ = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Container Engine.')
+ = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"})
diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml
index 371cdb1e403..1f8ae463d0f 100644
--- a/app/views/projects/clusters/_form.html.haml
+++ b/app/views/projects/clusters/_form.html.haml
@@ -4,34 +4,32 @@
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
- = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field|
+ = form_for @cluster, url: namespace_project_clusters_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+ = field.hidden_field :provider_type, value: :gcp
= form_errors(@cluster)
.form-group
- = field.label :gcp_cluster_name, s_('ClusterIntegration|Cluster name')
- = field.text_field :gcp_cluster_name, class: 'form-control'
+ = field.label :name, s_('ClusterIntegration|Cluster name')
+ = field.text_field :name, class: 'form-control'
- .form-group
- = field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
- = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
- = field.text_field :gcp_project_id, class: 'form-control'
-
- .form-group
- = field.label :gcp_cluster_zone, s_('ClusterIntegration|Zone')
- = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
- = field.text_field :gcp_cluster_zone, class: 'form-control', placeholder: 'us-central1-a'
+ = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
+ .form-group
+ = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
+ = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :gcp_project_id, class: 'form-control'
- .form-group
- = field.label :gcp_cluster_size, s_('ClusterIntegration|Number of nodes')
- = field.text_field :gcp_cluster_size, class: 'form-control', placeholder: '3'
+ .form-group
+ = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone')
+ = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a'
- .form-group
- = field.label :gcp_machine_type, s_('ClusterIntegration|Machine type')
- = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
- = field.text_field :gcp_machine_type, class: 'form-control', placeholder: 'n1-standard-4'
+ .form-group
+ = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
+ = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
.form-group
- = field.label :project_namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
- = field.text_field :project_namespace, class: 'form-control', placeholder: @cluster.project_namespace_placeholder
+ = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
+ = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
+ = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-2'
.form-group
= field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save'
diff --git a/app/views/projects/clusters/_header.html.haml b/app/views/projects/clusters/_header.html.haml
index 0134d46491c..beb798e7154 100644
--- a/app/views/projects/clusters/_header.html.haml
+++ b/app/views/projects/clusters/_header.html.haml
@@ -11,4 +11,4 @@
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }
%li
- link_to_container_project = link_to(s_('ClusterIntegration|Google Container Engine project'), target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|A %{link_to_container_project} must have been created under this account').html_safe % { link_to_container_project: link_to_container_project }
+ = s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
diff --git a/app/views/projects/clusters/login.html.haml b/app/views/projects/clusters/login.html.haml
index ae132672b7e..fde030b500b 100644
--- a/app/views/projects/clusters/login.html.haml
+++ b/app/views/projects/clusters/login.html.haml
@@ -10,7 +10,7 @@
.col-sm-8.col-sm-offset-4.signin-with-google
- if @authorize_url
= link_to @authorize_url do
- = image_tag('auth_buttons/signin_with_google.png')
+ = image_tag('auth_buttons/signin_with_google.png', width: '191px')
- else
- link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer')
= s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index c538d41ffad..665120c7e49 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -1,9 +1,20 @@
- breadcrumb_title "Cluster"
-- page_title _("New Cluster")
+- page_title _("Cluster")
.row.prepend-top-default
.col-sm-4
= render 'sidebar'
.col-sm-8
- = render 'header'
-= render 'form'
+ - if @project.deployment_platform&.active?
+ %h4.prepend-top-0= s_('ClusterIntegration|Cluster management')
+
+ %p= s_('ClusterIntegration|A cluster has been set up on this project through the Kubernetes integration page')
+ = link_to s_('ClusterIntegration|Manage Kubernetes integration'), edit_project_service_path(@project, :kubernetes), class: 'btn append-bottom-20'
+
+ - else
+ %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
+
+ %p= s_('ClusterIntegration|Create a new cluster on Google Container Engine right from GitLab')
+ = link_to s_('ClusterIntegration|Create on GKE'), providers_gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
+ %p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
+ = link_to s_('ClusterIntegration|Add an existing cluster'), edit_project_service_path(@project, :kubernetes), class: 'btn append-bottom-20'
diff --git a/app/views/projects/clusters/new_gcp.html.haml b/app/views/projects/clusters/new_gcp.html.haml
new file mode 100644
index 00000000000..48e6b6ae8e8
--- /dev/null
+++ b/app/views/projects/clusters/new_gcp.html.haml
@@ -0,0 +1,10 @@
+- breadcrumb_title "Cluster"
+- page_title _("New Cluster")
+
+.row.prepend-top-default
+ .col-sm-4
+ = render 'sidebar'
+ .col-sm-8
+ = render 'header'
+
+= render 'form'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index aee6f904a62..b7671f5e3c4 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -1,26 +1,46 @@
+- @content_class = "limit-container-width" unless fluid_layout
- breadcrumb_title "Cluster"
- page_title _("Cluster")
-- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) && @cluster.on_creation?
-.row.prepend-top-default.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
+- expanded = Rails.env.test?
+
+- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster)
+.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
+ install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm),
+ install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress),
toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_status: @cluster.status_name,
- cluster_status_reason: @cluster.status_reason } }
- .col-sm-4
- = render 'sidebar'
- .col-sm-8
- %label.append-bottom-10{ for: 'enable-cluster-integration' }
- = s_('ClusterIntegration|Enable cluster integration')
- %p
- - if @cluster.enabled?
- - if can?(current_user, :update_cluster, @cluster)
- = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
+ cluster_status_reason: @cluster.status_reason,
+ help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications') } }
+
+
+ .js-cluster-application-notice
+ .flash-container
+
+ %section.settings.no-animate.expanded
+ %h4= s_('ClusterIntegration|Enable cluster integration')
+ .settings-content
+
+ .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
+ = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine')
+ %p.js-error-reason
+
+ .hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
+ = s_('ClusterIntegration|Cluster is being created on Google Container Engine...')
+
+ .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
+ = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine')
+
+ %p
+ - if @cluster.enabled?
+ - if can?(current_user, :update_cluster, @cluster)
+ = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
+ - else
+ = s_('ClusterIntegration|Cluster integration is enabled for this project.')
- else
- = s_('ClusterIntegration|Cluster integration is enabled for this project.')
- - else
- = s_('ClusterIntegration|Cluster integration is disabled for this project.')
+ = s_('ClusterIntegration|Cluster integration is disabled for this project.')
- = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field|
+ = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
.form-group.append-bottom-20
%label.append-bottom-10
@@ -34,37 +54,32 @@
- if can?(current_user, :update_cluster, @cluster)
.form-group
- = field.submit s_('ClusterIntegration|Save'), class: 'btn btn-success'
+ = field.submit _('Save'), class: 'btn btn-success'
- - if can?(current_user, :admin_cluster, @cluster)
- %label.append-bottom-10{ for: 'google-container-engine' }
- = s_('ClusterIntegration|Google Container Engine')
- %p
- - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
+ .cluster-applications-table#js-cluster-applications
- .hidden.js-cluster-error.alert.alert-danger.alert-block{ role: 'alert' }
- = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine')
- %p.js-error-reason
+ %section.settings#js-cluster-details
+ .settings-header
+ %h4= s_('ClusterIntegration|Cluster details')
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p= s_('ClusterIntegration|See and edit the details for your cluster')
- .hidden.js-cluster-creating.alert.alert-info.alert-block{ role: 'alert' }
- = s_('ClusterIntegration|Cluster is being created on Google Container Engine...')
+ .settings-content
- .hidden.js-cluster-success.alert.alert-success.alert-block{ role: 'alert' }
- = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine')
+ .form_group.append-bottom-20
+ %label.append-bottom-10{ for: 'cluster-name' }
+ = s_('ClusterIntegration|Cluster name')
+ .input-group
+ %input.form-control.cluster-name{ value: @cluster.name, disabled: true }
+ %span.input-group-addon.clipboard-addon
+ = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name'))
- .form_group.append-bottom-20
- %label.append-bottom-10{ for: 'cluter-name' }
- = s_('ClusterIntegration|Cluster name')
- .input-group
- %input.form-control.cluster-name{ value: @cluster.gcp_cluster_name, disabled: true }
- %span.input-group-addon.clipboard-addon
- = clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name'))
-
- - if can?(current_user, :admin_cluster, @cluster)
- .well.form_group
- %label.text-danger
- = s_('ClusterIntegration|Remove cluster integration')
- %p
- = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project.')
- = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"})
+ %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4= _('Advanced settings')
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project')
+ .settings-content
+ = render 'advanced_settings'
diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml
index 83821326aec..36b28c731a1 100644
--- a/app/views/projects/commit/_ajax_signature.html.haml
+++ b/app/views/projects/commit/_ajax_signature.html.haml
@@ -1,2 +1,2 @@
- if commit.has_signature?
- %button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
+ %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 09bcd187e59..5f607c2ab25 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -61,14 +61,14 @@
%span.cgray= n_('parent', 'parents', @commit.parents.count)
- @commit.parents.each do |parent|
= link_to parent.short_id, project_commit_path(@project, parent), class: "commit-sha"
- %span.commit-info.branches
+ .commit-info.branches
%i.fa.fa-spinner.fa-spin
- if @commit.last_pipeline
- last_pipeline = @commit.last_pipeline
.well-segment.pipeline-info
- .status-icon-container{ class: "ci-status-icon-#{@commit.status}" }
- = link_to project_pipeline_path(@project, last_pipeline.id) do
+ .status-icon-container
+ = link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
= ci_icon_for_status(last_pipeline.status)
#{ _('Pipeline') }
= link_to "##{last_pipeline.id}", project_pipeline_path(@project, last_pipeline.id)
@@ -77,5 +77,6 @@
#{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) }
.mr-widget-pipeline-graph
= render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph'
- in
- = time_interval_in_words last_pipeline.duration
+ - if last_pipeline.duration
+ in
+ = time_interval_in_words last_pipeline.duration
diff --git a/app/views/projects/commit/_limit_exceeded_message.html.haml b/app/views/projects/commit/_limit_exceeded_message.html.haml
new file mode 100644
index 00000000000..84a52d49487
--- /dev/null
+++ b/app/views/projects/commit/_limit_exceeded_message.html.haml
@@ -0,0 +1,8 @@
+.has-tooltip{ class: "limit-box limit-box-#{objects} prepend-left-5", data: { title: "Project has too many #{label_for_message} to search"} }
+ .limit-icon
+ - if objects == :branch
+ = icon('code-fork')
+ - else
+ = icon('tag')
+ .limit-message
+ %span #{label_for_message.capitalize} unavailable
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index edff018ba6d..44aa8002f12 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -24,5 +24,5 @@
= 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 } }
+%a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }
= label
diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml
index 911c9ddce06..8611129b356 100644
--- a/app/views/projects/commit/branches.html.haml
+++ b/app/views/projects/commit/branches.html.haml
@@ -1,15 +1,15 @@
-- if @branches.any? || @tags.any?
+- if @branches_limit_exceeded
+ = render 'limit_exceeded_message', objects: :branch, label_for_message: "branches"
+- elsif @branches.any?
- branch = commit_default_branch(@project, @branches)
- = link_to(project_ref_path(@project, branch), class: "label label-gray ref-name") do
- = icon('code-fork')
- = branch
+ = commit_branch_link(project_ref_path(@project, branch), branch)
- -# `commit_default_branch` deletes the default branch from `@branches`,
- -# so only render this if we have more branches left
- - if @branches.any? || @tags.any?
- %span
- = link_to "…", "#", class: "js-details-expand label label-gray"
-
- %span.js-details-content.hide
- = commit_branches_links(@project, @branches) if @branches.any?
- = commit_tags_links(@project, @tags) if @tags.any?
+- if @branches.any? || @tags.any? || @tags_limit_exceeded
+ %span
+ = link_to "…", "#", class: "js-details-expand label label-gray"
+ %span.js-details-content.hide
+ = commit_branches_links(@project, @branches)
+ - if @tags_limit_exceeded
+ = render 'limit_exceeded_message', objects: :tag, label_for_message: "tags"
+ - else
+ = commit_tags_links(@project, @tags)
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index a16ffb433a5..a66177f20e9 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -1,11 +1,6 @@
- ref = local_assigns.fetch(:ref)
-- if @note_counts
- - note_count = @note_counts.fetch(commit.id, 0)
-- else
- - notes = commit.notes
- - note_count = notes.user.count
-- cache_key = [project.full_path, commit.id, current_application_settings, note_count, @path.presence, current_controller?(:commits), I18n.locale]
+- cache_key = [project.full_path, commit.id, current_application_settings, @path.presence, current_controller?(:commits), I18n.locale]
- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 45985a5ecef..e75ae87e771 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -1,5 +1,5 @@
- expanded = Rails.env.test?
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Deploy Keys
@@ -7,7 +7,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
%h5.prepend-top-0
Create a new deploy key for this project
= render @deploy_keys.form_partial_path
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index 2de2cf9e38c..dd473ebe580 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -22,9 +22,11 @@
- diff_files.each do |diff_file|
%li
%a.diff-changed-file{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path }
- = icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5")
- %span.diff-file-changes-path.append-right-5= diff_file.new_path
- .pull-right
+ = sprite_icon(diff_file_changed_icon(diff_file), size: 16, css_class: "#{diff_file_changed_icon_color(diff_file)} diff-file-changed-icon append-right-8")
+ %span.diff-changed-file-content.append-right-8
+ %strong.diff-changed-file-name= diff_file.blob.name
+ %span.diff-changed-file-path.prepend-top-5= diff_file.new_path
+ %span.diff-changed-stats
%span.cgreen<
+#{diff_file.added_lines}
%span.cred<
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 8ae4fd94146..71206f3a386 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -4,7 +4,7 @@
- expanded = Rails.env.test?
.project-edit-container
- %section.settings.general-settings
+ %section.settings.general-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
General project settings
@@ -12,7 +12,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Update your project name, description, avatar, and other general settings.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
.project-edit-errors
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f|
%fieldset
@@ -61,7 +61,7 @@
= link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
= f.submit 'Save changes', class: "btn btn-save"
- %section.settings.sharing-permissions
+ %section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Permissions
@@ -69,13 +69,13 @@
= expanded ? 'Collapse' : 'Expand'
%p
Enable or disable certain project features and choose access levels.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
%script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
.js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save"
- %section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) }
+ %section.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
.settings-header
%h4
Merge request settings
@@ -83,22 +83,22 @@
= expanded ? 'Collapse' : 'Expand'
%p
Customize your merge request restrictions.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f|
= render 'merge_request_settings', form: f
= f.submit 'Save changes', class: "btn btn-save"
= render 'export', project: @project
- %section.settings.advanced-settings
+ %section.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Advanced settings
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
- Perform advanced options such as housekeeping, exporting, archiving, renaming, transferring, or removing your project.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
+ .settings-content
.sub-section
%h4 Housekeeping
%p
@@ -147,7 +147,7 @@
%ul
%li Be careful. Renaming a project's repository can have unintended side effects.
%li You will need to update your local repositories to point to the new location.
- - if @project.deployment_services.any?
+ - if @project.deployment_platform.present?
%li Your deployment services will be broken, you will need to manually fix the services after renaming.
= f.submit 'Rename project', class: "btn btn-warning"
- if can?(current_user, :change_namespace, @project)
@@ -173,7 +173,10 @@
%p
This will remove the fork relationship to source project
= succeed "." do
- = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)
+ - if @project.fork_source
+ = link_to(fork_source_name(@project), project_path(@project.fork_source))
+ - else
+ = fork_source_name(@project)
= form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f|
%p
%strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 3f3ce10419f..af564b93dc3 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -24,10 +24,15 @@
%p
You will need to be owner or have the master permission level for the initial push, as the master branch is automatically protected.
+ - if show_auto_devops_callout?(@project)
+ %p
+ - link = link_to(s_('AutoDevOps|Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'))
+ = s_('AutoDevOps|You can activate %{link_to_settings} for this project.').html_safe % { link_to_settings: link }
+ %p
+ = s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
+
- if can?(current_user, :push_code, @project)
%div{ class: container_class }
- - if show_auto_devops_callout?(@project)
- = render 'shared/auto_devops_callout'
.prepend-top-20
.empty_wrapper
%h3.page-title-empty
@@ -67,6 +72,7 @@
%pre.light-well
:preserve
cd existing_repo
+ git remote rename origin old-origin
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git push -u origin --all
git push -u origin --tags
diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml
index 1bcc955ddc8..d9c9f0ed546 100644
--- a/app/views/projects/environments/folder.html.haml
+++ b/app/views/projects/environments/folder.html.haml
@@ -5,6 +5,8 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag("environments_folder")
-#environments-folder-list-view{ data: { "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
+#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json),
+ "folder-name" => @folder,
+ "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"css-class" => container_class } }
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 2e85f608823..88f1348da47 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -3,15 +3,13 @@
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag("common_vue")
= page_specific_javascript_bundle_tag("environments")
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"can-create-environment" => can?(current_user, :create_environment, @project).to_s,
- "project-environments-path" => project_environments_path(@project),
- "project-stopped-environments-path" => project_environments_path(@project, scope: :stopped),
"new-environment-path" => new_project_environment_path(@project),
"help-page-path" => help_page_path("ci/environments"),
"css-class" => container_class } }
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index d7859c9fbeb..add394a6356 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -19,14 +19,15 @@
.environments-container
- if @deployments.blank?
- .blank-state.blank-state-no-icon
- %h2.blank-state-title
- You don't have any deployments right now.
- %p.blank-state-text
- Define environments in the deploy stage(s) in
- %code .gitlab-ci.yml
- to track deployments here.
- = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
+ .blank-state-row
+ .blank-state-center
+ %h2.blank-state-title
+ You don't have any deployments right now.
+ %p.blank-state-text
+ Define environments in the deploy stage(s) in
+ %code .gitlab-ci.yml
+ to track deployments here.
+ = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
.ci-table.environments{ role: 'grid' }
diff --git a/app/views/projects/forks/_fork_button.html.haml b/app/views/projects/forks/_fork_button.html.haml
new file mode 100644
index 00000000000..8a549d431ee
--- /dev/null
+++ b/app/views/projects/forks/_fork_button.html.haml
@@ -0,0 +1,26 @@
+- avatar = namespace_icon(namespace, 100)
+- can_create_project = current_user.can?(:create_projects, namespace)
+
+- if forked_project = namespace.find_fork_of(@project)
+ .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default.forked
+ = link_to project_path(forked_project) do
+ - if /no_((\w*)_)*avatar/.match(avatar)
+ = project_identicon(namespace, class: "avatar s100 identicon")
+ - else
+ .avatar-container.s100
+ = image_tag(avatar, class: "avatar s100")
+ %h5.prepend-top-default
+ = namespace.human_name
+- else
+ .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default{ class: ("disabled" unless can_create_project) }
+ = link_to project_forks_path(@project, namespace_key: namespace.id),
+ method: "POST",
+ class: ("disabled has-tooltip" unless can_create_project),
+ title: (_('You have reached your project limit') unless can_create_project) do
+ - if /no_((\w*)_)*avatar/.match(avatar)
+ = project_identicon(namespace, class: "avatar s100 identicon")
+ - else
+ .avatar-container.s100
+ = image_tag(avatar, class: "avatar s100")
+ %h5.prepend-top-default
+ = namespace.human_name
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index e9613534dde..475c6ba4d3d 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -14,22 +14,7 @@
%h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default
Click to fork the project
- @namespaces.each do |namespace|
- - avatar = namespace_icon(namespace, 100)
- - can_create_project = current_user.can?(:create_projects, namespace)
- - forked_project = namespace.find_fork_of(@project)
- - fork_path = forked_project ? project_path(forked_project) : project_forks_path(@project, namespace_key: namespace.id)
- .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default{ class: [("disabled" unless can_create_project), ("forked" if forked_project)] }
- = link_to fork_path,
- method: "POST",
- class: [("js-fork-thumbnail" unless forked_project), ("disabled has-tooltip" unless can_create_project)],
- title: (_('You have reached your project limit') unless can_create_project) do
- - if /no_((\w*)_)*avatar/.match(avatar)
- = project_identicon(namespace, class: "avatar s100 identicon")
- - else
- .avatar-container.s100
- = image_tag(avatar, class: "avatar s100")
- %h5.prepend-top-default
- = namespace.human_name
+ = render 'fork_button', namespace: namespace
- else
%strong
No available namespaces to fork the project.
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index 70156c03e3c..cce16bc58b3 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "Contributors"
+- page_title _('Contributors')
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_d3')
= webpack_bundle_tag('graphs')
@@ -7,23 +7,23 @@
.js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) }
.sub-header-block
- .tree-ref-holder
+ .tree-ref-holder.inline.vertical-align-middle
= render 'shared/ref_switcher', destination: 'graphs'
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
+ = link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn'
.loading-graph
.center
%h3.page-title
%i.fa.fa-spinner.fa-spin
- Building repository graph.
- %p.slead Please wait a moment, this page will automatically refresh when ready.
+ = s_('ContributorsPage|Building repository graph.')
+ %p.slead
+ = s_('ContributorsPage|Please wait a moment, this page will automatically refresh when ready.')
.stat-graph.hide
.header.clearfix
%h3#date_header.page-title
%p.light
- Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits.
+ = s_('ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits.') % { branch_name: @ref }
%input#brush_change{ :type => "hidden" }
.graphs.row
#contributors-master
diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml
index 05b06cfc8b2..8096d9530c3 100644
--- a/app/views/projects/hook_logs/_index.html.haml
+++ b/app/views/projects/hook_logs/_index.html.haml
@@ -24,7 +24,7 @@
%td
= truncate(hook_log.url, length: 50)
%td.light
- #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%td.light
= time_ago_with_tooltip(hook_log.created_at)
%td
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index 13809da6523..0d39edb7bfd 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -3,8 +3,8 @@
- if @can_bulk_update
= button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
= link_to "New issue", new_project_issue_path(@project,
- issue: { assignee_id: issues_finder.assignee.try(:id),
- milestone_id: issues_finder.milestones.first.try(:id) }),
+ issue: { assignee_id: finder.assignee.try(:id),
+ milestone_id: finder.milestones.first.try(:id) }),
class: "btn btn-new",
title: "New issue",
id: "new_issue_link"
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index e1b4a49850a..331d62cf247 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,29 +1,50 @@
+- can_create_merge_request = can?(current_user, :create_merge_request, @project)
+- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
+- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
+
- if can?(current_user, :push_code, @project)
- .create-mr-dropdown-wrap{ data: { can_create_path: can_create_branch_project_issue_path(@project, @issue), create_mr_path: create_merge_request_project_issue_path(@project, @issue), create_branch_path: project_branches_path(@project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid) } }
+ - can_create_path = can_create_branch_project_issue_path(@project, @issue)
+ - create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch)
+ - create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
+ - refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
+
+ .create-mr-dropdown-wrap{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
.btn-group.unavailable
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
= icon('spinner', class: 'fa-spin')
%span.text
Checking branch availability…
.btn-group.available.hide
- %input.btn.js-create-merge-request.btn-inverted.btn-success{ type: 'button', value: 'Create a merge request', data: { action: 'create-mr' } }
- %button.btn.btn-inverted.dropdown-toggle.btn-inverted.btn-success.js-dropdown-toggle{ type: 'button', data: { 'dropdown-trigger' => '#create-merge-request-dropdown' } }
+ %button.btn.js-create-merge-request.btn-default{ type: 'button', data: { action: data_action } }
+ = value
+
+ %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-default.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
= icon('caret-down')
- %ul#create-merge-request-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } }
- %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', 'text' => 'Create a merge request' } }
- .menu-item
- .icon-container
- = icon('check')
- .description
- %strong Create a merge request
- %span
- Creates a merge request named after this issue, with source branch created from '#{@project.default_branch}'.
- %li.divider.droplab-item-ignore
- %li{ role: 'button', data: { value: 'create-branch', 'text' => 'Create a branch' } }
- .menu-item
- .icon-container
- = icon('check')
- .description
- %strong Create a branch
- %span
- Creates a branch named after this issue, from '#{@project.default_branch}'.
+
+ %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
+ - if can_create_merge_request
+ %li.create-item.droplab-item-selected.droplab-item-ignore-hiding{ role: 'button', data: { value: 'create-mr', text: 'Create merge request' } }
+ .menu-item.droplab-item-ignore-hiding
+ .icon-container.droplab-item-ignore-hiding= icon('check')
+ .description.droplab-item-ignore-hiding Create merge request and branch
+
+ %li.create-item.droplab-item-ignore-hiding{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: 'Create branch' } }
+ .menu-item.droplab-item-ignore-hiding
+ .icon-container.droplab-item-ignore-hiding= icon('check')
+ .description.droplab-item-ignore-hiding Create branch
+ %li.divider
+
+ %li.droplab-item-ignore
+ Branch name
+ %input.js-branch-name.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
+ %span.js-branch-message.branch-message.droplab-item-ignore
+
+ %li.droplab-item-ignore
+ Source (branch or tag)
+ %input.js-ref.ref.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
+ %span.js-ref-message.ref-message.droplab-item-ignore
+
+ %li.droplab-item-ignore
+ %button.btn.btn-success.js-create-target.droplab-item-ignore{ type: 'button', data: { action: 'create-mr' } }
+ Create merge request
+
diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml
new file mode 100644
index 00000000000..1b7d878c38c
--- /dev/null
+++ b/app/views/projects/issues/edit.html.haml
@@ -0,0 +1,7 @@
+- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues"
+
+%h3.page-title
+ Edit Issue ##{@issue.iid}
+%hr
+
+= render "form"
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index b9fec8af4d7..48410ffee21 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -27,9 +27,9 @@
.issuable-meta
- if @issue.confidential
- = icon('eye-slash', class: 'issuable-warning-icon')
+ .issuable-warning-icon.inline= sprite_icon('eye-slash', size: 16, css_class: 'icon')
- if @issue.discussion_locked?
- = icon('lock', class: 'issuable-warning-icon')
+ .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@issue, @project, "Issue")
.issuable-actions.js-issuable-actions
@@ -40,7 +40,7 @@
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- if can_update_issue
- %li= link_to 'Edit', edit_project_issue_path(@project, @issue)
+ %li= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'issuable-edit'
- unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 43e23bb2200..a71333497e6 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -4,8 +4,10 @@
.sidebar-container
.blocks-container
.block
- %strong
+ %strong.inline.prepend-top-8
= @build.name
+ - if can?(current_user, :update_build, @build) && @build.retryable?
+ = link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post
%a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' }
= icon('angle-double-right')
@@ -42,13 +44,14 @@
%h4.title
Trigger
- %p
- %span.build-light-text Token:
- #{@build.trigger_request.trigger.short_token}
+ - if @build.trigger_request&.trigger&.short_token
+ %p
+ %span.build-light-text Token:
+ #{@build.trigger_request.trigger.short_token}
- if @build.trigger_variables.any?
%p
- %button.btn.group.btn-group-justified.reveal-variables Reveal Variables
+ %button.btn.group.btn-group-justified.js-reveal-variables Reveal Variables
%dl.js-build-variables.trigger-build-variables.hide
- @build.trigger_variables.each do |trigger_variable|
@@ -89,7 +92,7 @@
- builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
= link_to project_job_path(@project, build) do
- = icon('arrow-right')
+ = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')
%span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status)
%span
@@ -98,4 +101,5 @@
- else
= build.id
- if build.retried?
- %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
+ %span.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
+ = sprite_icon('retry', size:16, css_class: 'icon-retry')
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index ce0e3872240..1d0aaa47b60 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -57,13 +57,13 @@
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
- .js-truncated-info.truncated-info.hidden<
+ .js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
Showing last
%span.js-truncated-info-size.truncated-info-size><
KiB of log -
%a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw
- .controllers
+ .controllers.pull-right
- if @build.has_trace?
= link_to raw_project_job_path(@project, @build),
title: 'Show complete raw',
@@ -71,7 +71,7 @@
class: 'js-raw-link-controller has-tooltip controllers-buttons' do
= icon('file-text-o')
- - if can?(current_user, :update_build, @project) && @build.erasable?
+ - if @build.erasable? && can?(current_user, :erase_build, @build)
= link_to erase_project_job_path(@project, @build),
method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index cb723fe6a18..75b3db7e505 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -16,7 +16,7 @@
.issuable-meta
- if @merge_request.discussion_locked?
- = icon('lock', class: 'issuable-warning-icon')
+ .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@merge_request, @project, "Merge request")
.issuable-actions.js-issuable-actions
@@ -34,7 +34,7 @@
%li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: merge_request_button_visibility(@merge_request, false) }
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
+ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
- if can_update_merge_request
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit"
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 6b8dcb3e60b..8da2243adef 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -13,8 +13,6 @@
- if @project.merge_requests.exists?
%div{ class: container_class }
- - if show_auto_devops_callout?(@project)
- = render 'shared/auto_devops_callout'
.top-area
= render 'shared/issuable/nav', type: :merge_requests
.nav-controls
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index a5153df1159..9fc297ab7f6 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -23,14 +23,18 @@
= milestone_date_range(@milestone)
.milestone-buttons
- if can?(current_user, :admin_milestone, @project)
+ = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped btn-nr" do
+ Edit
+
+ - if @project.group
+ = link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "Promoting this milestone will make it available for all projects inside the group. Existing project milestones with the same name will be merged. Are you sure?", toggle: "tooltip" }, method: :post do
+ Promote
+
- if @milestone.active?
= link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
- else
= link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
- = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped btn-nr" do
- Edit
-
= link_to project_milestone_path(@project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
Delete
@@ -40,6 +44,7 @@
.detail-page-description.milestone-detail
%h2.title
= markdown_field(@milestone, :title)
+
%div
- if @milestone.description.present?
.description
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index cc41b908946..0a7880ce4cd 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -14,114 +14,88 @@
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
New project
- - if import_sources_enabled?
- %p
- A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}.
- %p
- All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings.
+ %p
+ A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}.
+ %p
+ All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings.
.col-lg-9.js-toggle-container
- = form_for @project, html: { class: 'new_project' } do |f|
- .create-project-options
- .first-column
+ %ul.nav-links.gitlab-tabs{ role: 'tablist' }
+ %li.active{ role: 'presentation' }
+ %a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' }
+ %span.hidden-xs Blank project
+ %span.visible-xs Blank
+ %li{ role: 'presentation' }
+ %a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' }
+ %span.hidden-xs Create from template
+ %span.visible-xs Template
+ %li{ role: 'presentation' }
+ %a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' }
+ %span.hidden-xs Import project
+ %span.visible-xs Import
+
+ .tab-content.gitlab-tab-content
+ .tab-pane.active{ id: 'blank-project-pane', role: 'tabpanel' }
+ = form_for @project, html: { class: 'new_project' } do |f|
+ = render 'new_project_fields', f: f, project_name_id: "blank-project-name"
+
+ .tab-pane.no-padding{ id: 'create-from-template-pane', role: 'tabpanel' }
+ = form_for @project, html: { class: 'new_project' } do |f|
.project-template
.form-group
- = f.label :template_project, class: 'label-light' do
- Create from template
- = link_to icon('question-circle'), help_page_path("gitlab-basics/create-project"), target: '_blank', aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'}
%div
= render 'project_templates', f: f
- - if import_sources_enabled?
- .second-column
- .project-import
- .form-group.clearfix
- = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
- Import project from
- .col-sm-12.import-buttons
- %div
- - if github_import_enabled?
- = link_to new_import_github_path, class: 'btn import_github' do
- = icon('github', text: 'GitHub')
- %div
- - if bitbucket_import_enabled?
- = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
- = icon('bitbucket', text: 'Bitbucket')
- - unless bitbucket_import_configured?
- = render 'bitbucket_import_modal'
- %div
- - if gitlab_import_enabled?
- = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
- = icon('gitlab', text: 'GitLab.com')
- - unless gitlab_import_configured?
- = render 'gitlab_import_modal'
- %div
- - if google_code_import_enabled?
- = link_to new_import_google_code_path, class: 'btn import_google_code' do
- = icon('google', text: 'Google Code')
- %div
- - if fogbugz_import_enabled?
- = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
- = icon('bug', text: 'Fogbugz')
- %div
- - if gitea_import_enabled?
- = link_to new_import_gitea_url, class: 'btn import_gitea' do
- = custom_icon('go_logo')
- Gitea
- %div
- - if git_import_enabled?
- %button.btn.js-toggle-button.import_git{ type: "button" }
- = icon('git', text: 'Repo by URL')
- - 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
- .js-toggle-content.hide
- %hr
- = render "shared/import_form", f: f
- %hr
-
- .row
- .form-group.col-xs-12.col-sm-6
- = f.label :namespace_id, class: 'label-light' do
- %span
- Project path
- .form-group
- .input-group
- - if current_user.can_select_namespace?
- .input-group-addon
- = root_url
- = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1}
-
- - else
- .input-group-addon.static-namespace
- #{root_url}#{current_user.username}/
- = f.hidden_field :namespace_id, value: current_user.namespace_id
- .form-group.col-xs-12.col-sm-6.project-path
- = f.label :path, class: 'label-light' do
- %span
- Project name
- = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true
- - if current_user.can_create_group?
- .help-block
- Want to house several dependent projects under the same namespace?
- = link_to "Create a group", new_group_path
-
- .form-group
- = f.label :description, class: 'label-light' do
- Project description
- %span.light (optional)
- = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250
-
- .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' }
- = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
- = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
- = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
+ .tab-pane.import-project-pane{ id: 'import-project-pane', role: 'tabpanel' }
+ = form_for @project, html: { class: 'new_project' } do |f|
+ - if import_sources_enabled?
+ .project-import.row
+ .col-sm-12
+ .form-group.import-btn-container.clearfix
+ = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
+ Import project from
+ .import-buttons
+ - 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')
+ %div
+ - if github_import_enabled?
+ = link_to new_import_github_path, class: 'btn import_github' do
+ = icon('github', text: 'GitHub')
+ %div
+ - if bitbucket_import_enabled?
+ = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
+ = icon('bitbucket', text: 'Bitbucket')
+ - unless bitbucket_import_configured?
+ = render 'bitbucket_import_modal'
+ %div
+ - if gitlab_import_enabled?
+ = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
+ = icon('gitlab', text: 'GitLab.com')
+ - unless gitlab_import_configured?
+ = render 'gitlab_import_modal'
+ %div
+ - if google_code_import_enabled?
+ = link_to new_import_google_code_path, class: 'btn import_google_code' do
+ = icon('google', text: 'Google Code')
+ %div
+ - if fogbugz_import_enabled?
+ = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
+ = icon('bug', text: 'Fogbugz')
+ %div
+ - if gitea_import_enabled?
+ = link_to new_import_gitea_url, class: 'btn import_gitea' do
+ = custom_icon('go_logo')
+ Gitea
+ %div
+ - if git_import_enabled?
+ %button.btn.js-toggle-button.import_git{ type: "button" }
+ = icon('git', text: 'Repo by URL')
+ .col-lg-12
+ .js-toggle-content.hide.toggle-import-form
+ %hr
+ = render "shared/import_form", f: f
+ = render 'new_project_fields', f: f, project_name_id: "import-url-name"
.save-project-loader.hide
.center
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index a10a7c23924..b2e71cff6ce 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -2,8 +2,6 @@
- page_title "Pipelines"
%div{ 'class' => container_class }
- - if show_auto_devops_callout?(@project)
- = render 'shared/auto_devops_callout'
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
"help-page-path" => help_page_path('ci/quick_start/README'),
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
@@ -11,12 +9,6 @@
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
"new-pipeline-path" => new_project_pipeline_path(@project),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
- "all-path" => project_pipelines_path(@project),
- "pending-path" => project_pipelines_path(@project, scope: :pending),
- "running-path" => project_pipelines_path(@project, scope: :running),
- "finished-path" => project_pipelines_path(@project, scope: :finished),
- "branches-path" => project_pipelines_path(@project, scope: :branches),
- "tags-path" => project_pipelines_path(@project, scope: :tags),
"has-ci" => @repository.gitlab_ci_yml,
"ci-lint-path" => ci_lint_path } }
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index 77211099830..ee4fa663b9f 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -13,29 +13,39 @@
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
- .radio
+ .radio.js-auto-devops-enable-radio-wrapper
= form.label :enabled_true do
- = form.radio_button :enabled, 'true'
+ = form.radio_button :enabled, 'true', class: 'js-auto-devops-enable-radio'
%strong Enable Auto DevOps
%br
%span.descr
The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
- .radio
+ - if show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(@project)
+ .checkbox.hide.js-run-auto-devops-pipeline-checkbox-wrapper
+ = label_tag 'project[run_auto_devops_pipeline_explicit]' do
+ = check_box_tag 'project[run_auto_devops_pipeline_explicit]', true, false, class: 'js-run-auto-devops-pipeline-checkbox'
+ = s_('ProjectSettings|Immediately run a pipeline on the default branch')
+
+ .radio.js-auto-devops-enable-radio-wrapper
= form.label :enabled_false do
- = form.radio_button :enabled, 'false'
+ = form.radio_button :enabled, 'false', class: 'js-auto-devops-enable-radio'
%strong Disable Auto DevOps
%br
%span.descr
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
- .radio
- = form.label :enabled_nil do
- = form.radio_button :enabled, ''
+ .radio.js-auto-devops-enable-radio-wrapper
+ = form.label :enabled_ do
+ = form.radio_button :enabled, '', class: 'js-auto-devops-enable-radio'
%strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br
%span.descr
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
- %br
+ - if show_run_auto_devops_pipeline_checkbox_for_instance_setting?(@project)
+ .checkbox.hide.js-run-auto-devops-pipeline-checkbox-wrapper
+ = label_tag 'project[run_auto_devops_pipeline_implicit]' do
+ = check_box_tag 'project[run_auto_devops_pipeline_implicit]', true, false, class: 'js-run-auto-devops-pipeline-checkbox'
+ = s_('ProjectSettings|Immediately run a pipeline on the default branch')
%p
You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 25153fd0b6f..fd5d3ec56da 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -17,14 +17,14 @@
%i Owners
.light
- if can?(current_user, :admin_project_member, @project)
- %ul.nav-links.project-member-tabs{ role: 'tablist' }
+ %ul.nav-links.gitlab-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' }
%a{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member
- if @project.allowed_to_share_with_group?
%li{ role: 'presentation' }
%a{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group
- .tab-content.project-member-tab-content
+ .tab-content.gitlab-tab-content
.tab-pane.active{ id: 'add-member-pane', role: 'tabpanel' }
= render 'projects/project_members/new_project_member', tab_title: 'Add member'
.tab-pane{ id: 'share-with-group-pane', role: 'tabpanel' }
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index 6a47cbdf724..ba7d98228c3 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = Rails.env.test?
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Protected Branches
@@ -8,7 +8,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
%p
By default, protected branches are designed to:
%ul
diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml
index ea91e8af70e..f53b81cada6 100644
--- a/app/views/projects/protected_tags/_create_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml
@@ -2,7 +2,7 @@
.create_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-create wide',
- dropdown_class: 'dropdown-menu-selectable',
+ dropdown_class: 'dropdown-menu-selectable capitalize-header',
data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }})
= render 'projects/protected_tags/shared/create_protected_tag'
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index c07bd454ff6..e764a37bbd7 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = Rails.env.test?
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Protected Tags
@@ -8,7 +8,7 @@
= expanded ? 'Collapse' : 'Expand'
%p
Limit access to creating and updating tags.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
%p
By default, protected tags are designed to:
%ul
diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml
index d8e11500964..b0cb5ce5e8f 100644
--- a/app/views/projects/services/prometheus/_show.html.haml
+++ b/app/views/projects/services/prometheus/_show.html.haml
@@ -4,42 +4,39 @@
.row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring
.col-lg-3
%h4.prepend-top-0
- Metrics
+ = s_('PrometheusService|Metrics')
%p
- Metrics are automatically configured and monitored
- based on a library of metrics from popular exporters.
- = link_to 'More information', help_page_path('user/project/integrations/prometheus')
+ = s_('PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters.')
+ = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus')
.col-lg-9
.panel.panel-default.js-panel-monitored-metrics{ data: { "active-metrics" => "#{project_prometheus_active_metrics_path(@project, :json)}" } }
.panel-heading
%h3.panel-title
- Monitored
+ = s_('PrometheusService|Monitored')
%span.badge.js-monitored-count 0
.panel-body
.loading-metrics.text-center.js-loading-metrics
= icon('spinner spin 3x', class: 'metrics-load-spinner')
- %p Finding and configuring metrics...
+ %p
+ = s_('PrometheusService|Finding and configuring metrics...')
.empty-metrics.text-center.hidden.js-empty-metrics
= custom_icon('icon_empty_metrics')
- %p No metrics are being monitored. To start monitoring, deploy to an environment.
- = link_to project_environments_path(@project), title: 'View environments', class: 'btn btn-success' do
- View environments
+ %p
+ = s_('PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment.')
+ = link_to s_('PrometheusService|View environments'), project_environments_path(@project), class: 'btn btn-success'
%ul.list-unstyled.metrics-list.hidden.js-metrics-list
.panel.panel-default.hidden.js-panel-missing-env-vars
.panel-heading
%h3.panel-title
= icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel')
- Missing environment variable
+ = s_('PrometheusService|Missing environment variable')
%span.badge.js-env-var-count 0
.panel-body.hidden
.flash-container
.flash-notice
.flash-text
- To set up automatic monitoring, add the environment variable
- %code
- $CI_ENVIRONMENT_SLUG
- to exporter&rsquo;s queries.
- = link_to 'More information', help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels')
+ = s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe
+ = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels')
%ul.list-unstyled.metrics-list.js-missing-var-metrics-list
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 62455d0d40d..664a4554692 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -4,7 +4,7 @@
- expanded = Rails.env.test?
-%section.settings#js-general-pipeline-settings
+%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
General pipelines settings
@@ -12,10 +12,10 @@
= expanded ? 'Collapse' : 'Expand'
%p
Update your CI/CD configuration, like job timeout or Auto DevOps.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'projects/pipelines_settings/show'
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Runners settings
@@ -23,10 +23,10 @@
= expanded ? 'Collapse' : 'Expand'
%p
Register and see your runners for this project.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'projects/runners/index'
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Secret variables
@@ -35,10 +35,10 @@
= expanded ? 'Collapse' : 'Expand'
%p
= render "ci/variables/content"
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'ci/variables/index'
-%section.settings
+%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Pipeline triggers
@@ -48,5 +48,5 @@
Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
impersonate their associated user including their access to projects and their project
permissions.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .settings-content
= render 'projects/triggers/index'
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 705a4607ad2..7a68aa16aa4 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -67,7 +67,7 @@
- if koding_enabled? && @repository.koding_yml.blank?
%li.missing
= link_to _('Set up Koding'), add_koding_stack_path(@project)
- - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
+ - if @repository.gitlab_ci_yml.blank? && @project.deployment_platform.present?
%li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do
#{ _('Set up auto deploy') }
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 1927216e191..467f19b4c56 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -7,7 +7,7 @@
- if protected_tag?(@project, tag)
%span.label.label-success.prepend-left-4
- protected
+ = s_('TagsPage|protected')
- if tag.message.present?
&nbsp;
@@ -18,7 +18,7 @@
= render 'projects/branches/commit', commit: commit, project: @project
- else
%p
- Cant find HEAD commit for this tag
+ = s_("TagsPage|Can't find HEAD commit for this tag")
- if release && release.description.present?
.description.prepend-top-default
.wiki
@@ -28,9 +28,9 @@
= render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name]
- if can?(current_user, :push_code, @project)
- = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
+ = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do
= icon("pencil")
- if can?(current_user, :admin_project, @project)
- = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do
= icon("trash-o")
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 27d58d4c0e8..da364b58e36 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -1,16 +1,16 @@
- @no_container = true
- @sort ||= sort_value_recently_updated
-- page_title "Tags"
+- page_title s_('TagsPage|Tags')
- add_to_breadcrumbs("Repository", project_tree_path(@project))
.flex-list{ class: container_class }
.top-area.adjust
.nav-text.row-main-content
- Tags give the ability to mark specific points in history as being important
+ = s_('TagsPage|Tags give the ability to mark specific points in history as being important')
.nav-controls.row-fixed-content
= form_tag(filter_tags_path, method: :get) do
- = search_field_tag :search, params[:search], { placeholder: 'Filter by tag name', id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
+ = search_field_tag :search, params[:search], { placeholder: s_('TagsPage|Filter by tag name'), id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
.dropdown
%button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} }
@@ -19,13 +19,13 @@
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li.dropdown-header
- Sort by
+ = s_('TagsPage|Sort by')
- tags_sort_options_hash.each do |value, title|
%li
= link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value)
- if can?(current_user, :push_code, @project)
= link_to new_project_tag_path(@project), class: 'btn btn-create new-tag-btn' do
- New tag
+ = s_('TagsPage|New tag')
.tags
- if @tags.any?
@@ -36,9 +36,9 @@
- else
.nothing-here-block
- Repository has no tags yet.
+ = s_('TagsPage|Repository has no tags yet.')
%br
%small
- Use git tag command to add a new one:
+ = s_('TagsPage|Use git tag command to add a new one:')
%br
%span.monospace git tag -a v1.4 -m 'version 1.4'
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 521b4d927bc..031efa903c5 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -1,4 +1,4 @@
-- page_title "New Tag"
+- page_title s_('TagsPage|New Tag')
- default_ref = params[:ref] || @project.default_branch
- if @error
@@ -7,7 +7,7 @@
= @error
%h3.page-title
- New Tag
+ = s_('TagsPage|New Tag')
%hr
= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal common-note-form tag-form js-quick-submit js-requires-input" do
@@ -23,21 +23,24 @@
= button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
.text-left.dropdown-toggle-text= default_ref
= render 'shared/ref_dropdown', dropdown_class: 'wide'
- .help-block Existing branch name, tag, or commit SHA
+ .help-block
+ = s_('TagsPage|Existing branch name, tag, or commit SHA')
.form-group
= label_tag :message, nil, class: 'control-label'
.col-sm-10
= text_area_tag :message, @message, required: false, tabindex: 3, class: 'form-control', rows: 5
- .help-block Optionally, add a message to the tag.
+ .help-block
+ = s_('TagsPage|Optionally, add a message to the tag.')
%hr
.form-group
- = label_tag :release_description, 'Release notes', class: 'control-label'
+ = label_tag :release_description, s_('TagsPage|Release notes'), class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
- = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here...", current_text: @release_description
+ = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here...'), current_text: @release_description
= render 'shared/notes/hints'
- .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
+ .help-block
+ = s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.')
.form-actions
- = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3
- = link_to 'Cancel', project_tags_path(@project), class: 'btn btn-cancel'
+ = button_tag s_('TagsPage|Create tag'), class: 'btn btn-create', tabindex: 3
+ = link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel'
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 43aa2b27af6..dfe2c37ed8e 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -1,7 +1,7 @@
- @no_container = true
-- add_to_breadcrumbs "Tags", project_tags_path(@project)
+- add_to_breadcrumbs s_('TagsPage|Tags'), project_tags_path(@project)
- breadcrumb_title @tag.name
-- page_title @tag.name, "Tags"
+- page_title @tag.name, s_('TagsPage|Tags')
%div{ class: container_class }
.top-area.multi-line
@@ -12,25 +12,25 @@
= @tag.name
- if protected_tag?(@project, @tag)
%span.label.label-success
- protected
+ = s_('TagsPage|protected')
- if @commit
= render 'projects/branches/commit', commit: @commit, project: @project
- else
- Cant find HEAD commit for this tag
+ = s_("TagsPage|Can't find HEAD commit for this tag")
.nav-controls.controls-flex
- if can?(current_user, :push_code, @project)
- = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Edit release notes' do
+ = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do
= icon("pencil")
- = link_to project_tree_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse files' do
+ = link_to project_tree_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Browse files') do
= icon('files-o')
- = link_to project_commits_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: 'Browse commits' do
+ = link_to project_commits_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Browse commits') do
= icon('history')
.btn-container.controls-item
= render 'projects/buttons/download', project: @project, ref: @tag.name
- if can?(current_user, :admin_project, @project)
.btn-container.controls-item-full
- = link_to project_tag_path(@project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
+ = link_to project_tag_path(@project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: @tag.name } } do
%i.fa.fa-trash-o
- if @tag.message.present?
@@ -43,4 +43,4 @@
.wiki
= markdown_field(@release, :description)
- else
- This tag has no release notes.
+ = s_('TagsPage|This tag has no release notes.')
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 6cddc38d11a..c02f7ee37ed 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -1,13 +1,15 @@
.tree-ref-container
.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'tree', path: @path
+ = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- - unless show_new_repo?
+ - if show_new_repo? && can_push_branch?(@project, @ref)
+ .js-new-dropdown
+ - else
= render 'projects/tree/old_tree_header'
.tree-controls
- if show_new_repo?
- = render 'shared/repo/editable_mode'
+ .editable-mode
- else
= link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
diff --git a/app/views/projects/tree/_truncated_notice_tree_row.html.haml b/app/views/projects/tree/_truncated_notice_tree_row.html.haml
new file mode 100644
index 00000000000..693b641888b
--- /dev/null
+++ b/app/views/projects/tree/_truncated_notice_tree_row.html.haml
@@ -0,0 +1,7 @@
+%tr.tree-truncated-warning
+ %td{ colspan: '3' }
+ = icon('exclamation-triangle fw')
+ %span
+ Too many items to show. To preserve performance only
+ %strong #{number_with_delimiter(limit)} of #{number_with_delimiter(total)}
+ items are displayed.
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 0cc6674842a..64cc70053ef 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -11,8 +11,6 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'repo'
-%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- - if show_auto_devops_callout?(@project) && !show_new_repo?
- = render 'shared/auto_devops_callout'
+%div{ class: [(container_class unless show_new_repo?), ("limit-container-width" unless fluid_layout)] }
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml
index 0a1ccbc5f1c..efa16d38f84 100644
--- a/app/views/projects/wikis/_pages_wiki_page.html.haml
+++ b/app/views/projects/wikis/_pages_wiki_page.html.haml
@@ -2,4 +2,4 @@
= link_to wiki_page.title, project_wiki_path(@project, wiki_page)
%small (#{wiki_page.format})
.pull-right
- %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.commit.authored_date) }).html_safe
+ %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe
diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml
index 911e1339541..d6e568bac94 100644
--- a/app/views/projects/wikis/empty.html.haml
+++ b/app/views/projects/wikis/empty.html.haml
@@ -1,6 +1,6 @@
- page_title _("Wiki")
-%h3.page-title= _("Wiki|Empty page")
+%h3.page-title= s_("Wiki|Empty page")
%hr
.error_message
= s_("WikiEmptyPageError|You are not allowed to create wiki pages")
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 9ee09262324..969a1677d9a 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -21,7 +21,7 @@
%th= _("Last updated")
%th= _("Format")
%tbody
- - @page.versions.each_with_index do |version, index|
+ - @page_versions.each_with_index do |version, index|
- commit = version
%tr
%td
@@ -37,5 +37,6 @@
%td
%strong
= version.format
+= paginate @page_versions, theme: 'gitlab'
= render 'sidebar'
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index de15fc99eda..b3b83cee81a 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -11,8 +11,8 @@
.nav-text
%h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by
- = (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author_name}</strong>" }).html_safe
- #{time_ago_with_tooltip(@page.commit.authored_date)}
+ = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe
+ #{time_ago_with_tooltip(@page.last_version.authored_date)}
.nav-controls
= render 'main_links'
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index 7c633175a06..934d65e8b42 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -1,15 +1,16 @@
-.user-callout{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
- .bordered-box.landing.content-block
- %button.btn.btn-default.close.js-close-callout{ type: 'button',
- 'aria-label' => 'Dismiss Auto DevOps box' }
- = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
- .svg-container
- = custom_icon('icon_autodevops')
- .user-callout-copy
- %h4= s_('AutoDevOps|Auto DevOps (Beta)')
- %p= s_('AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
- %p
- - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
- = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
+.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
+ .banner-graphic
+ = custom_icon('icon_autodevops')
- = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn btn-primary js-close-callout'
+ .prepend-top-10.prepend-left-10.append-bottom-10
+ %h5= s_('AutoDevOps|Auto DevOps (Beta)')
+ %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
+ %p
+ - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
+ .prepend-top-10
+ = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout'
+
+ %button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
+ 'aria-label' => 'Dismiss Auto DevOps box' }
+ = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 3d9c90c38fe..fba08092351 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -7,7 +7,7 @@
%span
= enabled_project_button(project, enabled_protocol)
- else
- %a#clone-dropdown.clone-dropdown-btn.btn{ href: '#', data: { toggle: 'dropdown' } }
+ %a#clone-dropdown.btn.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown' } }
%span
= default_clone_protocol.upcase
= icon('caret-down')
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 233d8c95eda..736afa085e8 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -11,6 +11,7 @@
%li
If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
%li
- The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination.
+ The import will time out after #{time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout)}.
+ For repositories that take longer, use a clone/push combination.
%li
To migrate an SVN repository, check out #{link_to "this document", help_page_path('user/project/import/svn')}.
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
index dff847159d3..901a177323b 100644
--- a/app/views/shared/_mini_pipeline_graph.html.haml
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -7,7 +7,7 @@
.stage-container.dropdown{ class: klass }
%button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
- = custom_icon(icon_status)
+ = sprite_icon(icon_status)
= icon('caret-down')
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 7ad743b3b81..f4a4bfaec54 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,3 +1,6 @@
+- show_create = local_assigns.fetch(:show_create, false)
+
+- show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project)
- dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
= hidden_field_tag :destination, destination
@@ -6,9 +9,21 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
- .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
- = dropdown_title _("Switch branch/tag")
- = dropdown_filter _("Search branches and tags")
- = dropdown_content
- = dropdown_loading
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
+ .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
+ .dropdown-page-one
+ = dropdown_title _("Switch branch/tag")
+ = dropdown_filter _("Search branches and tags")
+ = dropdown_content
+ = dropdown_loading
+ - if show_new_branch_form
+ = dropdown_footer do
+ %ul.dropdown-footer-list
+ %li
+ %a.dropdown-toggle-page{ href: "#" }
+ Create new branch
+ - if show_new_branch_form
+ .dropdown-page-two
+ = dropdown_title("Create new branch", options: { back: true })
+ = dropdown_content do
+ .js-new-branch-dropdown
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 1f540bdaf93..dfc0f9be321 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -25,7 +25,7 @@
show_any: "true",
project_id: @project&.try(:id),
labels: labels_filter_path(false),
- namespace_path: @project.try(:namespace).try(:full_path),
+ namespace_path: @namespace_path,
project_path: @project.try(:path) },
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
%span.dropdown-toggle-text
diff --git a/app/views/shared/boards/components/sidebar/_notifications.html.haml b/app/views/shared/boards/components/sidebar/_notifications.html.haml
index 9b989c23cab..333dd1a00b4 100644
--- a/app/views/shared/boards/components/sidebar/_notifications.html.haml
+++ b/app/views/shared/boards/components/sidebar/_notifications.html.haml
@@ -1,7 +1,5 @@
- if current_user
- .block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" }
- %span.issuable-header-text.hide-collapsed.pull-left
- Notifications
- %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
- %span
- {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
+ .block.subscriptions
+ %subscriptions{ ":loading" => "issue.isFetching && issue.isFetching.subscriptions",
+ ":subscribed" => "issue.subscribed",
+ ":id" => "issue.id" }
diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
index 760370a6984..8e6747ca740 100644
--- a/app/views/shared/groups/_dropdown.html.haml
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -1,18 +1,32 @@
-.dropdown.inline.js-group-filter-dropdown-wrap
+- show_archive_options = local_assigns.fetch(:show_archive_options, false)
+- if @sort.present?
+ - default_sort_by = @sort
+- else
+ - if params[:sort]
+ - default_sort_by = params[:sort]
+ - else
+ - default_sort_by = sort_value_recently_created
+
+.dropdown.inline.js-group-filter-dropdown-wrap.append-right-10
%button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.dropdown-label
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_recently_created
+ = sort_options_hash[default_sort_by]
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- %li
- = link_to filter_groups_path(sort: sort_value_recently_created) do
- = sort_title_recently_created
- = link_to filter_groups_path(sort: sort_value_oldest_created) do
- = sort_title_oldest_created
- = link_to filter_groups_path(sort: sort_value_recently_updated) do
- = sort_title_recently_updated
- = link_to filter_groups_path(sort: sort_value_oldest_updated) do
- = sort_title_oldest_updated
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ = _("Sort by")
+ - groups_sort_options_hash.each do |value, title|
+ %li.js-filter-sort-order
+ = link_to filter_groups_path(sort: value), class: ("is-active" if default_sort_by == value) do
+ = title
+ - if show_archive_options
+ %li.divider
+ %li.js-filter-archived-projects
+ = link_to group_children_path(@group, archived: nil), class: ("is-active" unless params[:archived].present?) do
+ Hide archived projects
+ %li.js-filter-archived-projects
+ = link_to group_children_path(@group, archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do
+ Show archived projects
+ %li.js-filter-archived-projects
+ = link_to group_children_path(@group, archived: 'only'), class: ("is-active" if params[:archived] == 'only') do
+ Show archived projects only
diff --git a/app/views/shared/groups/_empty_state.html.haml b/app/views/shared/groups/_empty_state.html.haml
new file mode 100644
index 00000000000..13bb4baee3f
--- /dev/null
+++ b/app/views/shared/groups/_empty_state.html.haml
@@ -0,0 +1,7 @@
+.groups-empty-state
+ = custom_icon("icon_empty_groups")
+
+ .text-content
+ %h4= s_("GroupsEmptyState|A group is a collection of several projects.")
+ %p= s_("GroupsEmptyState|If you organize your projects under a group, it works like a folder.")
+ %p= s_("GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group.")
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index b361ec86ced..321d8767d08 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -9,9 +9,9 @@
.controls.hidden-xs
- if can?(current_user, :admin_group, group)
= link_to edit_group_path(group), class: "btn" do
- = icon('cogs')
+ = sprite_icon('settings')
- = link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: 'Leave this group' do
+ = link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: s_("GroupsTree|Leave this group") do
= icon('sign-out')
.stats
@@ -28,7 +28,7 @@
.avatar-container.s40
= link_to group do
- = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+ = group_icon(group, class: "avatar s40 hidden-xs")
.title
= link_to group_name, group, class: 'group-name'
diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml
index 427595c47a5..aec8ecd1714 100644
--- a/app/views/shared/groups/_list.html.haml
+++ b/app/views/shared/groups/_list.html.haml
@@ -3,4 +3,4 @@
- groups.each_with_index do |group, i|
= render "shared/groups/group", group: group
- else
- .nothing-here-block No groups found
+ .nothing-here-block= s_("GroupsEmptyState|No groups found")
diff --git a/app/views/shared/groups/_search_form.html.haml b/app/views/shared/groups/_search_form.html.haml
index ad7a7faedf1..3f91263089a 100644
--- a/app/views/shared/groups/_search_form.html.haml
+++ b/app/views/shared/groups/_search_form.html.haml
@@ -1,2 +1,2 @@
-= form_tag request.path, method: :get, class: 'group-filter-form', id: 'group-filter-form' do |f|
- = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name...', class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2"
+= form_tag request.path, method: :get, class: 'group-filter-form append-right-10', id: 'group-filter-form' do |f|
+ = search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Filter by name...'), class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2"
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
index af6a499fadb..c80b179d525 100644
--- a/app/views/shared/hook_logs/_content.html.haml
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -11,7 +11,7 @@
= hook_log.trigger.singularize.titleize
%p
%strong Elapsed time:
- #{number_with_precision(hook_log.execution_duration, precision: 2)} ms
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
%p
%strong Request time:
= time_ago_with_tooltip(hook_log.created_at)
diff --git a/app/views/shared/icons/_add_new_project.svg b/app/views/shared/icons/_add_new_project.svg
index 3c1e15453df..cf8762944ca 100644
--- a/app/views/shared/icons/_add_new_project.svg
+++ b/app/views/shared/icons/_add_new_project.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M30 24a4 4 0 0 0-4 4v22a4 4 0 0 0 4 4h18a4 4 0 0 0 4-4V28a4 4 0 0 0-4-4H30zm0-4h18a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V28a8 8 0 0 1 8-8z"/><path fill="#FC6D26" d="M33 30h8a2 2 0 1 1 0 4h-8a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4z"/></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 24c-2.21 0-4 1.79-4 4v22c0 2.21 1.79 4 4 4h18c2.21 0 4-1.79 4-4V28c0-2.21-1.79-4-4-4H30zm0-4h18c4.418 0 8 3.582 8 8v22c0 4.418-3.582 8-8 8H30c-4.418 0-8-3.582-8-8V28c0-4.418 3.582-8 8-8z"/><path fill="#6B4FBB" d="M33 30h8c1.105 0 2 .895 2 2s-.895 2-2 2h-8c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2z"/></g></svg>
diff --git a/app/views/shared/icons/_express.svg b/app/views/shared/icons/_express.svg
index f2c94319f19..a51e81e5568 100644
--- a/app/views/shared/icons/_express.svg
+++ b/app/views/shared/icons/_express.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="27" height="32" viewBox="0 0 27 32" class="btn-template-icon icon-node-express">
- <g fill="none" fill-rule="evenodd" transform="translate(-3)">
- <rect width="32" height="32"/>
- <path fill="#353535" d="M4.19170065,16.2667139 C4.23142421,18.3323387 4.47969269,20.2489714 4.93651356,22.0166696 C5.39333443,23.7843677 6.09841693,25.3236323 7.05178222,26.6345096 C8.00514751,27.9453869 9.23655921,28.9781838 10.7460543,29.7329313 C12.2555493,30.4876788 14.1026668,30.8650469 16.2874623,30.8650469 C19.5050701,30.8650469 22.1764391,30.0209341 24.3016492,28.3326831 C26.4268593,26.644432 27.7476477,24.1120935 28.2640539,20.7355914 L29.4557545,20.7355914 C29.0187954,24.3107112 27.6086304,27.0813875 25.2252172,29.0477034 C22.841804,31.0140194 19.9023051,31.9971626 16.4066324,31.9971626 C14.0232191,32.0368861 11.9874175,31.659518 10.2991665,30.8650469 C8.61091547,30.0705759 7.23054269,28.9484023 6.15800673,27.4984926 C5.08547078,26.0485829 4.29101162,24.3404957 3.77460543,22.3741798 C3.25819923,20.4078639 3,18.2926164 3,16.0283738 C3,13.4860664 3.3773681,11.2218578 4.13211562,9.23568007 C4.88686314,7.24950238 5.87993709,5.57120741 7.11136726,4.20074481 C8.34279742,2.8302822 9.77282391,1.78755456 11.4014896,1.07253059 C13.0301553,0.357506621 14.6985195,0 16.4066324,0 C18.7900456,0 20.8457087,0.456814016 22.5736832,1.37045575 C24.3016578,2.28409749 25.7118228,3.4956477 26.8042206,5.00514275 C27.8966183,6.51463779 28.6910775,8.24258646 29.1876219,10.1890406 C29.6841663,12.1354947 29.8927118,14.1613656 29.8132647,16.2667139 L4.19170065,16.2667139 Z M28.6215641,15.0750133 C28.6215641,13.2080062 28.3633648,11.4304039 27.8469586,9.74215285 C27.3305524,8.05390181 26.5658855,6.57422163 25.5529349,5.30306791 C24.5399843,4.03191419 23.2787803,3.0289095 21.7692853,2.29402376 C20.2597903,1.55913801 18.5119801,1.19170065 16.5258024,1.19170065 C14.8574132,1.19170065 13.2982871,1.50948432 11.8483774,2.14506118 C10.3984676,2.78063804 9.12733299,3.70419681 8.03493526,4.9157652 C6.94253754,6.12733359 6.05870172,7.58715229 5.38340131,9.2952651 C4.70810089,11.0033779 4.31087132,12.9299414 4.19170065,15.0750133 L28.6215641,15.0750133 Z"/>
- </g>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="27" height="32" viewBox="0 0 27 32" class="btn-template-icon icon-node-express"><g fill="none" fill-rule="evenodd"><path d="M-3 0h32v32H-3z"/><path fill="#353535" d="M1.192 16.267c.04 2.065.288 3.982.745 5.75.456 1.767 1.16 3.307 2.115 4.618.953 1.31 2.185 2.343 3.694 3.098 1.51.755 3.357 1.132 5.54 1.132 3.22 0 5.89-.844 8.016-2.532 2.125-1.69 3.446-4.22 3.962-7.597h1.192c-.437 3.575-1.847 6.345-4.23 8.312-2.384 1.966-5.324 2.95-8.82 2.95-2.383.04-4.42-.338-6.107-1.133-1.69-.794-3.07-1.917-4.142-3.367-1.073-1.45-1.867-3.158-2.383-5.124C.258 20.408 0 18.294 0 16.028c0-2.542.377-4.806 1.132-6.792C1.887 7.25 2.88 5.57 4.112 4.2 5.34 2.83 6.77 1.79 8.4 1.074 10.03.358 11.698 0 13.406 0c2.383 0 4.44.457 6.167 1.37 1.728.914 3.138 2.126 4.23 3.635 1.093 1.51 1.887 3.238 2.384 5.184.496 1.945.705 3.97.625 6.077H1.193zm24.43-1.192c0-1.867-.26-3.645-.775-5.333-.516-1.688-1.28-3.168-2.294-4.44-1.013-1.27-2.274-2.273-3.784-3.008-1.51-.735-3.258-1.102-5.244-1.102-1.67 0-3.228.317-4.678.953-1.45.636-2.72 1.56-3.813 2.77-1.092 1.212-1.976 2.672-2.652 4.38-.675 1.708-1.072 3.635-1.19 5.78h24.43z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_autodevops.svg b/app/views/shared/icons/_icon_autodevops.svg
index 807ff27bb67..7e47c084bde 100644
--- a/app/views/shared/icons/_icon_autodevops.svg
+++ b/app/views/shared/icons/_icon_autodevops.svg
@@ -1,4 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="189" height="179" viewBox="0 0 189 179">
+<svg xmlns="http://www.w3.org/2000/svg" width="189" height="110" viewBox="0 0 189 179">
<g fill="none" fill-rule="evenodd">
<path fill="#FFFFFF" fill-rule="nonzero" d="M110.160166,47.6956996 L160.160166,47.6956996 C165.683013,47.6956996 170.160166,52.1728521 170.160166,57.6956996 L170.160166,117.6957 C170.160166,123.218547 165.683013,127.6957 160.160166,127.6957 L110.160166,127.6957 C104.637318,127.6957 100.160166,123.218547 100.160166,117.6957 L100.160166,57.6956996 C100.160166,52.1728521 104.637318,47.6956996 110.160166,47.6956996 Z" transform="rotate(10 135.16 87.696)"/>
<path fill="#EEEEEE" fill-rule="nonzero" d="M110.160166,51.6956996 C106.846457,51.6956996 104.160166,54.3819911 104.160166,57.6956996 L104.160166,117.6957 C104.160166,121.009408 106.846457,123.6957 110.160166,123.6957 L160.160166,123.6957 C163.473874,123.6957 166.160166,121.009408 166.160166,117.6957 L166.160166,57.6956996 C166.160166,54.3819911 163.473874,51.6956996 160.160166,51.6956996 L110.160166,51.6956996 Z M110.160166,47.6956996 L160.160166,47.6956996 C165.683013,47.6956996 170.160166,52.1728521 170.160166,57.6956996 L170.160166,117.6957 C170.160166,123.218547 165.683013,127.6957 160.160166,127.6957 L110.160166,127.6957 C104.637318,127.6957 100.160166,123.218547 100.160166,117.6957 L100.160166,57.6956996 C100.160166,52.1728521 104.637318,47.6956996 110.160166,47.6956996 Z" transform="rotate(10 135.16 87.696)"/>
diff --git a/app/views/shared/icons/_icon_hourglass.svg b/app/views/shared/icons/_icon_hourglass.svg
new file mode 100644
index 00000000000..fe7e497ce13
--- /dev/null
+++ b/app/views/shared/icons/_icon_hourglass.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></svg>
diff --git a/app/views/shared/icons/_lightbulb.svg b/app/views/shared/icons/_lightbulb.svg
new file mode 100644
index 00000000000..2fcc4c65f99
--- /dev/null
+++ b/app/views/shared/icons/_lightbulb.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#6B4FBB" d="M33 52h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm1 5h10c1.105 0 2 .895 2 2s-.895 2-2 2H34c-1.105 0-2-.895-2-2s.895-2 2-2z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M45.542 46.932l.346-2.36c.198-1.348.737-2.623 1.566-3.705 3.025-3.946 4.485-7.29 4.547-9.96C52.153 24.41 46.843 20 39 20c-7.777 0-13 4.374-13 11 0 2.4 1.462 5.73 4.573 9.846.815 1.08 1.343 2.345 1.536 3.683l.353 2.456 13.08-.054zm-17.038.624L28.15 45.1c-.097-.67-.36-1.303-.768-1.842C23.794 38.51 22 34.424 22 31c0-9.39 7.61-15 17-15s17.218 5.614 17 15c-.085 3.64-1.875 7.74-5.37 12.3-.416.54-.685 1.18-.784 1.853l-.346 2.36c-.288 1.958-1.963 3.41-3.942 3.42l-13.08.053c-1.994.008-3.69-1.455-3.974-3.43z"/><path fill="#6B4FBB" d="M41 38.732c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268zm-6 0c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268z"/></g></svg>
diff --git a/app/views/shared/icons/_rails.svg b/app/views/shared/icons/_rails.svg
index 0bb09a705df..852bd183cc7 100644
--- a/app/views/shared/icons/_rails.svg
+++ b/app/views/shared/icons/_rails.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="32" height="20" viewBox="0 0 32 20" class="btn-template-icon icon-rails">
- <g fill="none" fill-rule="evenodd" transform="translate(0 -6)">
- <rect width="32" height="32"/>
- <path fill="#C00" fill-rule="nonzero" d="M0.984615385,25.636044 C0.984615385,25.636044 1.40659341,21.4725275 4.36043956,16.5494505 C7.31428571,11.6263736 12.3498901,7.8989011 16.4430769,7.53318681 C24.5872527,6.71736264 31.9015385,14.0175824 31.9015385,14.0175824 C31.9015385,14.0175824 31.6624176,14.1863736 31.4092308,14.3973626 C23.4197802,8.48967033 18.5389011,11.2747253 17.0057143,12.0202198 C9.97274725,15.9446154 12.0967033,25.636044 12.0967033,25.636044 L0.984615385,25.636044 Z M24.1371429,8.32087912 C23.687033,8.13802198 23.2369231,7.96923077 22.7727473,7.81450549 L22.829011,6.88615385 C23.7151648,7.13934066 24.0668132,7.30813187 24.1934066,7.37846154 L24.1371429,8.32087912 Z M22.8008791,11.3028571 C23.250989,11.330989 23.7151648,11.3872527 24.1934066,11.4857143 L24.1371429,12.3578022 C23.672967,12.2593407 23.2087912,12.2030769 22.7446154,12.189011 L22.8008791,11.3028571 Z M17.5964835,6.91428571 C17.1885714,6.91428571 16.7806593,6.92835165 16.3727473,6.97054945 L16.1054945,6.14065934 C16.5696703,6.0843956 17.0197802,6.05626374 17.4558242,6.05626374 L17.7371429,6.91428571 C17.6949451,6.91428571 17.6386813,6.91428571 17.5964835,6.91428571 Z M18.2716484,12.0905495 C18.6232967,11.9358242 19.0312088,11.7810989 19.5094505,11.6404396 L19.8189011,12.5687912 C19.410989,12.6953846 19.0030769,12.8641758 18.5951648,13.0610989 L18.2716484,12.0905495 Z M11.8857143,8.39120879 C11.52,8.57406593 11.1683516,8.78505495 10.8026374,9.01010989 L10.1556044,8.02549451 C10.5353846,7.80043956 10.9010989,7.60351648 11.2527473,7.42065934 L11.8857143,8.39120879 Z M14.7692308,14.7208791 C15.0224176,14.3973626 15.3178022,14.0738462 15.6413187,13.7784615 L16.2742857,14.7349451 C15.9648352,15.0584615 15.6835165,15.381978 15.4443956,15.7336264 L14.7692308,14.7208791 Z M12.7296703,19.2501099 C12.8421978,18.7437363 12.9687912,18.2232967 13.1516484,17.7028571 L14.1643956,18.5046154 C14.0237363,19.0531868 13.9252747,19.6017582 13.869011,20.1503297 L12.7296703,19.2501099 Z M6.56879121,12.5687912 C6.23120879,12.9204396 5.90769231,13.3002198 5.61230769,13.68 L4.52923077,12.7516484 C4.85274725,12.4 5.2043956,12.0483516 5.57010989,11.6967033 L6.56879121,12.5687912 Z M2.32087912,18.8562637 C2.09582418,19.3767033 1.80043956,20.0659341 1.61758242,20.5441758 L0,19.9534066 C0.140659341,19.5736264 0.436043956,18.8703297 0.703296703,18.2654945 L2.32087912,18.8562637 Z M12.5186813,22.8228571 L14.0378022,23.3714286 C14.1221978,24.0325275 14.2487912,24.6514286 14.3753846,25.2 L12.6874725,24.5951648 C12.6171429,24.1731868 12.5468132,23.5683516 12.5186813,22.8228571 Z"/>
- </g>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="20" viewBox="0 0 32 20" class="btn-template-icon icon-rails"><g fill="none" fill-rule="evenodd"><path d="M0-6h32v32H0z"/><path fill="#c00" fill-rule="nonzero" d="M.985 19.636s.422-4.163 3.375-9.087c2.954-4.924 7.99-8.65 12.083-9.017 8.144-.816 15.46 6.485 15.46 6.485s-.24.168-.494.38C23.42 2.49 18.54 5.274 17.005 6.02c-7.033 3.925-4.91 13.616-4.91 13.616H.987zM24.137 2.32c-.45-.182-.9-.35-1.364-.505l.056-.93c.885.254 1.237.423 1.363.493l-.056.943zM22.8 5.304c.45.028.915.084 1.393.183l-.056.872c-.464-.1-.928-.155-1.392-.17l.056-.885zM17.597.913c-.407 0-.815.015-1.223.058l-.268-.83c.465-.056.915-.084 1.35-.084l.282.858h-.14zm.676 5.178c.35-.154.76-.31 1.237-.45l.31.93c-.41.125-.817.294-1.225.49l-.323-.97zm-6.386-3.7c-.366.184-.718.395-1.083.62l-.647-.985c.38-.225.745-.42 1.097-.604l.633.97zm2.883 6.33c.252-.323.548-.646.87-.942l.634.957c-.31.323-.59.647-.83 1L14.77 8.72zm-2.04 4.53c.112-.506.24-1.027.422-1.547l1.012.802c-.14.548-.24 1.097-.295 1.645l-1.14-.9zM6.57 6.57c-.34.35-.662.73-.958 1.11L4.53 6.752c.323-.352.674-.704 1.04-1.055l1 .872zm-4.25 6.286c-.224.52-.52 1.21-.702 1.688L0 13.954c.14-.38.436-1.084.703-1.69l1.618.592zm10.2 3.967l1.518.548c.084.663.21 1.28.337 1.83l-1.688-.605c-.07-.422-.14-1.027-.168-1.772z"/></g></svg>
diff --git a/app/views/shared/icons/_spring.svg b/app/views/shared/icons/_spring.svg
index 508349aa456..ccf18749029 100644
--- a/app/views/shared/icons/_spring.svg
+++ b/app/views/shared/icons/_spring.svg
@@ -1,6 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="btn-template-icon icon-java-spring">
- <g fill="none" fill-rule="evenodd">
- <rect width="32" height="32"/>
- <path fill="#70AD51" d="M5.46647617,27.9932117 C6.0517027,28.4658996 6.91159892,28.3777063 7.38425926,27.7914452 C7.85922261,27.2048452 7.76991326,26.3449044 7.18398981,25.8699411 C6.59874295,25.3956543 5.74015536,25.4869934 5.26383884,26.0722403 C4.81393367,26.6267596 4.87238621,27.4284565 5.37913494,27.9159868 L5.11431334,27.6818383 C1.97157151,24.7616933 0,20.5966301 0,15.9782542 C0,7.16842834 7.16775175,0 15.9796074,0 C20.4586065,0 24.5113565,1.8565519 27.4145869,4.8362365 C28.0749348,3.93840692 28.6466499,2.93435335 29.115524,1.82069284 C31.1513712,7.93770658 32.3482517,13.0811131 31.909824,17.1311567 C31.3178113,25.4044499 24.4017495,31.9585382 15.9796074,31.9585382 C12.0682639,31.9585382 8.48438805,30.5444735 5.7042963,28.2034861 L5.46647617,27.9932117 Z M29.0471888,23.0106888 C33.0546075,17.6737787 30.8211972,9.04527781 28.9612624,3.529749 C27.3029502,6.98304378 23.2217836,9.62375882 19.6981239,10.4613722 C16.3950312,11.2482417 13.4715032,10.6021021 10.4153644,11.7780085 C3.44517575,14.457289 3.55613585,22.7698242 7.39373146,24.6365249 C7.39711439,24.6392312 7.62444728,24.7616933 7.62174094,24.7576338 C7.62309411,24.7562806 13.2658211,23.6358542 16.3862356,22.4843049 C20.9450718,20.7996058 25.9524846,16.6494275 27.5986182,11.8273993 C26.723116,16.8415779 22.4179995,21.6669891 18.093262,23.8828081 C15.7908399,25.0648038 14.0005934,25.3279957 10.2123886,26.6385428 C9.74892722,26.798217 9.38492397,26.9538318 9.38492397,26.9538318 C10.3463526,26.7948341 11.301692,26.7420604 11.301692,26.7420604 C16.6954354,26.4869875 25.1087819,28.2582896 29.0471888,23.0106888 Z"/>
- </g>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="btn-template-icon icon-java-spring"><g fill="none" fill-rule="evenodd"><path d="M0 0h32v32H0z"/><path fill="#70AD51" d="M5.466 27.993c.586.473 1.446.385 1.918-.202.475-.585.386-1.445-.2-1.92-.585-.474-1.444-.383-1.92.202-.45.555-.392 1.356.115 1.844l-.266-.234C1.972 24.762 0 20.597 0 15.978 0 7.168 7.168 0 15.98 0c4.48 0 8.53 1.857 11.435 4.836.66-.898 1.232-1.902 1.7-3.015 2.036 6.118 3.233 11.26 2.795 15.31-.592 8.274-7.508 14.83-15.93 14.83-3.912 0-7.496-1.416-10.276-3.757l-.238-.21zm23.58-4.982c4.01-5.336 1.775-13.965-.085-19.48-1.657 3.453-5.738 6.094-9.262 6.93-3.303.788-6.226.142-9.283 1.318-6.97 2.68-6.86 10.992-3.02 12.86.002 0 .23.124.227.12 0-.002 5.644-1.122 8.764-2.274 4.56-1.684 9.566-5.835 11.213-10.657-.877 5.015-5.182 9.84-9.507 12.056-2.302 1.182-4.092 1.445-7.88 2.756-.464.158-.828.314-.828.314.96-.16 1.917-.212 1.917-.212 5.393-.255 13.807 1.516 17.745-3.73z"/></g></svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index d3f0aa2d339..8442d7ff4a2 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,4 +1,3 @@
-- finder = controller.controller_name == 'issues' ? issues_finder : merge_requests_finder
- boards_page = controller.controller_name == 'boards'
.issues-filters
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
deleted file mode 100644
index d2b62557e03..00000000000
--- a/app/views/shared/issuable/_participants.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- participants_row = 7
-- participants_size = participants.size
-- participants_extra = participants_size - participants_row
-.block.participants
- .sidebar-collapsed-icon
- = icon('users')
- %span
- = participants.count
- .title.hide-collapsed
- = pluralize participants.count, "participant"
- .hide-collapsed.participants-list
- - participants.each do |participant|
- .participants-author.js-participants-author
- = link_to_member(@project, participant, name: false, size: 24, lazy_load: true)
- - if participants_extra > 0
- .hide-collapsed.participants-more
- %a.js-participants-more{ href: "#", data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } }
- + #{participants_extra} more
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 161b1c9fd72..fabb17c7340 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -25,7 +25,6 @@
%ul.tokens-container.list-unstyled
%li.input-token
%input.form-control.filtered-search{ search_filter_input_options(type) }
- = icon('filter')
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 7b7411b1e23..e0009a35b9f 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -123,17 +123,10 @@
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point
- = render "shared/issuable/participants", participants: issuable.participants(current_user)
+ .js-sidebar-participants-entry-point
+
- if current_user
- - subscribed = issuable.subscribed?(current_user, @project)
- .block.light.subscription{ data: { url: toggle_subscription_path(issuable) } }
- .sidebar-collapsed-icon
- = icon('rss', 'aria-hidden': 'true')
- %span.issuable-header-text.hide-collapsed.pull-left
- Notifications
- - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
- %span= subscribed ? 'Unsubscribe' : 'Subscribe'
+ .js-sidebar-subscriptions-entry-point
- project_ref = cross_project_reference(@project, issuable)
.block.project-reference
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index bcdad3c153a..5868c52566d 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -4,7 +4,7 @@
- dom_id = "group_member_#{group_link.id}"
%li.member.group_member{ id: dom_id }
%span.list-item-name
- = image_tag group_icon(group), class: "avatar s40", alt: ''
+ = group_icon(group, class: "avatar s40", alt: '')
%strong
= link_to group.full_name, group_path(group)
.cgray
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 951b4dd7b36..2c27dd638a7 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -104,7 +104,6 @@
class: 'btn btn-remove prepend-left-10'
- else
= link_to member,
- remote: true,
method: :delete,
data: { confirm: remove_member_message(member) },
class: 'btn btn-remove prepend-left-10',
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 305e2542281..7ba8f9d4313 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -49,6 +49,13 @@
= link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-xs btn-grouped" do
Edit
\
+
+ - if @project.group
+ = link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "Promoting this milestone will make it available for all projects inside the group. Existing project milestones with the same name will be merged. Are you sure?", toggle: "tooltip" }, method: :post do
+ Promote
+
= link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped"
+
= link_to project_milestone_path(milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove btn-grouped" do
Delete
+
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index f03e0ab154c..4f51455c26e 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -85,6 +85,22 @@
Closed:
= milestone.issues_visible_to_user(current_user).closed.count
+ .block.time_spent
+ .sidebar-collapsed-icon
+ = custom_icon('icon_hourglass')
+ %span.collapsed-milestone-total-time-spent
+ - if milestone.human_total_issue_time_spent
+ = milestone.human_total_issue_time_spent
+ - else
+ = _("None")
+ .title.hide-collapsed
+ = _("Total issue time spent")
+ .value.hide-collapsed
+ - if milestone.human_total_issue_time_spent
+ %span.bold= milestone.human_total_issue_time_spent
+ - else
+ %span.no-value= _("No time spent")
+
.block.merge-requests
.sidebar-collapsed-icon
%strong
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
index 80432a73e4e..3d917346f6b 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -1,5 +1,5 @@
- @sort ||= sort_value_latest_activity
-.dropdown
+.dropdown.js-project-filter-dropdown-wrap
- toggle_text = projects_sort_options_hash[@sort]
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 52a8fe8bb67..98bfc7c4d36 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -20,7 +20,7 @@
= project_icon(project, alt: '', class: 'avatar project-avatar s40')
.project-details
%h3.prepend-top-0.append-bottom-0
- = link_to project_path(project), class: dom_class(project) do
+ = link_to project_path(project), class: 'text-plain' do
%span.project-full-name
%span.namespace-name
- if project.namespace && !skip_namespace
diff --git a/app/views/shared/repo/_editable_mode.html.haml b/app/views/shared/repo/_editable_mode.html.haml
deleted file mode 100644
index 73fdb8b523f..00000000000
--- a/app/views/shared/repo/_editable_mode.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-.editable-mode
- %repo-edit-button
diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml
index 87fa2007d16..87e8c416194 100644
--- a/app/views/shared/repo/_repo.html.haml
+++ b/app/views/shared/repo/_repo.html.haml
@@ -1,7 +1,13 @@
-#repo{ data: { url: content_url,
+- @no_container = true;
+#repo{ data: { root: @path.empty?.to_s,
+ root_url: project_tree_path(project),
+ url: content_url,
+ current_branch: @ref,
+ ref: @commit.id,
project_name: project.name,
- refs_url: refs_project_path(project, format: :json),
project_url: project_path(project),
project_id: project.id,
+ new_merge_request_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '' }),
can_commit: (!!can_push_branch?(project, @ref)).to_s,
- on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } }
+ on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s,
+ current_path: @path } }
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index 8bbaf431536..ae437dd16d6 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -7,3 +7,4 @@
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
= label_tag ("#{prefix}_scopes_#{scope}"), scope
%span= t(scope, scope: [:doorkeeper, :scopes])
+ .scope-description= t scope, scope: [:doorkeeper, :scope_desc]
diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml
index eff6c80d144..55799e10a46 100644
--- a/app/views/users/_groups.html.haml
+++ b/app/views/users/_groups.html.haml
@@ -2,4 +2,4 @@
- groups.each do |group|
= link_to group, class: 'profile-groups-avatars inline', title: group.name do
.avatar-container.s40
- = image_tag group_icon(group), class: 'avatar group-avatar s40'
+ = group_icon(group, class: 'avatar group-avatar s40')
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 6c3cd6ecefe..cc59f8660fd 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -4,6 +4,9 @@
- page_description @user.bio
- header_title @user.name, user_path(@user)
- @no_container = true
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag 'common_d3'
+ = webpack_bundle_tag 'users'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
diff --git a/app/workers/cluster_install_app_worker.rb b/app/workers/cluster_install_app_worker.rb
new file mode 100644
index 00000000000..899aed904e4
--- /dev/null
+++ b/app/workers/cluster_install_app_worker.rb
@@ -0,0 +1,11 @@
+class ClusterInstallAppWorker
+ include Sidekiq::Worker
+ include ClusterQueue
+ include ClusterApplications
+
+ def perform(app_name, app_id)
+ find_application(app_name, app_id) do |app|
+ Clusters::Applications::InstallService.new(app).execute
+ end
+ end
+end
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
index 63300b58a25..b01f9708424 100644
--- a/app/workers/cluster_provision_worker.rb
+++ b/app/workers/cluster_provision_worker.rb
@@ -3,8 +3,10 @@ class ClusterProvisionWorker
include ClusterQueue
def perform(cluster_id)
- Gcp::Cluster.find_by_id(cluster_id).try do |cluster|
- Ci::ProvisionClusterService.new.execute(cluster)
+ Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
+ cluster.provider.try do |provider|
+ Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp?
+ end
end
end
end
diff --git a/app/workers/cluster_wait_for_app_installation_worker.rb b/app/workers/cluster_wait_for_app_installation_worker.rb
new file mode 100644
index 00000000000..4bb8c293e5d
--- /dev/null
+++ b/app/workers/cluster_wait_for_app_installation_worker.rb
@@ -0,0 +1,14 @@
+class ClusterWaitForAppInstallationWorker
+ include Sidekiq::Worker
+ include ClusterQueue
+ include ClusterApplications
+
+ INTERVAL = 10.seconds
+ TIMEOUT = 20.minutes
+
+ def perform(app_name, app_id)
+ find_application(app_name, app_id) do |app|
+ Clusters::Applications::CheckInstallationProgressService.new(app).execute
+ end
+ end
+end
diff --git a/app/workers/concerns/cluster_applications.rb b/app/workers/concerns/cluster_applications.rb
new file mode 100644
index 00000000000..24ecaa0b52f
--- /dev/null
+++ b/app/workers/concerns/cluster_applications.rb
@@ -0,0 +1,9 @@
+module ClusterApplications
+ extend ActiveSupport::Concern
+
+ included do
+ def find_application(app_name, id, &blk)
+ Clusters::Cluster::APPLICATIONS[app_name].find(id).try(&blk)
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/notify_upon_death.rb b/app/workers/concerns/gitlab/github_import/notify_upon_death.rb
new file mode 100644
index 00000000000..3d7120665b6
--- /dev/null
+++ b/app/workers/concerns/gitlab/github_import/notify_upon_death.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # NotifyUponDeath can be included into a GitHub worker class if it should
+ # notify any JobWaiter instances upon being moved to the Sidekiq dead queue.
+ #
+ # Note that this will only notify the waiter upon graceful termination, a
+ # SIGKILL will still result in the waiter _not_ being notified.
+ #
+ # Workers including this module must have jobs passed where the last
+ # argument is the key to notify, as a String.
+ module NotifyUponDeath
+ extend ActiveSupport::Concern
+
+ included do
+ # If a job is being exhausted we still want to notify the
+ # AdvanceStageWorker. This prevents the entire import from getting stuck
+ # just because 1 job threw too many errors.
+ sidekiq_retries_exhausted do |job|
+ args = job['args']
+ jid = job['jid']
+
+ if args.length == 3 && (key = args.last) && key.is_a?(String)
+ JobWaiter.notify(key, jid)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
new file mode 100644
index 00000000000..67e36c811de
--- /dev/null
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # ObjectImporter defines the base behaviour for every Sidekiq worker that
+ # imports a single resource such as a note or pull request.
+ module ObjectImporter
+ extend ActiveSupport::Concern
+
+ included do
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include ReschedulingMethods
+ include NotifyUponDeath
+ end
+
+ # project - An instance of `Project` to import the data into.
+ # client - An instance of `Gitlab::GithubImport::Client`
+ # hash - A Hash containing the details of the object to import.
+ def import(project, client, hash)
+ object = representation_class.from_json_hash(hash)
+
+ importer_class.new(object, project, client).execute
+
+ counter.increment(project: project.path_with_namespace)
+ end
+
+ def counter
+ @counter ||= Gitlab::Metrics.counter(counter_name, counter_description)
+ end
+
+ # Returns the representation class to use for the object. This class must
+ # define the class method `from_json_hash`.
+ def representation_class
+ raise NotImplementedError
+ end
+
+ # Returns the class to use for importing the object.
+ def importer_class
+ raise NotImplementedError
+ end
+
+ # Returns the name (as a Symbol) of the Prometheus counter.
+ def counter_name
+ raise NotImplementedError
+ end
+
+ # Returns the description (as a String) of the Prometheus counter.
+ def counter_description
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/queue.rb b/app/workers/concerns/gitlab/github_import/queue.rb
new file mode 100644
index 00000000000..a2bee361b86
--- /dev/null
+++ b/app/workers/concerns/gitlab/github_import/queue.rb
@@ -0,0 +1,16 @@
+module Gitlab
+ module GithubImport
+ module Queue
+ extend ActiveSupport::Concern
+
+ included do
+ # If a job produces an error it may block a stage from advancing
+ # forever. To prevent this from happening we prevent jobs from going to
+ # the dead queue. This does mean some resources may not be imported, but
+ # this is better than a project being stuck in the "import" state
+ # forever.
+ sidekiq_options queue: 'github_importer', dead: false, retry: 5
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
new file mode 100644
index 00000000000..692ca6b7f42
--- /dev/null
+++ b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # Module that provides methods shared by the various workers used for
+ # importing GitHub projects.
+ module ReschedulingMethods
+ # project_id - The ID of the GitLab project to import the note into.
+ # hash - A Hash containing the details of the GitHub object to imoprt.
+ # notify_key - The Redis key to notify upon completion, if any.
+ def perform(project_id, hash, notify_key = nil)
+ project = Project.find_by(id: project_id)
+
+ return notify_waiter(notify_key) unless project
+
+ client = GithubImport.new_client_for(project, parallel: true)
+
+ if try_import(project, client, hash)
+ notify_waiter(notify_key)
+ else
+ # In the event of hitting the rate limit we want to reschedule the job
+ # so its retried after our rate limit has been reset.
+ self.class
+ .perform_in(client.rate_limit_resets_in, project.id, hash, notify_key)
+ end
+ end
+
+ def try_import(*args)
+ import(*args)
+ true
+ rescue RateLimitError
+ false
+ end
+
+ def notify_waiter(key = nil)
+ JobWaiter.notify(key, jid) if key
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb
new file mode 100644
index 00000000000..147c8c8d683
--- /dev/null
+++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module StageMethods
+ # project_id - The ID of the GitLab project to import the data into.
+ def perform(project_id)
+ return unless (project = find_project(project_id))
+
+ client = GithubImport.new_client_for(project)
+
+ try_import(client, project)
+ end
+
+ # client - An instance of Gitlab::GithubImport::Client.
+ # project - An instance of Project.
+ def try_import(client, project)
+ import(client, project)
+ rescue RateLimitError
+ self.class.perform_in(client.rate_limit_resets_in, project.id)
+ end
+
+ def find_project(id)
+ # If the project has been marked as failed we want to bail out
+ # automatically.
+ Project.import_started.find_by(id: id)
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/project_start_import.rb b/app/workers/concerns/project_start_import.rb
new file mode 100644
index 00000000000..0704ebbb0fd
--- /dev/null
+++ b/app/workers/concerns/project_start_import.rb
@@ -0,0 +1,9 @@
+module ProjectStartImport
+ def start(project)
+ if project.import_started? && project.import_jid == self.jid
+ return true
+ end
+
+ project.import_start
+ end
+end
diff --git a/app/workers/create_pipeline_worker.rb b/app/workers/create_pipeline_worker.rb
new file mode 100644
index 00000000000..865ad1ba420
--- /dev/null
+++ b/app/workers/create_pipeline_worker.rb
@@ -0,0 +1,16 @@
+class CreatePipelineWorker
+ include Sidekiq::Worker
+ include PipelineQueue
+
+ enqueue_in group: :creation
+
+ def perform(project_id, user_id, ref, source, params = {})
+ project = Project.find(project_id)
+ user = User.find(user_id)
+ params = params.deep_symbolize_keys
+
+ Ci::CreatePipelineService
+ .new(project, user, ref: ref)
+ .execute(source, **params)
+ end
+end
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
new file mode 100644
index 00000000000..877f88c043f
--- /dev/null
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # AdvanceStageWorker is a worker used by the GitHub importer to wait for a
+ # number of jobs to complete, without blocking a thread. Once all jobs have
+ # been completed this worker will advance the import process to the next
+ # stage.
+ class AdvanceStageWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: 'github_importer_advance_stage', dead: false
+
+ INTERVAL = 30.seconds.to_i
+
+ # The number of seconds to wait (while blocking the thread) before
+ # continueing to the next waiter.
+ BLOCKING_WAIT_TIME = 5
+
+ # The known importer stages and their corresponding Sidekiq workers.
+ STAGES = {
+ issues_and_diff_notes: Stage::ImportIssuesAndDiffNotesWorker,
+ notes: Stage::ImportNotesWorker,
+ finish: Stage::FinishImportWorker
+ }.freeze
+
+ # project_id - The ID of the project being imported.
+ # waiters - A Hash mapping Gitlab::JobWaiter keys to the number of
+ # remaining jobs.
+ # next_stage - The name of the next stage to start when all jobs have been
+ # completed.
+ def perform(project_id, waiters, next_stage)
+ return unless (project = find_project(project_id))
+
+ new_waiters = wait_for_jobs(waiters)
+
+ if new_waiters.empty?
+ # We refresh the import JID here so workers importing individual
+ # resources (e.g. notes) don't have to do this all the time, reducing
+ # the pressure on Redis. We _only_ do this once all jobs are done so
+ # we don't get stuck forever if one or more jobs failed to notify the
+ # JobWaiter.
+ project.refresh_import_jid_expiration
+
+ STAGES.fetch(next_stage.to_sym).perform_async(project_id)
+ else
+ self.class.perform_in(INTERVAL, project_id, new_waiters, next_stage)
+ end
+ end
+
+ def wait_for_jobs(waiters)
+ waiters.each_with_object({}) do |(key, remaining), new_waiters|
+ waiter = JobWaiter.new(remaining, key)
+
+ # We wait for a brief moment of time so we don't reschedule if we can
+ # complete the work fast enough.
+ waiter.wait(BLOCKING_WAIT_TIME)
+
+ next unless waiter.jobs_remaining.positive?
+
+ new_waiters[waiter.key] = waiter.jobs_remaining
+ end
+ end
+
+ def find_project(id)
+ # We only care about the import JID so we can refresh it. We also only
+ # want the project if it hasn't been marked as failed yet. It's possible
+ # the import gets marked as stuck when jobs of the current stage failed
+ # somehow.
+ Project.select(:import_jid).import_started.find_by(id: id)
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/import_diff_note_worker.rb b/app/workers/gitlab/github_import/import_diff_note_worker.rb
new file mode 100644
index 00000000000..ef2a74c51c5
--- /dev/null
+++ b/app/workers/gitlab/github_import/import_diff_note_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class ImportDiffNoteWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::DiffNote
+ end
+
+ def importer_class
+ Importer::DiffNoteImporter
+ end
+
+ def counter_name
+ :github_importer_imported_diff_notes
+ end
+
+ def counter_description
+ 'The number of imported GitHub pull request review comments'
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/import_issue_worker.rb b/app/workers/gitlab/github_import/import_issue_worker.rb
new file mode 100644
index 00000000000..1b081ae5966
--- /dev/null
+++ b/app/workers/gitlab/github_import/import_issue_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class ImportIssueWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::Issue
+ end
+
+ def importer_class
+ Importer::IssueAndLabelLinksImporter
+ end
+
+ def counter_name
+ :github_importer_imported_issues
+ end
+
+ def counter_description
+ 'The number of imported GitHub issues'
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/import_note_worker.rb b/app/workers/gitlab/github_import/import_note_worker.rb
new file mode 100644
index 00000000000..d2b4c36a5b9
--- /dev/null
+++ b/app/workers/gitlab/github_import/import_note_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class ImportNoteWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::Note
+ end
+
+ def importer_class
+ Importer::NoteImporter
+ end
+
+ def counter_name
+ :github_importer_imported_notes
+ end
+
+ def counter_description
+ 'The number of imported GitHub comments'
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/import_pull_request_worker.rb b/app/workers/gitlab/github_import/import_pull_request_worker.rb
new file mode 100644
index 00000000000..62a6da152a3
--- /dev/null
+++ b/app/workers/gitlab/github_import/import_pull_request_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class ImportPullRequestWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::PullRequest
+ end
+
+ def importer_class
+ Importer::PullRequestImporter
+ end
+
+ def counter_name
+ :github_importer_imported_pull_requests
+ end
+
+ def counter_description
+ 'The number of imported GitHub pull requests'
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
new file mode 100644
index 00000000000..45a38927225
--- /dev/null
+++ b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class RefreshImportJidWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+
+ # The interval to schedule new instances of this job at.
+ INTERVAL = 1.minute.to_i
+
+ def self.perform_in_the_future(*args)
+ perform_in(INTERVAL, *args)
+ end
+
+ # project_id - The ID of the project that is being imported.
+ # check_job_id - The ID of the job for which to check the status.
+ def perform(project_id, check_job_id)
+ return unless (project = find_project(project_id))
+
+ if SidekiqStatus.running?(check_job_id)
+ # As long as the repository is being cloned we want to keep refreshing
+ # the import JID status.
+ project.refresh_import_jid_expiration
+ self.class.perform_in_the_future(project_id, check_job_id)
+ end
+
+ # If the job is no longer running there's nothing else we need to do. If
+ # the clone job completed successfully it will have scheduled the next
+ # stage, if it died there's nothing we can do anyway.
+ end
+
+ def find_project(id)
+ Project.select(:import_jid).import_started.find_by(id: id)
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/finish_import_worker.rb b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
new file mode 100644
index 00000000000..1a09497780a
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class FinishImportWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include StageMethods
+
+ # project - An instance of Project.
+ def import(_, project)
+ project.after_import
+ report_import_time(project)
+ end
+
+ def report_import_time(project)
+ duration = Time.zone.now - project.created_at
+ path = project.path_with_namespace
+
+ histogram.observe({ project: path }, duration)
+ counter.increment
+
+ logger.info("GitHub importer finished for #{path} in #{duration.round(2)} seconds")
+ end
+
+ def histogram
+ @histogram ||= Gitlab::Metrics.histogram(
+ :github_importer_total_duration_seconds,
+ 'Total time spent importing GitHub projects, in seconds'
+ )
+ end
+
+ def counter
+ @counter ||= Gitlab::Metrics.counter(
+ :github_importer_imported_projects,
+ 'The number of imported GitHub projects'
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
new file mode 100644
index 00000000000..f8a3684c6ba
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class ImportBaseDataWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include StageMethods
+
+ # These importers are fast enough that we can just run them in the same
+ # thread.
+ IMPORTERS = [
+ Importer::LabelsImporter,
+ Importer::MilestonesImporter,
+ Importer::ReleasesImporter
+ ].freeze
+
+ # client - An instance of Gitlab::GithubImport::Client.
+ # project - An instance of Project.
+ def import(client, project)
+ IMPORTERS.each do |klass|
+ klass.new(project, client).execute
+ end
+
+ project.refresh_import_jid_expiration
+
+ ImportPullRequestsWorker.perform_async(project.id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
new file mode 100644
index 00000000000..e110b7c1c36
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class ImportIssuesAndDiffNotesWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include StageMethods
+
+ # The importers to run in this stage. Issues can't be imported earlier
+ # on as we also use these to enrich pull requests with assigned labels.
+ IMPORTERS = [
+ Importer::IssuesImporter,
+ Importer::DiffNotesImporter
+ ].freeze
+
+ # client - An instance of Gitlab::GithubImport::Client.
+ # project - An instance of Project.
+ def import(client, project)
+ waiters = IMPORTERS.each_with_object({}) do |klass, hash|
+ waiter = klass.new(project, client).execute
+ hash[waiter.key] = waiter.jobs_remaining
+ end
+
+ AdvanceStageWorker.perform_async(project.id, waiters, :notes)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/import_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
new file mode 100644
index 00000000000..9810ed25cf9
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class ImportNotesWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include StageMethods
+
+ # client - An instance of Gitlab::GithubImport::Client.
+ # project - An instance of Project.
+ def import(client, project)
+ waiter = Importer::NotesImporter
+ .new(project, client)
+ .execute
+
+ AdvanceStageWorker.perform_async(
+ project.id,
+ { waiter.key => waiter.jobs_remaining },
+ :finish
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
new file mode 100644
index 00000000000..c531f26e897
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class ImportPullRequestsWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include StageMethods
+
+ # client - An instance of Gitlab::GithubImport::Client.
+ # project - An instance of Project.
+ def import(client, project)
+ waiter = Importer::PullRequestsImporter
+ .new(project, client)
+ .execute
+
+ project.refresh_import_jid_expiration
+
+ AdvanceStageWorker.perform_async(
+ project.id,
+ { waiter.key => waiter.jobs_remaining },
+ :issues_and_diff_notes
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/stage/import_repository_worker.rb b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
new file mode 100644
index 00000000000..aa5762e773d
--- /dev/null
+++ b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Stage
+ class ImportRepositoryWorker
+ include Sidekiq::Worker
+ include GithubImport::Queue
+ include StageMethods
+
+ # client - An instance of Gitlab::GithubImport::Client.
+ # project - An instance of Project.
+ def import(client, project)
+ # In extreme cases it's possible for a clone to take more than the
+ # import job expiration time. To work around this we schedule a
+ # separate job that will periodically run and refresh the import
+ # expiration time.
+ RefreshImportJidWorker.perform_in_the_future(project.id, jid)
+
+ importer = Importer::RepositoryImporter.new(project, client)
+
+ return unless importer.execute
+
+ counter.increment
+
+ ImportBaseDataWorker.perform_async(project.id)
+ end
+
+ def counter
+ Gitlab::Metrics.counter(
+ :github_importer_imported_repositories,
+ 'The number of imported GitHub repositories'
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 3dd14466994..311fc187e49 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -104,6 +104,7 @@ class IrkerWorker
parents = commit.parents
# Return old value if there's no new one
return push_data['before'] if parents.empty?
+
# Or return the first parent-commit
parents[0].id
end
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index d7087f20dfc..7320db1065e 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -9,7 +9,7 @@ class PipelineScheduleWorker
pipeline = Ci::CreatePipelineService.new(schedule.project,
schedule.owner,
ref: schedule.ref)
- .execute(:schedule, save_on_errors: false, schedule: schedule)
+ .execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
schedule.deactivate! unless pipeline.persisted?
rescue => e
diff --git a/app/workers/project_migrate_hashed_storage_worker.rb b/app/workers/project_migrate_hashed_storage_worker.rb
index ca276d7801c..127aa6b9d7d 100644
--- a/app/workers/project_migrate_hashed_storage_worker.rb
+++ b/app/workers/project_migrate_hashed_storage_worker.rb
@@ -2,10 +2,34 @@ class ProjectMigrateHashedStorageWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ LEASE_TIMEOUT = 30.seconds.to_i
+
def perform(project_id)
project = Project.find_by(id: project_id)
return if project.nil? || project.pending_delete?
- ::Projects::HashedStorageMigrationService.new(project, logger).execute
+ uuid = lease_for(project_id).try_obtain
+ if uuid
+ ::Projects::HashedStorageMigrationService.new(project, logger).execute
+ else
+ false
+ end
+ rescue => ex
+ cancel_lease_for(project_id, uuid) if uuid
+ raise ex
+ end
+
+ def lease_for(project_id)
+ Gitlab::ExclusiveLease.new(lease_key(project_id), timeout: LEASE_TIMEOUT)
+ end
+
+ private
+
+ def lease_key(project_id)
+ "project_migrate_hashed_storage_worker:#{project_id}"
+ end
+
+ def cancel_lease_for(project_id, uuid)
+ Gitlab::ExclusiveLease.cancel(lease_key(project_id), uuid)
end
end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index cde5b45ad41..264706e3e23 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -4,6 +4,7 @@ class RepositoryForkWorker
include Sidekiq::Worker
include Gitlab::ShellAdapter
include DedicatedSidekiqQueue
+ include ProjectStartImport
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
@@ -37,7 +38,7 @@ class RepositoryForkWorker
private
def start_fork(project)
- return true if project.import_start
+ return true if start(project)
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.")
false
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 00a021abbdc..4e90b137b26 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -4,6 +4,7 @@ class RepositoryImportWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
include ExceptionBacktrace
+ include ProjectStartImport
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
@@ -16,11 +17,16 @@ class RepositoryImportWorker
import_url: project.import_url,
path: project.full_path)
- result = Projects::ImportService.new(project, project.creator).execute
+ service = Projects::ImportService.new(project, project.creator)
+ result = service.execute
+
+ # Some importers may perform their work asynchronously. In this case it's up
+ # to those importers to mark the import process as complete.
+ return if service.async?
+
raise ImportError, result[:message] if result[:status] == :error
- project.repository.after_import
- project.import_finish
+ project.after_import
rescue ImportError => ex
fail_import(project, ex.message)
raise
@@ -34,7 +40,7 @@ class RepositoryImportWorker
private
def start_import(project)
- return true if project.import_start
+ return true if start(project)
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
false
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index 269776a1f62..367e227f680 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -39,14 +39,23 @@ class StuckCiJobsWorker
def drop_stuck(status, timeout)
search(status, timeout) do |build|
return unless build.stuck?
+
drop_build :stuck, build, status, timeout
end
end
def search(status, timeout)
- builds = Ci::Build.where(status: status).where('ci_builds.updated_at < ?', timeout.ago)
- builds.joins(:project).merge(Project.without_deleted).includes(:tags, :runner, project: :namespace).find_each(batch_size: 50).each do |build|
- yield(build)
+ loop do
+ jobs = Ci::Build.where(status: status)
+ .where('ci_builds.updated_at < ?', timeout.ago)
+ .includes(:tags, :runner, project: :namespace)
+ .limit(100)
+ .to_a
+ break if jobs.empty?
+
+ jobs.each do |job|
+ yield(job)
+ end
end
end
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
index 7843179d77c..a396c0f27b2 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -23,7 +23,7 @@ class StuckMergeJobsWorker
merge_requests = MergeRequest.where(id: completed_ids)
merge_requests.where.not(merge_commit_sha: nil).update_all(state: :merged)
- merge_requests.where(merge_commit_sha: nil).update_all(state: :opened)
+ merge_requests.where(merge_commit_sha: nil).update_all(state: :opened, merge_jid: nil)
Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}")
end
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 89ae17cef37..afc47fc63d6 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -2,6 +2,8 @@ class UpdateMergeRequestsWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ LOG_TIME_THRESHOLD = 90 # seconds
+
def perform(project_id, user_id, oldrev, newrev, ref)
project = Project.find_by(id: project_id)
return unless project
@@ -9,6 +11,20 @@ class UpdateMergeRequestsWorker
user = User.find_by(id: user_id)
return unless user
- MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
+ # TODO: remove this benchmarking when we have rich logging
+ time = Benchmark.measure do
+ MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
+ end
+
+ args_log = [
+ "elapsed=#{time.real}",
+ "project_id=#{project_id}",
+ "user_id=#{user_id}",
+ "oldrev=#{oldrev}",
+ "newrev=#{newrev}",
+ "ref=#{ref}"
+ ].join(',')
+
+ Rails.logger.info("UpdateMergeRequestsWorker#perform #{args_log}") if time.real > LOG_TIME_THRESHOLD
end
end
diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb
index 5aa3bbdaa9d..241ed3901dc 100644
--- a/app/workers/wait_for_cluster_creation_worker.rb
+++ b/app/workers/wait_for_cluster_creation_worker.rb
@@ -2,25 +2,10 @@ class WaitForClusterCreationWorker
include Sidekiq::Worker
include ClusterQueue
- INITIAL_INTERVAL = 2.minutes
- EAGER_INTERVAL = 10.seconds
- TIMEOUT = 20.minutes
-
def perform(cluster_id)
- Gcp::Cluster.find_by_id(cluster_id).try do |cluster|
- Ci::FetchGcpOperationService.new.execute(cluster) do |operation|
- case operation.status
- when 'RUNNING'
- if TIMEOUT < Time.now.utc - operation.start_time.to_time.utc
- return cluster.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}")
- end
-
- WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, cluster.id)
- when 'DONE'
- Ci::FinalizeClusterCreationService.new.execute(cluster)
- else
- return cluster.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
- end
+ Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
+ cluster.provider.try do |provider|
+ Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) if cluster.gcp?
end
end
end
diff --git a/bin/changelog b/bin/changelog
index 61d4de06e90..efe25032ba1 100755
--- a/bin/changelog
+++ b/bin/changelog
@@ -28,6 +28,7 @@ class ChangelogOptionParser
Type.new('deprecated', 'New deprecation'),
Type.new('removed', 'Feature removal'),
Type.new('security', 'Security fix'),
+ Type.new('performance', 'Performance improvement'),
Type.new('other', 'Other')
].freeze
TYPES_OFFSET = 1
diff --git a/changelogs/unreleased/13637-show-account-confirmation-link-in-e-mail-text.yml b/changelogs/unreleased/13637-show-account-confirmation-link-in-e-mail-text.yml
deleted file mode 100644
index 5f98d0cc766..00000000000
--- a/changelogs/unreleased/13637-show-account-confirmation-link-in-e-mail-text.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Confirmation email shows link as text instead of human readable text
-merge_request: 14243
-author: bitsapien
-type: changed
diff --git a/changelogs/unreleased/13711-allow-same-period-housekeeping.yml b/changelogs/unreleased/13711-allow-same-period-housekeeping.yml
deleted file mode 100644
index 607a8683aff..00000000000
--- a/changelogs/unreleased/13711-allow-same-period-housekeeping.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Allow to use same periods for different housekeeping tasks (effectively
- skipping the lesser task)
-merge_request: 13711
-author: cernvcs
-type: added
diff --git a/changelogs/unreleased/14395-upgrade-gitlab-markup.yml b/changelogs/unreleased/14395-upgrade-gitlab-markup.yml
deleted file mode 100644
index d1f90fe5eb1..00000000000
--- a/changelogs/unreleased/14395-upgrade-gitlab-markup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade gitlab-markup gem
-merge_request: 14395
-author: Markus Koller
-type: other
diff --git a/changelogs/unreleased/14553-missing-space-in-log-msg.yml b/changelogs/unreleased/14553-missing-space-in-log-msg.yml
deleted file mode 100644
index a0420d49770..00000000000
--- a/changelogs/unreleased/14553-missing-space-in-log-msg.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Add missing space in Sidekiq memory killer log message"
-merge_request: 14553
-author: Benjamin Drung
-type: fixed
diff --git a/changelogs/unreleased/15588-fix-empty-dropdown-on-create-new-pipeline-in-case-of-validation-errors.yml b/changelogs/unreleased/15588-fix-empty-dropdown-on-create-new-pipeline-in-case-of-validation-errors.yml
new file mode 100644
index 00000000000..a4934c8896d
--- /dev/null
+++ b/changelogs/unreleased/15588-fix-empty-dropdown-on-create-new-pipeline-in-case-of-validation-errors.yml
@@ -0,0 +1,5 @@
+---
+title: Initializes the branches dropdown when the 'Start new pipeline' failed due to validation errors
+merge_request: 15588
+author: Christiaan Van den Poel
+type: fixed
diff --git a/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml b/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml
new file mode 100644
index 00000000000..e3c7ffc8046
--- /dev/null
+++ b/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml
@@ -0,0 +1,5 @@
+---
+title: Adds Rubocop rule for line break after guard clause
+merge_request: 15188
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/18308-escape-characters.yml b/changelogs/unreleased/18308-escape-characters.yml
deleted file mode 100644
index 8766e971490..00000000000
--- a/changelogs/unreleased/18308-escape-characters.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Escape quotes in git username
-merge_request: 14020
-author: Brandon Everett
-type: fixed
diff --git a/changelogs/unreleased/18608-lock-issues.yml b/changelogs/unreleased/18608-lock-issues.yml
deleted file mode 100644
index 7d907f744f6..00000000000
--- a/changelogs/unreleased/18608-lock-issues.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-title: Discussion lock for issues and merge requests
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/20049-projects-api-forks.yml b/changelogs/unreleased/20049-projects-api-forks.yml
deleted file mode 100644
index c6470620f57..00000000000
--- a/changelogs/unreleased/20049-projects-api-forks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add an API endpoint to determine the forks of a project
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml b/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml
deleted file mode 100644
index 245b8129de8..00000000000
--- a/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Return only group's members in user dropdowns on issuables list pages
-merge_request: 14249
-author:
-type: changed
diff --git a/changelogs/unreleased/21143-customize-branch-name-when-using-create-branch-in-an-issue.yml b/changelogs/unreleased/21143-customize-branch-name-when-using-create-branch-in-an-issue.yml
new file mode 100644
index 00000000000..d4c209aaf2b
--- /dev/null
+++ b/changelogs/unreleased/21143-customize-branch-name-when-using-create-branch-in-an-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Add an ability to use a custom branch name on creation from issues
+merge_request: 13884
+author: Vitaliy @blackst0ne Klachkov
+type: added
diff --git a/changelogs/unreleased/21331-improve-confusing-compare-page.yml b/changelogs/unreleased/21331-improve-confusing-compare-page.yml
deleted file mode 100644
index 469cc04930b..00000000000
--- a/changelogs/unreleased/21331-improve-confusing-compare-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make the labels in the Compare form less confusing
-merge_request: 14225
-author:
-type: changed
diff --git a/changelogs/unreleased/24121_extract_yet_another_users_finder.yml b/changelogs/unreleased/24121_extract_yet_another_users_finder.yml
deleted file mode 100644
index e43e97303e2..00000000000
--- a/changelogs/unreleased/24121_extract_yet_another_users_finder.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Extract AutocompleteController#users into finder
-merge_request: 13778
-author: Maxim Rydkin, Mayra Cabrera
-type: other
diff --git a/changelogs/unreleased/26890-fix-default-branches-sorting.yml b/changelogs/unreleased/26890-fix-default-branches-sorting.yml
deleted file mode 100644
index cf7060190b3..00000000000
--- a/changelogs/unreleased/26890-fix-default-branches-sorting.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix the default branches sorting to actually be 'Last updated'
-merge_request: 14295
-author:
-type: fixed
diff --git a/changelogs/unreleased/28377-add-edit-button-to-mobile-file-view.yml b/changelogs/unreleased/28377-add-edit-button-to-mobile-file-view.yml
new file mode 100644
index 00000000000..b6646379b8d
--- /dev/null
+++ b/changelogs/unreleased/28377-add-edit-button-to-mobile-file-view.yml
@@ -0,0 +1,5 @@
+---
+title: Add edit button to mobile file view
+merge_request: 15199
+author: Travis Miller
+type: added
diff --git a/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml b/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml
deleted file mode 100644
index 6036e1a43a0..00000000000
--- a/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Decrease Perceived Complexity threshold to 15
-merge_request: 14160
-author: Maxim Rydkin
-type: other
diff --git a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml
deleted file mode 100644
index a404456198a..00000000000
--- a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Decrease Cyclomatic Complexity threshold to 13
-merge_request: 14152
-author: Maxim Rydkin
-type: other
diff --git a/changelogs/unreleased/32057_support_ordering_project_notes_in_notes_api.yml b/changelogs/unreleased/32057_support_ordering_project_notes_in_notes_api.yml
new file mode 100644
index 00000000000..cd19d24e035
--- /dev/null
+++ b/changelogs/unreleased/32057_support_ordering_project_notes_in_notes_api.yml
@@ -0,0 +1,5 @@
+---
+title: added support for ordering and sorting in notes api
+merge_request: 15342
+author: haseebeqx
+type: added
diff --git a/changelogs/unreleased/32098-pipelines-navigation.yml b/changelogs/unreleased/32098-pipelines-navigation.yml
new file mode 100644
index 00000000000..925c92b6be8
--- /dev/null
+++ b/changelogs/unreleased/32098-pipelines-navigation.yml
@@ -0,0 +1,6 @@
+---
+title: Stop reloading the page when using pagination and tabs - use API calls - in
+ Pipelines table
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/32163-protected-branch-form-should-have-sane-defaults-for-dropdowns.yml b/changelogs/unreleased/32163-protected-branch-form-should-have-sane-defaults-for-dropdowns.yml
deleted file mode 100644
index 6110e245013..00000000000
--- a/changelogs/unreleased/32163-protected-branch-form-should-have-sane-defaults-for-dropdowns.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added defaults for protected branches dropdowns on the repository settings
-merge_request: 14278
-author:
-type: changed
diff --git a/changelogs/unreleased/33328-usage-ping-for-gitlab-features-and-components.yml b/changelogs/unreleased/33328-usage-ping-for-gitlab-features-and-components.yml
deleted file mode 100644
index d3aac241b75..00000000000
--- a/changelogs/unreleased/33328-usage-ping-for-gitlab-features-and-components.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds gitlab features and components to usage ping data.
-merge_request: 14305
-author:
-type: other
diff --git a/changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml b/changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml
new file mode 100644
index 00000000000..d61bbf2e355
--- /dev/null
+++ b/changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml
@@ -0,0 +1,5 @@
+---
+title: Add internationalization support for the prometheus integration
+merge_request: 33338
+author:
+type: other
diff --git a/changelogs/unreleased/33493-attempt-to-link-saml-users-to-ldap-by-email.yml b/changelogs/unreleased/33493-attempt-to-link-saml-users-to-ldap-by-email.yml
deleted file mode 100644
index 727f3cecd52..00000000000
--- a/changelogs/unreleased/33493-attempt-to-link-saml-users-to-ldap-by-email.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Link SAML users to LDAP by email.
-merge_request: 14216
-author:
-type: changed
diff --git a/changelogs/unreleased/34102-online-view-of-artifacts-fe.yml b/changelogs/unreleased/34102-online-view-of-artifacts-fe.yml
deleted file mode 100644
index ce83b140eb6..00000000000
--- a/changelogs/unreleased/34102-online-view-of-artifacts-fe.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add online view of HTML artifacts for public projects
-merge_request: 14399
-author:
-type: added
diff --git a/changelogs/unreleased/34259-project-denial-of-service-via-gitmodules-fix.yml b/changelogs/unreleased/34259-project-denial-of-service-via-gitmodules-fix.yml
deleted file mode 100644
index 8260f7fa4b2..00000000000
--- a/changelogs/unreleased/34259-project-denial-of-service-via-gitmodules-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes project denial of service via gitmodules using Extended ASCII.
-merge_request: 14301
-author:
-type: fixed
diff --git a/changelogs/unreleased/34366-issue-sidebar-don-t-render-participants-in-collapsed-state.yml b/changelogs/unreleased/34366-issue-sidebar-don-t-render-participants-in-collapsed-state.yml
deleted file mode 100644
index d34e685b5f5..00000000000
--- a/changelogs/unreleased/34366-issue-sidebar-don-t-render-participants-in-collapsed-state.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Load sidebar participants avatars only when visible
-merge_request: 14270
-author:
-type: other
diff --git a/changelogs/unreleased/34371-cycle-analitcs-global.yml b/changelogs/unreleased/34371-cycle-analitcs-global.yml
deleted file mode 100644
index 5e9f0a85e9a..00000000000
--- a/changelogs/unreleased/34371-cycle-analitcs-global.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removes cycle analytics service and store from global namespace
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/34510-board-issues-sql-speedup.yml b/changelogs/unreleased/34510-board-issues-sql-speedup.yml
deleted file mode 100644
index 244ff7e9dfa..00000000000
--- a/changelogs/unreleased/34510-board-issues-sql-speedup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Optimize the boards' issues fetching.
-merge_request: 14198
-author:
-type: other
diff --git a/changelogs/unreleased/34600-performance-wiki-pages.yml b/changelogs/unreleased/34600-performance-wiki-pages.yml
new file mode 100644
index 00000000000..541ae8f8e60
--- /dev/null
+++ b/changelogs/unreleased/34600-performance-wiki-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Performance issues when loading large number of wiki pages
+merge_request: 15276
+author:
+type: performance
diff --git a/changelogs/unreleased/3523-i18n-autodevops.yml b/changelogs/unreleased/3523-i18n-autodevops.yml
deleted file mode 100644
index 10cb22b42a0..00000000000
--- a/changelogs/unreleased/3523-i18n-autodevops.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improves i18n for Auto Devops callout
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/35290_allow_public_project_apis.yml b/changelogs/unreleased/35290_allow_public_project_apis.yml
deleted file mode 100644
index 1968eee0a53..00000000000
--- a/changelogs/unreleased/35290_allow_public_project_apis.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: made read-only APIs for public merge requests available without authentication
-merge_request: 13291
-author: haseebeqx
diff --git a/changelogs/unreleased/35917_create_services_for_keys.yml b/changelogs/unreleased/35917_create_services_for_keys.yml
deleted file mode 100644
index e7cad5a11d5..00000000000
--- a/changelogs/unreleased/35917_create_services_for_keys.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: creation of keys moved to services
-merge_request: 13331
-author: haseebeqx
diff --git a/changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml b/changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml
deleted file mode 100644
index cea6cb2e48b..00000000000
--- a/changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Re-arrange <script> tags before <template> tags in .vue files
-merge_request: 14671
-author:
-type: changed
diff --git a/changelogs/unreleased/36255-metrics-that-do-not-have-a-complete-history-are-not-shown-at-all.yml b/changelogs/unreleased/36255-metrics-that-do-not-have-a-complete-history-are-not-shown-at-all.yml
deleted file mode 100644
index a820ecee7d2..00000000000
--- a/changelogs/unreleased/36255-metrics-that-do-not-have-a-complete-history-are-not-shown-at-all.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow prometheus graphs to correctly handle NaN values
-merge_request: 14741
-author:
-type: fixed
diff --git a/changelogs/unreleased/36400-trigger-job.yml b/changelogs/unreleased/36400-trigger-job.yml
new file mode 100644
index 00000000000..27243813dc8
--- /dev/null
+++ b/changelogs/unreleased/36400-trigger-job.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent 500 error when inspecting job after trigger was removed
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml b/changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml
deleted file mode 100644
index f5ccb163d98..00000000000
--- a/changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow the git circuit breaker to correctly handle missing repository storages
-merge_request: 14417
-author:
-type: fixed
diff --git a/changelogs/unreleased/36631-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml b/changelogs/unreleased/36631-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml
deleted file mode 100644
index a2e1d07158b..00000000000
--- a/changelogs/unreleased/36631-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Reschedule merge request diff background migrations to catch failures from
- 9.5 run
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/36670-remove-edit-form.yml b/changelogs/unreleased/36670-remove-edit-form.yml
deleted file mode 100644
index 4e80b685f67..00000000000
--- a/changelogs/unreleased/36670-remove-edit-form.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove the ability to visit the issue edit form directly
-merge_request: 14523
-author:
-type: removed
diff --git a/changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml b/changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml
deleted file mode 100644
index 3d3efcdbcc6..00000000000
--- a/changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Hide close MR button after merge without reloading page
-merge_request: 14122
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/36829-add-ability-to-verify-gpg-subkeys.yml b/changelogs/unreleased/36829-add-ability-to-verify-gpg-subkeys.yml
deleted file mode 100644
index ee6a7287e86..00000000000
--- a/changelogs/unreleased/36829-add-ability-to-verify-gpg-subkeys.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for GPG subkeys in signature verification
-merge_request: 14517
-author:
-type: added
diff --git a/changelogs/unreleased/36884-gitaly-admin-version.yml b/changelogs/unreleased/36884-gitaly-admin-version.yml
deleted file mode 100644
index 0b3b9a205b5..00000000000
--- a/changelogs/unreleased/36884-gitaly-admin-version.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Gitaly version to Admin Dashboard
-merge_request: 14313
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml b/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml
deleted file mode 100644
index 9ac4a0ae7f3..00000000000
--- a/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add GitLab-Pages version to Admin Dashboard
-merge_request: 14040
-author: travismiller
-type: added
diff --git a/changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml b/changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml
deleted file mode 100644
index f3118cf0f2f..00000000000
--- a/changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed non-UTF-8 valid branch names from causing an error.
-merge_request: 14090
-type: fixed
diff --git a/changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml b/changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml
deleted file mode 100644
index 3364b1d46b3..00000000000
--- a/changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix incorrect X-axis labels in Prometheus graphs
-merge_request: 14258
-author:
-type: fixed
diff --git a/changelogs/unreleased/37220-es-modules.yml b/changelogs/unreleased/37220-es-modules.yml
deleted file mode 100644
index de81fa9e74d..00000000000
--- a/changelogs/unreleased/37220-es-modules.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Exports common_utils utility functions as modules
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/37229-mr-widget-status-icon.yml b/changelogs/unreleased/37229-mr-widget-status-icon.yml
deleted file mode 100644
index 6d84d1964ca..00000000000
--- a/changelogs/unreleased/37229-mr-widget-status-icon.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: fix merge request widget status icon for failed CI
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/37335-counter-active-state.yml b/changelogs/unreleased/37335-counter-active-state.yml
deleted file mode 100644
index a9632201a89..00000000000
--- a/changelogs/unreleased/37335-counter-active-state.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add active states to nav bar counters
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml b/changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml
deleted file mode 100644
index bec7da26b1a..00000000000
--- a/changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Add help text to runner edit: tags should be separated by commas.'
-merge_request:
-author: Brendan O'Leary
-type: added
diff --git a/changelogs/unreleased/37970-ci-sections-tracking.yml b/changelogs/unreleased/37970-ci-sections-tracking.yml
deleted file mode 100644
index a9011b22c6c..00000000000
--- a/changelogs/unreleased/37970-ci-sections-tracking.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Parse and store gitlab-runner timestamped section markers
-merge_request: 14551
-author:
-type: added
diff --git a/changelogs/unreleased/37970-timestamped-ci.yml b/changelogs/unreleased/37970-timestamped-ci.yml
deleted file mode 100644
index 2a4797f069a..00000000000
--- a/changelogs/unreleased/37970-timestamped-ci.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Strip gitlab-runner section markers in build trace HTML view
-merge_request: 14393
-author:
-type: added
diff --git a/changelogs/unreleased/37999-fix-circuit-breaker.yml b/changelogs/unreleased/37999-fix-circuit-breaker.yml
deleted file mode 100644
index a75315c4988..00000000000
--- a/changelogs/unreleased/37999-fix-circuit-breaker.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix the filesystem shard health check to check all configured shards
-merge_request: 14341
-author:
-type: fixed
diff --git a/changelogs/unreleased/38031-monitoring-hover-info-is-clipped.yml b/changelogs/unreleased/38031-monitoring-hover-info-is-clipped.yml
deleted file mode 100644
index 8b3fae2c103..00000000000
--- a/changelogs/unreleased/38031-monitoring-hover-info-is-clipped.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Move the deployment flag content to the left when deployment marker is near
- the end
-merge_request: 14514
-author:
-type: fixed
diff --git a/changelogs/unreleased/38036-hover-and-legend-data-should-be-linked.yml b/changelogs/unreleased/38036-hover-and-legend-data-should-be-linked.yml
deleted file mode 100644
index 591e542cd17..00000000000
--- a/changelogs/unreleased/38036-hover-and-legend-data-should-be-linked.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Sync up hover and legend data across all graphs for the prometheus dashboard
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/38052-use-simple-api-for-projects.yml b/changelogs/unreleased/38052-use-simple-api-for-projects.yml
deleted file mode 100644
index 49c7485861e..00000000000
--- a/changelogs/unreleased/38052-use-simple-api-for-projects.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use `simple=true` for projects API in Projects dropdown for better search performance
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/38075_allow_refernce_integer_labels.yml b/changelogs/unreleased/38075_allow_refernce_integer_labels.yml
new file mode 100644
index 00000000000..b5342d4adf8
--- /dev/null
+++ b/changelogs/unreleased/38075_allow_refernce_integer_labels.yml
@@ -0,0 +1,5 @@
+---
+title: Fix errors when selecting numeric-only labels in the labels autocomplete selector
+merge_request: 14607
+author: haseebeqx
+type: fixed
diff --git a/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml b/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml
deleted file mode 100644
index 579c247c4c2..00000000000
--- a/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix bottom spacing for dropdowns that open upwards
-merge_request: 14535
-author:
-type: fixed
diff --git a/changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml b/changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml
deleted file mode 100644
index df562077fb3..00000000000
--- a/changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix `rake gitlab:incoming_email:check` and make it report the actual error
-merge_request: 14423
-author:
-type: fixed
diff --git a/changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml b/changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml
deleted file mode 100644
index 768e296fcd7..00000000000
--- a/changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Does not check if an invariant hashed storage path exists on disk when renaming
- projects.
-merge_request: 14428
-author:
-type: fixed
diff --git a/changelogs/unreleased/38234-reserve-refs-replace.yml b/changelogs/unreleased/38234-reserve-refs-replace.yml
deleted file mode 100644
index 3a5ffbf9db0..00000000000
--- a/changelogs/unreleased/38234-reserve-refs-replace.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Also reserve refs/replace after importing a project
-merge_request: 14436
-author:
-type: fixed
diff --git a/changelogs/unreleased/38389-allow-merge-without-success.yml b/changelogs/unreleased/38389-allow-merge-without-success.yml
deleted file mode 100644
index 6a37bcc55fc..00000000000
--- a/changelogs/unreleased/38389-allow-merge-without-success.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Allow merge in MR widget with no pipeline but using "Only allow merge requests
- to be merged if the pipeline succeeds"
-merge_request: 14633
-author:
-type: fixed
diff --git a/changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml b/changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml
new file mode 100644
index 00000000000..c73cf8bf60b
--- /dev/null
+++ b/changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml
@@ -0,0 +1,5 @@
+---
+title: Changed validation error message on wrong milestone dates
+merge_request:
+author: Xurxo Méndez Pérez
+type: fixed
diff --git a/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml b/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml
deleted file mode 100644
index 419e9295d32..00000000000
--- a/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use explicit boolean true attribute for show-disabled-button in Vue files
-merge_request: 14672
-author:
-type: fixed
diff --git a/changelogs/unreleased/38432-fix-notes-type-for-import.yml b/changelogs/unreleased/38432-fix-notes-type-for-import.yml
deleted file mode 100644
index db8371f4420..00000000000
--- a/changelogs/unreleased/38432-fix-notes-type-for-import.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix notes type created from import. This should fix some missing notes issues
- from imported projects
-merge_request: 14524
-author:
-type: fixed
diff --git a/changelogs/unreleased/38502-fix-nav-dropdown-close-animation.yml b/changelogs/unreleased/38502-fix-nav-dropdown-close-animation.yml
deleted file mode 100644
index 974adb9ed28..00000000000
--- a/changelogs/unreleased/38502-fix-nav-dropdown-close-animation.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix navigation dropdown close animation on mobile screens
-merge_request: 14649
-author:
-type: fixed
diff --git a/changelogs/unreleased/38571-fix-exception-in-raven-report.yml b/changelogs/unreleased/38571-fix-exception-in-raven-report.yml
deleted file mode 100644
index 62e3b8d304c..00000000000
--- a/changelogs/unreleased/38571-fix-exception-in-raven-report.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Ensure no exception is raised when Raven tries to get the current user in API
- context
-merge_request: 14580
-author:
-type: fixed
diff --git a/changelogs/unreleased/38619-fix-comment-delete-confirm-text.yml b/changelogs/unreleased/38619-fix-comment-delete-confirm-text.yml
deleted file mode 100644
index a203bff8410..00000000000
--- a/changelogs/unreleased/38619-fix-comment-delete-confirm-text.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix comment deletion confirmation dialog typo
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/38635-fix-gitlab-check-git-ssh-config.yml b/changelogs/unreleased/38635-fix-gitlab-check-git-ssh-config.yml
deleted file mode 100644
index 49d0671233a..00000000000
--- a/changelogs/unreleased/38635-fix-gitlab-check-git-ssh-config.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Whitelist authorized_keys.lock in the gitlab:check rake task
-merge_request: 14624
-author:
-type: fixed
diff --git a/changelogs/unreleased/38696-fix-project-snippets-breadcrumb-link.yml b/changelogs/unreleased/38696-fix-project-snippets-breadcrumb-link.yml
deleted file mode 100644
index 18b1645d7a9..00000000000
--- a/changelogs/unreleased/38696-fix-project-snippets-breadcrumb-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix project snippets breadcrumb link
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/38775-scrollable-tabs-on-admin.yml b/changelogs/unreleased/38775-scrollable-tabs-on-admin.yml
deleted file mode 100644
index 65a66714bcb..00000000000
--- a/changelogs/unreleased/38775-scrollable-tabs-on-admin.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make tabs on top scrollable on admin dashboard
-merge_request: 14685
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml b/changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml
deleted file mode 100644
index bbfe5d49a3e..00000000000
--- a/changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix broken Y-axis scaling in some Prometheus graphs
-merge_request: 14693
-author:
-type: fixed
diff --git a/changelogs/unreleased/38822-oauth-search-case-insensitive.yml b/changelogs/unreleased/38822-oauth-search-case-insensitive.yml
new file mode 100644
index 00000000000..d84360b4c5c
--- /dev/null
+++ b/changelogs/unreleased/38822-oauth-search-case-insensitive.yml
@@ -0,0 +1,5 @@
+---
+title: OAuth identity lookups case-insensitive
+merge_request: 15312
+author:
+type: fixed
diff --git a/changelogs/unreleased/38877-disable-autocomplete-in-filtered-search.yml b/changelogs/unreleased/38877-disable-autocomplete-in-filtered-search.yml
new file mode 100644
index 00000000000..07439a860ec
--- /dev/null
+++ b/changelogs/unreleased/38877-disable-autocomplete-in-filtered-search.yml
@@ -0,0 +1,5 @@
+---
+title: Disables autocomplete in filtered searc
+merge_request: 15477
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/38962-automatically-run-a-pipeline-when-auto-devops-is-turned-on-in-project-settings.yml b/changelogs/unreleased/38962-automatically-run-a-pipeline-when-auto-devops-is-turned-on-in-project-settings.yml
new file mode 100644
index 00000000000..a4d703bc69f
--- /dev/null
+++ b/changelogs/unreleased/38962-automatically-run-a-pipeline-when-auto-devops-is-turned-on-in-project-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Add the option to automatically run a pipeline after updating AutoDevOps settings
+merge_request: 15380
+author:
+type: changed
diff --git a/changelogs/unreleased/39335-add-time-spend-to-milestones.yml b/changelogs/unreleased/39335-add-time-spend-to-milestones.yml
new file mode 100644
index 00000000000..41a43418cbf
--- /dev/null
+++ b/changelogs/unreleased/39335-add-time-spend-to-milestones.yml
@@ -0,0 +1,5 @@
+---
+title: Add total time spent to milestones
+merge_request: 15116
+author: George Andrinopoulos
+type: added
diff --git a/changelogs/unreleased/39436-pages-api-administrative.yml b/changelogs/unreleased/39436-pages-api-administrative.yml
new file mode 100644
index 00000000000..f38bbbd479c
--- /dev/null
+++ b/changelogs/unreleased/39436-pages-api-administrative.yml
@@ -0,0 +1,5 @@
+---
+title: Add administrative endpoint to list all pages domains
+merge_request: 15160
+author: Travis Miller
+type: added
diff --git a/changelogs/unreleased/39455-clone-dropdown-should-not-have-a-tooltip.yml b/changelogs/unreleased/39455-clone-dropdown-should-not-have-a-tooltip.yml
new file mode 100644
index 00000000000..cb522cb7611
--- /dev/null
+++ b/changelogs/unreleased/39455-clone-dropdown-should-not-have-a-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Removed tooltip from clone dropdown
+merge_request: 15334
+author:
+type: other
diff --git a/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml b/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml
new file mode 100644
index 00000000000..fc7c024f95a
--- /dev/null
+++ b/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml
@@ -0,0 +1,5 @@
+---
+title: Add inline editing to issues on mobile
+merge_request: 15438
+author:
+type: changed
diff --git a/changelogs/unreleased/39601-create-issuable-destroy-service.yml b/changelogs/unreleased/39601-create-issuable-destroy-service.yml
new file mode 100644
index 00000000000..b0463f02eba
--- /dev/null
+++ b/changelogs/unreleased/39601-create-issuable-destroy-service.yml
@@ -0,0 +1,5 @@
+---
+title: Create issuable destroy service
+merge_request: 15604
+author: George Andrinopoulos
+type: other
diff --git a/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml b/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml
new file mode 100644
index 00000000000..056afe43010
--- /dev/null
+++ b/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml
@@ -0,0 +1,5 @@
+---
+title: Move update_project_counter_caches? out of issue and merge request
+merge_request: 15300
+author: George Andrinopoulos
+type: other
diff --git a/changelogs/unreleased/39720-group-milestone-sorting.yml b/changelogs/unreleased/39720-group-milestone-sorting.yml
new file mode 100644
index 00000000000..15ef87fa567
--- /dev/null
+++ b/changelogs/unreleased/39720-group-milestone-sorting.yml
@@ -0,0 +1,5 @@
+---
+title: Add dropdown sort to group milestones
+merge_request: 15230
+author: George Andrinopoulos
+type: added
diff --git a/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml b/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml
new file mode 100644
index 00000000000..8b27c43d15b
--- /dev/null
+++ b/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml
@@ -0,0 +1,5 @@
+---
+title: Fix commits page throwing 500 when the multi-file editor was enabled
+merge_request: 15502
+author:
+type: fixed
diff --git a/changelogs/unreleased/39827-fix-projects-dropdown-overflow.yml b/changelogs/unreleased/39827-fix-projects-dropdown-overflow.yml
new file mode 100644
index 00000000000..ebd7b582e8a
--- /dev/null
+++ b/changelogs/unreleased/39827-fix-projects-dropdown-overflow.yml
@@ -0,0 +1,5 @@
+---
+title: Fix item name and namespace text overflow in Projects dropdown
+merge_request: 15451
+author:
+type: fixed
diff --git a/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml b/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml
new file mode 100644
index 00000000000..580b97241e7
--- /dev/null
+++ b/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml
@@ -0,0 +1,6 @@
+---
+title: Fix pipeline status transition for single manual job. This would also fix pipeline
+ duration becuse it is depending on status transition
+merge_request: 15251
+author:
+type: fixed
diff --git a/changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml b/changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml
new file mode 100644
index 00000000000..358c007387e
--- /dev/null
+++ b/changelogs/unreleased/39895-cant-set-mattermost-username-channel-from-api.yml
@@ -0,0 +1,5 @@
+---
+title: Fix acceptance of username for Mattermost service update
+merge_request: 15275
+author:
+type: fixed
diff --git a/changelogs/unreleased/39977-gitlab-shell-default-timeout.yml b/changelogs/unreleased/39977-gitlab-shell-default-timeout.yml
new file mode 100644
index 00000000000..b7a974fd8d9
--- /dev/null
+++ b/changelogs/unreleased/39977-gitlab-shell-default-timeout.yml
@@ -0,0 +1,5 @@
+---
+title: Set the default gitlab-shell timeout to 3 hours
+merge_request: 15292
+author:
+type: fixed
diff --git a/changelogs/unreleased/40016-log-header.yml b/changelogs/unreleased/40016-log-header.yml
new file mode 100644
index 00000000000..f52c2d2a0d5
--- /dev/null
+++ b/changelogs/unreleased/40016-log-header.yml
@@ -0,0 +1,5 @@
+---
+title: Hide log size for mobile screens
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml b/changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml
new file mode 100644
index 00000000000..a2ae2059c47
--- /dev/null
+++ b/changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml
@@ -0,0 +1,5 @@
+---
+title: Add total_time_spent to the `changes` hash in issuable Webhook payloads
+merge_request: 15381
+author:
+type: changed
diff --git a/changelogs/unreleased/40146_fix_special_charecter_search_in_filenames.yml b/changelogs/unreleased/40146_fix_special_charecter_search_in_filenames.yml
new file mode 100644
index 00000000000..00f7dd7c0f0
--- /dev/null
+++ b/changelogs/unreleased/40146_fix_special_charecter_search_in_filenames.yml
@@ -0,0 +1,5 @@
+---
+title: Fix search results when a filename would contain a special character.
+merge_request: 15606
+author: haseebeqx
+type: fixed
diff --git a/changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml b/changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml
new file mode 100644
index 00000000000..fdaa90f0d5d
--- /dev/null
+++ b/changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml
@@ -0,0 +1,5 @@
+---
+title: Remove extra margin from wordmark in header
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml b/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml
new file mode 100644
index 00000000000..9c308321a19
--- /dev/null
+++ b/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml
@@ -0,0 +1,5 @@
+---
+title: Removed unused rake task, 'rake gitlab:sidekiq:drop_post_receive'
+merge_request: 15493
+author:
+type: fixed
diff --git a/changelogs/unreleased/40291-ignore-hashed-repos-cleanup-repositories.yml b/changelogs/unreleased/40291-ignore-hashed-repos-cleanup-repositories.yml
new file mode 100644
index 00000000000..1e3f52b3a9c
--- /dev/null
+++ b/changelogs/unreleased/40291-ignore-hashed-repos-cleanup-repositories.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure that rake gitlab:cleanup:repos task does not mess with hashed repositories
+merge_request: 15520
+author:
+type: fixed
diff --git a/changelogs/unreleased/40352-ignore-hashed-repos-cleanup-dirs.yml b/changelogs/unreleased/40352-ignore-hashed-repos-cleanup-dirs.yml
new file mode 100644
index 00000000000..0ccbc699729
--- /dev/null
+++ b/changelogs/unreleased/40352-ignore-hashed-repos-cleanup-dirs.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure that rake gitlab:cleanup:dirs task does not mess with hashed repositories
+merge_request: 15600
+author:
+type: fixed
diff --git a/changelogs/unreleased/40373-fix-issue-note-submit-disabled-on-paste.yml b/changelogs/unreleased/40373-fix-issue-note-submit-disabled-on-paste.yml
new file mode 100644
index 00000000000..e683e60397e
--- /dev/null
+++ b/changelogs/unreleased/40373-fix-issue-note-submit-disabled-on-paste.yml
@@ -0,0 +1,6 @@
+---
+title: Fix Issue comment submit button being disabled when pasting content from another
+ GFM note
+merge_request: 15530
+author:
+type: fixed
diff --git a/changelogs/unreleased/40481-bump-jquery-to-2-2-4.yml b/changelogs/unreleased/40481-bump-jquery-to-2-2-4.yml
new file mode 100644
index 00000000000..e275c65e8c8
--- /dev/null
+++ b/changelogs/unreleased/40481-bump-jquery-to-2-2-4.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade jQuery to 2.2.4
+merge_request: 15570
+author: Takuya Noguchi
+type: security
diff --git a/changelogs/unreleased/40530-merge-request-generates-wrong-diff-when-branch-and-tag-have-the-same-name.yml b/changelogs/unreleased/40530-merge-request-generates-wrong-diff-when-branch-and-tag-have-the-same-name.yml
new file mode 100644
index 00000000000..e9fae6fe0d7
--- /dev/null
+++ b/changelogs/unreleased/40530-merge-request-generates-wrong-diff-when-branch-and-tag-have-the-same-name.yml
@@ -0,0 +1,5 @@
+---
+title: Fix merge requests where the source or target branch name matches a tag name
+merge_request: 15591
+author:
+type: fixed
diff --git a/changelogs/unreleased/40561-environment-scope-value-is-not-trimmed.yml b/changelogs/unreleased/40561-environment-scope-value-is-not-trimmed.yml
new file mode 100644
index 00000000000..e0e3ddbdaa8
--- /dev/null
+++ b/changelogs/unreleased/40561-environment-scope-value-is-not-trimmed.yml
@@ -0,0 +1,5 @@
+---
+title: Strip leading & trailing whitespaces in CI/CD secret variable keys
+merge_request: 15615
+author:
+type: fixed
diff --git a/changelogs/unreleased/40568-bump-seed-fu-to-2-3-7.yml b/changelogs/unreleased/40568-bump-seed-fu-to-2-3-7.yml
new file mode 100644
index 00000000000..708269d5c83
--- /dev/null
+++ b/changelogs/unreleased/40568-bump-seed-fu-to-2-3-7.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade seed-fu to 2.3.7
+merge_request: 15607
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/4080-align-retry-btn.yml b/changelogs/unreleased/4080-align-retry-btn.yml
new file mode 100644
index 00000000000..c7d3997839c
--- /dev/null
+++ b/changelogs/unreleased/4080-align-retry-btn.yml
@@ -0,0 +1,5 @@
+---
+title: Align retry button with job title with new grid size
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml b/changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml
deleted file mode 100644
index 44e16512bae..00000000000
--- a/changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move `lib/ci` to `lib/gitlab/ci`
-merge_request: 14078
-author: Maxim Rydkin
-type: other
diff --git a/changelogs/unreleased/add-1000-plus-counters-for-jobs-page.yml b/changelogs/unreleased/add-1000-plus-counters-for-jobs-page.yml
deleted file mode 100644
index 5f5a61406da..00000000000
--- a/changelogs/unreleased/add-1000-plus-counters-for-jobs-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add 1000+ counters to job page
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml b/changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml
deleted file mode 100644
index 7f098c8f60c..00000000000
--- a/changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Change index on ci_builds to optimize Jobs Controller
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/add-composite-index-on-merge-requests-merge-commit-sha.yml b/changelogs/unreleased/add-composite-index-on-merge-requests-merge-commit-sha.yml
deleted file mode 100644
index 9a600282716..00000000000
--- a/changelogs/unreleased/add-composite-index-on-merge-requests-merge-commit-sha.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add index for merge_requests.merge_commit_sha
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/add-labels-template-index.yml b/changelogs/unreleased/add-labels-template-index.yml
deleted file mode 100644
index 5f66c4ce181..00000000000
--- a/changelogs/unreleased/add-labels-template-index.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add (partial) index on Labels.template
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/add-view-replaced-file-link.yml b/changelogs/unreleased/add-view-replaced-file-link.yml
deleted file mode 100644
index b5a85f2e0f2..00000000000
--- a/changelogs/unreleased/add-view-replaced-file-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add view replaced file link for image diffs
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/add_closed_at_attribute.yml b/changelogs/unreleased/add_closed_at_attribute.yml
deleted file mode 100644
index 3afb75e8915..00000000000
--- a/changelogs/unreleased/add_closed_at_attribute.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add 'closed_at' attribute to Issues API
-merge_request: 14316
-author: Vitaliy @blackst0ne Klachkov
-type: added
diff --git a/changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml b/changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml
deleted file mode 100644
index 0470c6519f4..00000000000
--- a/changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add tooltip for milestone due date to issue and merge request lists
-merge_request: 14318
-author: Vitaliy @blackst0ne Klachkov
-type: added
diff --git a/changelogs/unreleased/adjusting-tooltips.yml b/changelogs/unreleased/adjusting-tooltips.yml
deleted file mode 100644
index 726b75caecd..00000000000
--- a/changelogs/unreleased/adjusting-tooltips.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjust tooltips to adhere to 8px grid and make them more readable
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/an-gitaly-timeouts.yml b/changelogs/unreleased/an-gitaly-timeouts.yml
new file mode 100644
index 00000000000..e18d82b2704
--- /dev/null
+++ b/changelogs/unreleased/an-gitaly-timeouts.yml
@@ -0,0 +1,5 @@
+---
+title: Add timeouts for Gitaly calls
+merge_request: 15047
+author:
+type: performance
diff --git a/changelogs/unreleased/brand_header_change.yml b/changelogs/unreleased/brand_header_change.yml
new file mode 100644
index 00000000000..6ea6e8192a4
--- /dev/null
+++ b/changelogs/unreleased/brand_header_change.yml
@@ -0,0 +1,5 @@
+---
+title: When a custom header logo is present, don't show GitLab type logo
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/breadcrumb-item-links.yml b/changelogs/unreleased/breadcrumb-item-links.yml
deleted file mode 100644
index 9b66456efca..00000000000
--- a/changelogs/unreleased/breadcrumb-item-links.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed issue/merge request breadcrumb titles not having links
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/breadcrumbs-line-height-padding.yml b/changelogs/unreleased/breadcrumbs-line-height-padding.yml
deleted file mode 100644
index 3ac56c8b593..00000000000
--- a/changelogs/unreleased/breadcrumbs-line-height-padding.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: breadcrumbs receives padding when double lined
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/bugfix-graph-friendly-notes-number.yml b/changelogs/unreleased/bugfix-graph-friendly-notes-number.yml
deleted file mode 100644
index 3a99729fb48..00000000000
--- a/changelogs/unreleased/bugfix-graph-friendly-notes-number.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show notes number more user-friendly in the graph
-merge_request: 13949
-author: Vladislav Kaverin
-type: changed
diff --git a/changelogs/unreleased/bvl-delete-empty-fork-networks.yml b/changelogs/unreleased/bvl-delete-empty-fork-networks.yml
new file mode 100644
index 00000000000..3bbb4cf6e3c
--- /dev/null
+++ b/changelogs/unreleased/bvl-delete-empty-fork-networks.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up empty fork networks
+merge_request: 15373
+author:
+type: other
diff --git a/changelogs/unreleased/bvl-double-fork.yml b/changelogs/unreleased/bvl-double-fork.yml
new file mode 100644
index 00000000000..50bc1adde26
--- /dev/null
+++ b/changelogs/unreleased/bvl-double-fork.yml
@@ -0,0 +1,5 @@
+---
+title: Correctly link to a forked project from the new fork page.
+merge_request: 15653
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-fork-network-schema.yml b/changelogs/unreleased/bvl-fork-network-schema.yml
deleted file mode 100644
index 97b2d5acada..00000000000
--- a/changelogs/unreleased/bvl-fork-network-schema.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow creating merge requests across a fork network
-merge_request: 14422
-author:
-type: changed
diff --git a/changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml b/changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml
new file mode 100644
index 00000000000..2acb98db785
--- /dev/null
+++ b/changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Create a fork network for forks with a deleted source
+merge_request: 15595
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-subgroup-in-dropdowns.yml b/changelogs/unreleased/bvl-subgroup-in-dropdowns.yml
new file mode 100644
index 00000000000..1114d429dec
--- /dev/null
+++ b/changelogs/unreleased/bvl-subgroup-in-dropdowns.yml
@@ -0,0 +1,5 @@
+---
+title: Make sure a user can add projects to subgroups they have access to
+merge_request: 15294
+author:
+type: fixed
diff --git a/changelogs/unreleased/change-dashed-border-button-color.yml b/changelogs/unreleased/change-dashed-border-button-color.yml
deleted file mode 100644
index 038bea79273..00000000000
--- a/changelogs/unreleased/change-dashed-border-button-color.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: changed dashed border button color to be darker
-merge_request: !14041
-author:
-type: other
diff --git a/changelogs/unreleased/cleanup-issues-schema.yml b/changelogs/unreleased/cleanup-issues-schema.yml
new file mode 100644
index 00000000000..9f5fb0bdf82
--- /dev/null
+++ b/changelogs/unreleased/cleanup-issues-schema.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up schema of the "issues" table
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/close-issue-by-implements.yml b/changelogs/unreleased/close-issue-by-implements.yml
deleted file mode 100644
index fe36ce3f7aa..00000000000
--- a/changelogs/unreleased/close-issue-by-implements.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Add \"implements\" to the default issue closing message regex"
-merge_request: 14612
-author: Guilherme Vieira
-type: added
diff --git a/changelogs/unreleased/commit-row-avatar-align-top.yml b/changelogs/unreleased/commit-row-avatar-align-top.yml
deleted file mode 100644
index aa5ab770bd8..00000000000
--- a/changelogs/unreleased/commit-row-avatar-align-top.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed commit avatars being centered vertically
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/consistent-tooltip-direction-on-commits.yml b/changelogs/unreleased/consistent-tooltip-direction-on-commits.yml
deleted file mode 100644
index 9e6a429f6f0..00000000000
--- a/changelogs/unreleased/consistent-tooltip-direction-on-commits.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Tooltips in the commit info box now all face the same direction
-merge_request:
-author: Jedidiah Broadbent
-type: fixed
diff --git a/changelogs/unreleased/content-title-link-hover-bg.yml b/changelogs/unreleased/content-title-link-hover-bg.yml
deleted file mode 100644
index c4c31c2ad06..00000000000
--- a/changelogs/unreleased/content-title-link-hover-bg.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed navbar title colors leaking out of the navbar
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/declarative-policy-optimisations.yml b/changelogs/unreleased/declarative-policy-optimisations.yml
deleted file mode 100644
index dc51c89d575..00000000000
--- a/changelogs/unreleased/declarative-policy-optimisations.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Speed up permission checks
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/default-values-for-mr-states.yml b/changelogs/unreleased/default-values-for-mr-states.yml
new file mode 100644
index 00000000000..f873a5335d0
--- /dev/null
+++ b/changelogs/unreleased/default-values-for-mr-states.yml
@@ -0,0 +1,5 @@
+---
+title: Fix defaults for MR states and merge statuses
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-bitbucket-import-truncated-shas.yml b/changelogs/unreleased/dm-bitbucket-import-truncated-shas.yml
deleted file mode 100644
index 057407b78d9..00000000000
--- a/changelogs/unreleased/dm-bitbucket-import-truncated-shas.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix bug that caused merge requests with diff notes imported from Bitbucket
- to raise errors
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-closing-issue-urls.yml b/changelogs/unreleased/dm-closing-issue-urls.yml
deleted file mode 100644
index 059e406b63d..00000000000
--- a/changelogs/unreleased/dm-closing-issue-urls.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Correctly detect multiple issue URLs after 'Closes...' in MR descriptions
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-copy-parallel-diff.yml b/changelogs/unreleased/dm-copy-parallel-diff.yml
deleted file mode 100644
index 96a65007661..00000000000
--- a/changelogs/unreleased/dm-copy-parallel-diff.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Only copy old/new code when selecting left/right side of parallel diff
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/dm-fix-registry-with-sudo-token.yml b/changelogs/unreleased/dm-fix-registry-with-sudo-token.yml
new file mode 100644
index 00000000000..be687fda147
--- /dev/null
+++ b/changelogs/unreleased/dm-fix-registry-with-sudo-token.yml
@@ -0,0 +1,5 @@
+---
+title: Fix pulling and pushing using a personal access token with the sudo scope
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-pat-revoke.yml b/changelogs/unreleased/dm-pat-revoke.yml
deleted file mode 100644
index 32ac66056d5..00000000000
--- a/changelogs/unreleased/dm-pat-revoke.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Set default scope on PATs that don't have one set to allow them to be revoked
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-project-search-performance.yml b/changelogs/unreleased/dm-project-search-performance.yml
new file mode 100644
index 00000000000..b533043b163
--- /dev/null
+++ b/changelogs/unreleased/dm-project-search-performance.yml
@@ -0,0 +1,6 @@
+---
+title: Drastically improve project search performance by no longer searching namespace
+ name
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/dm-search-pattern.yml b/changelogs/unreleased/dm-search-pattern.yml
new file mode 100644
index 00000000000..1670d8c4b9a
--- /dev/null
+++ b/changelogs/unreleased/dm-search-pattern.yml
@@ -0,0 +1,5 @@
+---
+title: Use fuzzy search with minimum length of 3 characters where appropriate
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/dm-simple-project-avatar-url.yml b/changelogs/unreleased/dm-simple-project-avatar-url.yml
deleted file mode 100644
index e517345f5d2..00000000000
--- a/changelogs/unreleased/dm-simple-project-avatar-url.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expose avatar_url when requesting list of projects from API with simple=true
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml b/changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml
deleted file mode 100644
index 3dfe4114cc9..00000000000
--- a/changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix docs for lightweight tag creation via API
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/docs-28814-clarify-artifacts-ref.yml b/changelogs/unreleased/docs-28814-clarify-artifacts-ref.yml
deleted file mode 100644
index 3cdcff8caaf..00000000000
--- a/changelogs/unreleased/docs-28814-clarify-artifacts-ref.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clarify artifact download via the API only accepts branch or tag name for ref
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/docs-38152-bump-recommended-mysql-version.yml b/changelogs/unreleased/docs-38152-bump-recommended-mysql-version.yml
deleted file mode 100644
index eea679d0814..00000000000
--- a/changelogs/unreleased/docs-38152-bump-recommended-mysql-version.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Change recommended MySQL version to 5.6
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/docs-add-summary-about-project-archiving.yml b/changelogs/unreleased/docs-add-summary-about-project-archiving.yml
deleted file mode 100644
index cc1b48a682d..00000000000
--- a/changelogs/unreleased/docs-add-summary-about-project-archiving.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add documentation to summarise project archiving
-merge_request: 14650
-author:
-type: other
diff --git a/changelogs/unreleased/docs-openid-connect.yml b/changelogs/unreleased/docs-openid-connect.yml
deleted file mode 100644
index 3989ec53cfa..00000000000
--- a/changelogs/unreleased/docs-openid-connect.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add link to OpenID Connect documentation
-merge_request: 14368
-author: Markus Koller
-type: other
diff --git a/changelogs/unreleased/ee-add-project-repository-storages-index.yml b/changelogs/unreleased/ee-add-project-repository-storages-index.yml
deleted file mode 100644
index 8a144783cec..00000000000
--- a/changelogs/unreleased/ee-add-project-repository-storages-index.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Index projects on repository storage
-merge_request: 14414
-author:
-type: other
diff --git a/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml b/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml
new file mode 100644
index 00000000000..1049e94f312
--- /dev/null
+++ b/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml
@@ -0,0 +1,5 @@
+---
+title: Enable UnnecessaryMantissa in scss-lint
+merge_request: 15255
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/events-atom-feed-author-query.yml b/changelogs/unreleased/events-atom-feed-author-query.yml
new file mode 100644
index 00000000000..84c51f25de7
--- /dev/null
+++ b/changelogs/unreleased/events-atom-feed-author-query.yml
@@ -0,0 +1,5 @@
+---
+title: Reuse authors when rendering event Atom feeds
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/expose-last-pipeline-details-in-api-for-single-commit.yml b/changelogs/unreleased/expose-last-pipeline-details-in-api-for-single-commit.yml
deleted file mode 100644
index d16e052cd92..00000000000
--- a/changelogs/unreleased/expose-last-pipeline-details-in-api-for-single-commit.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expose last pipeline details in API response when getting a single commit
-merge_request: 13521
-author: Mehdi Lahmam (@mehlah)
-type: added
diff --git a/changelogs/unreleased/feature-custom-attributes.yml b/changelogs/unreleased/feature-custom-attributes.yml
deleted file mode 100644
index 98736bc8d72..00000000000
--- a/changelogs/unreleased/feature-custom-attributes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Support custom attributes on users
-merge_request: 13038
-author: Markus Koller
diff --git a/changelogs/unreleased/feature-disable-password-authentication.yml b/changelogs/unreleased/feature-disable-password-authentication.yml
new file mode 100644
index 00000000000..999203f12ce
--- /dev/null
+++ b/changelogs/unreleased/feature-disable-password-authentication.yml
@@ -0,0 +1,5 @@
+---
+title: Allow password authentication to be disabled entirely
+merge_request: 15223
+author: Markus Koller
+type: changed
diff --git a/changelogs/unreleased/feature-sm-35954-create-kubernetes-cluster-on-gke-from-k8s-service.yml b/changelogs/unreleased/feature-sm-35954-create-kubernetes-cluster-on-gke-from-k8s-service.yml
deleted file mode 100644
index 14b35b6daee..00000000000
--- a/changelogs/unreleased/feature-sm-35954-create-kubernetes-cluster-on-gke-from-k8s-service.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create Kubernetes cluster on GKE from k8s service
-merge_request: 14470
-author:
-type: added
diff --git a/changelogs/unreleased/feature-verify_secondary_emails.yml b/changelogs/unreleased/feature-verify_secondary_emails.yml
deleted file mode 100644
index e1ecc527f85..00000000000
--- a/changelogs/unreleased/feature-verify_secondary_emails.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: A confirmation email is now sent when adding a secondary email address
-merge_request:
-author: digitalmoksha
-type: added
diff --git a/changelogs/unreleased/feature_add_mermaid.yml b/changelogs/unreleased/feature_add_mermaid.yml
new file mode 100644
index 00000000000..caeb5d3470d
--- /dev/null
+++ b/changelogs/unreleased/feature_add_mermaid.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add support of Mermaid (generation of diagrams and flowcharts from text)'
+merge_request: 15107
+author: Vitaliy @blackst0ne Klachkov
+type: added
diff --git a/changelogs/unreleased/ff_port_from_ee.yml b/changelogs/unreleased/ff_port_from_ee.yml
deleted file mode 100644
index e1cb7804a47..00000000000
--- a/changelogs/unreleased/ff_port_from_ee.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move Custom merge methods from EE
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/fix-edit-project-service-cancel-button-position.yml b/changelogs/unreleased/fix-edit-project-service-cancel-button-position.yml
deleted file mode 100644
index efb993eff71..00000000000
--- a/changelogs/unreleased/fix-edit-project-service-cancel-button-position.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix edit project service cancel button position
-merge_request: 14596
-author: Matt Coleman
-type: fixed
diff --git a/changelogs/unreleased/fix-filter-by-my-reaction.yml b/changelogs/unreleased/fix-filter-by-my-reaction.yml
new file mode 100644
index 00000000000..8bf91ddf893
--- /dev/null
+++ b/changelogs/unreleased/fix-filter-by-my-reaction.yml
@@ -0,0 +1,5 @@
+---
+title: Fix filter by my reaction is not working
+merge_request: 15345
+author: Hiroyuki Sato
+type: fixed
diff --git a/changelogs/unreleased/fix-gpg-case-insensitive.yml b/changelogs/unreleased/fix-gpg-case-insensitive.yml
deleted file mode 100644
index 744ec00a4a8..00000000000
--- a/changelogs/unreleased/fix-gpg-case-insensitive.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Compare email addresses case insensitively when verifying GPG signatures
-merge_request: 14376
-author: Tim Bishop
-type: fixed
diff --git a/changelogs/unreleased/fix-image-diff-swipe-handle.yml b/changelogs/unreleased/fix-image-diff-swipe-handle.yml
deleted file mode 100644
index a4e0c2e8465..00000000000
--- a/changelogs/unreleased/fix-image-diff-swipe-handle.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix image diff swipe handle offset to correctly align with the frame
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-import-uploads-hashed-storage.yml b/changelogs/unreleased/fix-import-uploads-hashed-storage.yml
new file mode 100644
index 00000000000..d43cabbfb8f
--- /dev/null
+++ b/changelogs/unreleased/fix-import-uploads-hashed-storage.yml
@@ -0,0 +1,5 @@
+---
+title: Fix hashed storage for Import/Export uploads
+merge_request: 15482
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-multi-line-hook-output.yml b/changelogs/unreleased/fix-multi-line-hook-output.yml
deleted file mode 100644
index f625ec2ee6c..00000000000
--- a/changelogs/unreleased/fix-multi-line-hook-output.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display full pre-receive and post-receive hook output in GitLab UI
-merge_request: 14222
-author: Robin Bobbitt
-type: fixed
diff --git a/changelogs/unreleased/fix-protected-branches-descriptions.yml b/changelogs/unreleased/fix-protected-branches-descriptions.yml
new file mode 100644
index 00000000000..8e233d9defd
--- /dev/null
+++ b/changelogs/unreleased/fix-protected-branches-descriptions.yml
@@ -0,0 +1,5 @@
+---
+title: Clarify wording of protected branch settings for the default branch
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/fix-sidebar-with-scrollbars.yml b/changelogs/unreleased/fix-sidebar-with-scrollbars.yml
deleted file mode 100644
index e0b3851b97f..00000000000
--- a/changelogs/unreleased/fix-sidebar-with-scrollbars.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed the sidebar scrollbar overlapping links
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml b/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml
new file mode 100644
index 00000000000..198116f34aa
--- /dev/null
+++ b/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml
@@ -0,0 +1,5 @@
+---
+title: Only owner or master can erase jobs
+merge_request: 15216
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-sm-37991-avoid-deactivation-when-pipeline-schedules-execute-a-commit-includes-ci-skip.yml b/changelogs/unreleased/fix-sm-37991-avoid-deactivation-when-pipeline-schedules-execute-a-commit-includes-ci-skip.yml
new file mode 100644
index 00000000000..4e525c875ca
--- /dev/null
+++ b/changelogs/unreleased/fix-sm-37991-avoid-deactivation-when-pipeline-schedules-execute-a-commit-includes-ci-skip.yml
@@ -0,0 +1,6 @@
+---
+title: Avoid deactivation when pipeline schedules execute a branch includes `[ci skip]`
+ comment
+merge_request: 15405
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-todos-last-page.yml b/changelogs/unreleased/fix-todos-last-page.yml
new file mode 100644
index 00000000000..efcdbb75e6e
--- /dev/null
+++ b/changelogs/unreleased/fix-todos-last-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fix access to the final page of todos
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-tooltip-width-issue-board.yml b/changelogs/unreleased/fix-tooltip-width-issue-board.yml
deleted file mode 100644
index a648953c5bd..00000000000
--- a/changelogs/unreleased/fix-tooltip-width-issue-board.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Issue board tooltips are now the correct width when the column is collapsed
-merge_request:
-author: Jedidiah Broadbent
-type: fixed
diff --git a/changelogs/unreleased/fix-update-doorkeeper-openid-connect.yml b/changelogs/unreleased/fix-update-doorkeeper-openid-connect.yml
deleted file mode 100644
index c57fceec92f..00000000000
--- a/changelogs/unreleased/fix-update-doorkeeper-openid-connect.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade doorkeeper-openid_connect
-merge_request: 14372
-author: Markus Koller
-type: other
diff --git a/changelogs/unreleased/fl-fix-ca-time-component.yml b/changelogs/unreleased/fl-fix-ca-time-component.yml
deleted file mode 100644
index ecd377409ca..00000000000
--- a/changelogs/unreleased/fl-fix-ca-time-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix typo in cycle analytics breaking time component
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fl-upgrade-svg.yml b/changelogs/unreleased/fl-upgrade-svg.yml
new file mode 100644
index 00000000000..caf73881d0f
--- /dev/null
+++ b/changelogs/unreleased/fl-upgrade-svg.yml
@@ -0,0 +1,5 @@
+---
+title: Update svg external depencency
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/force-two-up-view.yml b/changelogs/unreleased/force-two-up-view.yml
deleted file mode 100644
index 1074eb384bb..00000000000
--- a/changelogs/unreleased/force-two-up-view.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Force two up view to load by default for image diffs
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml b/changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml
deleted file mode 100644
index 13ec113167f..00000000000
--- a/changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bump google-api-client Gem from 0.8.6 to 0.13.6
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/gitaly_feature_flag_metadata.yml b/changelogs/unreleased/gitaly_feature_flag_metadata.yml
deleted file mode 100644
index 58e42ef9324..00000000000
--- a/changelogs/unreleased/gitaly_feature_flag_metadata.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add client and call site metadata to Gitaly calls for better traceability
-merge_request: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14332
-author:
-type: added
diff --git a/changelogs/unreleased/group-milestones-breadcrumb.yml b/changelogs/unreleased/group-milestones-breadcrumb.yml
deleted file mode 100644
index 87085759fda..00000000000
--- a/changelogs/unreleased/group-milestones-breadcrumb.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed milestone breadcrumb links
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/group-new-miletone-breadcrumb.yml b/changelogs/unreleased/group-new-miletone-breadcrumb.yml
new file mode 100644
index 00000000000..b82c5b604e8
--- /dev/null
+++ b/changelogs/unreleased/group-new-miletone-breadcrumb.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed new group milestone breadcrumbs
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/hash-mr-scroll-load.yml b/changelogs/unreleased/hash-mr-scroll-load.yml
deleted file mode 100644
index 7e3965add03..00000000000
--- a/changelogs/unreleased/hash-mr-scroll-load.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed notes not being scrolled to in merge requests
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/hashed-storage-attachments-migration-path.yml b/changelogs/unreleased/hashed-storage-attachments-migration-path.yml
new file mode 100644
index 00000000000..32535437046
--- /dev/null
+++ b/changelogs/unreleased/hashed-storage-attachments-migration-path.yml
@@ -0,0 +1,5 @@
+---
+title: Hashed Storage migration script now supports migrating project attachments
+merge_request: 15352
+author:
+type: added
diff --git a/changelogs/unreleased/hashed-storage-migration-path.yml b/changelogs/unreleased/hashed-storage-migration-path.yml
deleted file mode 100644
index 5890eb09c38..00000000000
--- a/changelogs/unreleased/hashed-storage-migration-path.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Script to migrate project's repositories to new Hashed Storage
-merge_request: 14067
-author:
-type: added
diff --git a/changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml b/changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml
deleted file mode 100644
index 22ac9b9073f..00000000000
--- a/changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Hide read_registry scope when registry is disabled on instance
-merge_request: 13314
-author: Robin Bobbitt
diff --git a/changelogs/unreleased/ie-event-polyfill.yml b/changelogs/unreleased/ie-event-polyfill.yml
deleted file mode 100644
index eaab089a47e..00000000000
--- a/changelogs/unreleased/ie-event-polyfill.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds Event polyfill for IE11
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/import-sources-fix.yml b/changelogs/unreleased/import-sources-fix.yml
deleted file mode 100644
index 03e23bc617c..00000000000
--- a/changelogs/unreleased/import-sources-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Read import sources from setting at first initialization
-merge_request: 14141
-author: Visay Keo
-type: fixed
diff --git a/changelogs/unreleased/improve_sorting_list.yml b/changelogs/unreleased/improve_sorting_list.yml
deleted file mode 100644
index a3730e23ed1..00000000000
--- a/changelogs/unreleased/improve_sorting_list.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve list of sorting options
-merge_request: 14320
-author: Vitaliy @blackst0ne Klachkov
-type: added
diff --git a/changelogs/unreleased/improved-changes-dropdown.yml b/changelogs/unreleased/improved-changes-dropdown.yml
new file mode 100644
index 00000000000..f305cbe573b
--- /dev/null
+++ b/changelogs/unreleased/improved-changes-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Improved diff changed files dropdown design
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/issue_32215.yml b/changelogs/unreleased/issue_32215.yml
deleted file mode 100644
index c608eb6dd28..00000000000
--- a/changelogs/unreleased/issue_32215.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow developer role to admin milestones
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/issue_35873.yml b/changelogs/unreleased/issue_35873.yml
deleted file mode 100644
index 65064b97e56..00000000000
--- a/changelogs/unreleased/issue_35873.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Commenting on image diffs
-merge_request: 14061
-author:
-type: added
diff --git a/changelogs/unreleased/issue_39238.yml b/changelogs/unreleased/issue_39238.yml
new file mode 100644
index 00000000000..75a4969ca9e
--- /dev/null
+++ b/changelogs/unreleased/issue_39238.yml
@@ -0,0 +1,5 @@
+---
+title: Fix image diff notification email from showing wrong content
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue_40374.yml b/changelogs/unreleased/issue_40374.yml
new file mode 100644
index 00000000000..73b48b890fe
--- /dev/null
+++ b/changelogs/unreleased/issue_40374.yml
@@ -0,0 +1,5 @@
+---
+title: Fix WIP system note not being created
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/italicized_emoji.yml b/changelogs/unreleased/italicized_emoji.yml
deleted file mode 100644
index d3f15f94363..00000000000
--- a/changelogs/unreleased/italicized_emoji.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update native unicode emojis to always render as normal text (previously could render italicized)
-merge_request:
-author: Branka Martinovic
-type: fixed
diff --git a/changelogs/unreleased/jk-group-mentions-fix.yml b/changelogs/unreleased/jk-group-mentions-fix.yml
new file mode 100644
index 00000000000..a28e3a87b6d
--- /dev/null
+++ b/changelogs/unreleased/jk-group-mentions-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fix link text from group context
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jobs-sort-by-id.yml b/changelogs/unreleased/jobs-sort-by-id.yml
deleted file mode 100644
index ec2c3a17b74..00000000000
--- a/changelogs/unreleased/jobs-sort-by-id.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Sort JobsController by id, not created_at
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/lint-changelog-yaml.yml b/changelogs/unreleased/lint-changelog-yaml.yml
deleted file mode 100644
index dcc8bf54827..00000000000
--- a/changelogs/unreleased/lint-changelog-yaml.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Detect when changelog entries are invalid
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/mentions-in-comments.yml b/changelogs/unreleased/mentions-in-comments.yml
deleted file mode 100644
index 907f455007b..00000000000
--- a/changelogs/unreleased/mentions-in-comments.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Makes @mentions links have a different styling for better separation
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/merge-request-notes-performance.yml b/changelogs/unreleased/merge-request-notes-performance.yml
deleted file mode 100644
index 6cf7a5047df..00000000000
--- a/changelogs/unreleased/merge-request-notes-performance.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use a UNION ALL for getting merge request notes
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/merge-requests-schema-cleanup.yml b/changelogs/unreleased/merge-requests-schema-cleanup.yml
new file mode 100644
index 00000000000..ccce9b1436c
--- /dev/null
+++ b/changelogs/unreleased/merge-requests-schema-cleanup.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up schema of the "merge_requests" table
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/milestone-avatar-issuable-link.yml b/changelogs/unreleased/milestone-avatar-issuable-link.yml
deleted file mode 100644
index 7915ad60fa8..00000000000
--- a/changelogs/unreleased/milestone-avatar-issuable-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed milestone issuable assignee link URL
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/mk-add-user-rate-limits.yml b/changelogs/unreleased/mk-add-user-rate-limits.yml
new file mode 100644
index 00000000000..512757da5fc
--- /dev/null
+++ b/changelogs/unreleased/mk-add-user-rate-limits.yml
@@ -0,0 +1,6 @@
+---
+title: Add anonymous rate limit per IP, and authenticated (web or API) rate limits
+ per user
+merge_request: 14708
+author:
+type: added
diff --git a/changelogs/unreleased/mk-clarify-moving-namespaces.yml b/changelogs/unreleased/mk-clarify-moving-namespaces.yml
deleted file mode 100644
index 8d89c296f61..00000000000
--- a/changelogs/unreleased/mk-clarify-moving-namespaces.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expand docs for changing username or group path
-merge_request: 13914
-author:
-type: other
diff --git a/changelogs/unreleased/mk-normalize-ldap-user-dns.yml b/changelogs/unreleased/mk-normalize-ldap-user-dns.yml
deleted file mode 100644
index 5a128d6acc1..00000000000
--- a/changelogs/unreleased/mk-normalize-ldap-user-dns.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Search or compare LDAP DNs case-insensitively and ignore excess whitespace
-merge_request: 14697
-author:
-type: fixed
diff --git a/changelogs/unreleased/mr-side-by-side-breadcrumbs-container.yml b/changelogs/unreleased/mr-side-by-side-breadcrumbs-container.yml
deleted file mode 100644
index 39b636bdfda..00000000000
--- a/changelogs/unreleased/mr-side-by-side-breadcrumbs-container.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed breadcrumbs container expanding in side-by-side diff view
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/mr-widget-merged-date-tooltip.yml b/changelogs/unreleased/mr-widget-merged-date-tooltip.yml
deleted file mode 100644
index ea22993ff52..00000000000
--- a/changelogs/unreleased/mr-widget-merged-date-tooltip.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed merge request widget merged & closed date tooltip text
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/optimise-stuck-ci-jobs-worker.yml b/changelogs/unreleased/optimise-stuck-ci-jobs-worker.yml
new file mode 100644
index 00000000000..7f6adfb4fd8
--- /dev/null
+++ b/changelogs/unreleased/optimise-stuck-ci-jobs-worker.yml
@@ -0,0 +1,5 @@
+---
+title: Optimise StuckCiJobsWorker using cheap SQL query outside, and expensive inside
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/pawel-update_prometheus_gem_to_well_tested_version.yml b/changelogs/unreleased/pawel-update_prometheus_gem_to_well_tested_version.yml
new file mode 100644
index 00000000000..a4133ae5cec
--- /dev/null
+++ b/changelogs/unreleased/pawel-update_prometheus_gem_to_well_tested_version.yml
@@ -0,0 +1,5 @@
+---
+title: Reenable Prometheus metrics, add more control over Prometheus method instrumentation
+merge_request: 15558
+author:
+type: fixed
diff --git a/changelogs/unreleased/project-page-clearer.yml b/changelogs/unreleased/project-page-clearer.yml
deleted file mode 100644
index 7db01373360..00000000000
--- a/changelogs/unreleased/project-page-clearer.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added tabs to dashboard/projects to easily switch to personal projects
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/protected-branches-names.yml b/changelogs/unreleased/protected-branches-names.yml
new file mode 100644
index 00000000000..3c6767df571
--- /dev/null
+++ b/changelogs/unreleased/protected-branches-names.yml
@@ -0,0 +1,5 @@
+---
+title: Only load branch names for protected branch checks
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/rc-fix-gh-import-branches-performance.yml b/changelogs/unreleased/rc-fix-gh-import-branches-performance.yml
deleted file mode 100644
index af359ce96b4..00000000000
--- a/changelogs/unreleased/rc-fix-gh-import-branches-performance.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve GitHub import performance
-merge_request: 14445
-author:
-type: other
diff --git a/changelogs/unreleased/rd-fix-case-sensative-email-conf-signup.yml b/changelogs/unreleased/rd-fix-case-sensative-email-conf-signup.yml
deleted file mode 100644
index 69695e403a9..00000000000
--- a/changelogs/unreleased/rd-fix-case-sensative-email-conf-signup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix case sensitive email confirmation on signup
-merge_request: 14606
-author: robdel12
-type: fixed
diff --git a/changelogs/unreleased/refactor-animate-js.yml b/changelogs/unreleased/refactor-animate-js.yml
deleted file mode 100644
index ec32d68bbdd..00000000000
--- a/changelogs/unreleased/refactor-animate-js.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove animate.js and label animation.
-merge_request:
-author:
-type: removed
diff --git a/changelogs/unreleased/refactor-monitoring-service.yml b/changelogs/unreleased/refactor-monitoring-service.yml
deleted file mode 100644
index 685397cadb8..00000000000
--- a/changelogs/unreleased/refactor-monitoring-service.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Perform prometheus data endpoint requests in parallel
-merge_request: 14003
-author:
-type: fixed
diff --git a/changelogs/unreleased/remote_user.yml b/changelogs/unreleased/remote_user.yml
deleted file mode 100644
index 75a941fa95f..00000000000
--- a/changelogs/unreleased/remote_user.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add username as GL_USERNAME in hooks
-merge_request:
-author:
diff --git a/changelogs/unreleased/remove-temporary-ci-index.yml b/changelogs/unreleased/remove-temporary-ci-index.yml
deleted file mode 100644
index a319f7fff7f..00000000000
--- a/changelogs/unreleased/remove-temporary-ci-index.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove an index on ci_builds meant to be only temporary
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/remove-use-key-worker.yml b/changelogs/unreleased/remove-use-key-worker.yml
deleted file mode 100644
index a39bcae66bc..00000000000
--- a/changelogs/unreleased/remove-use-key-worker.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Stop using Sidekiq for updating Key#last_used_at
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/remove_repo_prefix_from_api.yml b/changelogs/unreleased/remove_repo_prefix_from_api.yml
deleted file mode 100644
index bf2075e529c..00000000000
--- a/changelogs/unreleased/remove_repo_prefix_from_api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove 'Repo' prefix from API entites
-merge_request: 14694
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_emails-feature.yml b/changelogs/unreleased/replace_emails-feature.yml
deleted file mode 100644
index d7f1a7a7ba9..00000000000
--- a/changelogs/unreleased/replace_emails-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the profile/emails.feature spinach test with an rspec analog
-merge_request: 14172
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_group_links-feature.yml b/changelogs/unreleased/replace_group_links-feature.yml
deleted file mode 100644
index 7dd157632c9..00000000000
--- a/changelogs/unreleased/replace_group_links-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace project/group_links.feature spinach test with an rspec analog
-merge_request: 14169
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_milestone-feature.yml b/changelogs/unreleased/replace_milestone-feature.yml
deleted file mode 100644
index effe6d65645..00000000000
--- a/changelogs/unreleased/replace_milestone-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the project/milestone.feature spinach test with an rspec analog
-merge_request: 14171
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_profile_active_tab-feature.yml b/changelogs/unreleased/replace_profile_active_tab-feature.yml
deleted file mode 100644
index e911396a2b9..00000000000
--- a/changelogs/unreleased/replace_profile_active_tab-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the 'profile/active_tab.feature' spinach test with an rspec analog
-merge_request: 14239
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_project_archived-feature.yml b/changelogs/unreleased/replace_project_archived-feature.yml
deleted file mode 100644
index d0697347aa0..00000000000
--- a/changelogs/unreleased/replace_project_archived-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the 'project/archived.feature' spinach test with an rspec analog
-merge_request: 14322
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_project_builds_summary-feature.yml b/changelogs/unreleased/replace_project_builds_summary-feature.yml
deleted file mode 100644
index 48652b39b7e..00000000000
--- a/changelogs/unreleased/replace_project_builds_summary-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the 'project/builds/summary.feature' spinach test with an rspec analog
-merge_request: 14177
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_project_commits_revert-feature.yml b/changelogs/unreleased/replace_project_commits_revert-feature.yml
deleted file mode 100644
index 7fc9fcf3580..00000000000
--- a/changelogs/unreleased/replace_project_commits_revert-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the 'project/commits/revert.feature' spinach test with an rspec analog
-merge_request: 14325
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_project_issues_award_emoji-feature.yml b/changelogs/unreleased/replace_project_issues_award_emoji-feature.yml
deleted file mode 100644
index a4a7435d4fa..00000000000
--- a/changelogs/unreleased/replace_project_issues_award_emoji-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the 'project/issues/award_emoji.feature' spinach test with an rspec analog
-merge_request: 14202
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_project_merge_requests-feature.yml b/changelogs/unreleased/replace_project_merge_requests-feature.yml
deleted file mode 100644
index 082c922a32b..00000000000
--- a/changelogs/unreleased/replace_project_merge_requests-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the 'project/merge_requests.feature' spinach test with an rspec analog
-merge_request: 14621
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_project_merge_requests_accept-feature.yml b/changelogs/unreleased/replace_project_merge_requests_accept-feature.yml
deleted file mode 100644
index 03562d6025e..00000000000
--- a/changelogs/unreleased/replace_project_merge_requests_accept-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the 'project/merge_requests/accept.feature' spinach test with an rspec analog
-merge_request: 14176
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_project_merge_requests_revert-feature.yml b/changelogs/unreleased/replace_project_merge_requests_revert-feature.yml
deleted file mode 100644
index 7d1ab4566b6..00000000000
--- a/changelogs/unreleased/replace_project_merge_requests_revert-feature.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Replace the 'project/merge_requests/revert.feature' spinach test with an rspec
- analog
-merge_request: 14201
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_project_service-feature.yml b/changelogs/unreleased/replace_project_service-feature.yml
deleted file mode 100644
index 11814732a9b..00000000000
--- a/changelogs/unreleased/replace_project_service-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the 'project/service.feature' spinach test with an rspec analog
-merge_request: 14432
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_project_shortcuts-feature.yml b/changelogs/unreleased/replace_project_shortcuts-feature.yml
deleted file mode 100644
index 89e47a7a983..00000000000
--- a/changelogs/unreleased/replace_project_shortcuts-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the 'project/shortcuts.feature' spinach test with an rspec analog
-merge_request: 14431
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_project_snippets-feature.yml b/changelogs/unreleased/replace_project_snippets-feature.yml
deleted file mode 100644
index 4fdee70008a..00000000000
--- a/changelogs/unreleased/replace_project_snippets-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the 'project/snippets.feature' spinach test with an rspec analog
-merge_request: 14326
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_search-feature.yml b/changelogs/unreleased/replace_search-feature.yml
deleted file mode 100644
index 487f602ba30..00000000000
--- a/changelogs/unreleased/replace_search-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the 'search.feature' spinach test with an rspec analog
-merge_request: 14248
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_spinach_wiki-feature.yml b/changelogs/unreleased/replace_spinach_wiki-feature.yml
deleted file mode 100644
index a1801f1b58d..00000000000
--- a/changelogs/unreleased/replace_spinach_wiki-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace 'project/wiki.feature' spinach test with an rspec analog
-merge_request: 13856
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/replace_team_management-feature.yml b/changelogs/unreleased/replace_team_management-feature.yml
deleted file mode 100644
index bc2bb17faf1..00000000000
--- a/changelogs/unreleased/replace_team_management-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the project/team_management.feature spinach test with an rspec analog
-merge_request: 14173
-author: Vitaliy @blackst0ne Klachkov
-type: other
diff --git a/changelogs/unreleased/rotated_profile_image.yml b/changelogs/unreleased/rotated_profile_image.yml
deleted file mode 100644
index 1e221e47379..00000000000
--- a/changelogs/unreleased/rotated_profile_image.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix profile image orientation based on EXIF data gvieira37
-merge_request: 14461
-author: gvieira37
-type: fixed
diff --git a/changelogs/unreleased/save-a-query-on-todos-with-no-filters.yml b/changelogs/unreleased/save-a-query-on-todos-with-no-filters.yml
deleted file mode 100644
index c9fb042aa37..00000000000
--- a/changelogs/unreleased/save-a-query-on-todos-with-no-filters.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove a SQL query from the todos index page
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/sh-fix-username-logging.yml b/changelogs/unreleased/sh-fix-username-logging.yml
deleted file mode 100644
index dadf3fb6729..00000000000
--- a/changelogs/unreleased/sh-fix-username-logging.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix username and ID not logging in production_json.log for Git activity
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-show-all-slack-names.yml b/changelogs/unreleased/sh-show-all-slack-names.yml
deleted file mode 100644
index f970cd0fb15..00000000000
--- a/changelogs/unreleased/sh-show-all-slack-names.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Include GitLab full name in Slack messages
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/sh-thread-safe-markdown.yml b/changelogs/unreleased/sh-thread-safe-markdown.yml
deleted file mode 100644
index af7d9d58a9f..00000000000
--- a/changelogs/unreleased/sh-thread-safe-markdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make Redcarpet Markdown renderer thread-safe
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/skip_confirmation_user_API.yml b/changelogs/unreleased/skip_confirmation_user_API.yml
new file mode 100644
index 00000000000..144ccd69e68
--- /dev/null
+++ b/changelogs/unreleased/skip_confirmation_user_API.yml
@@ -0,0 +1,7 @@
+---
+title: Add email confirmation parameters for user creation and update via API
+merge_request:
+author: Daniel Juarez
+type: added
+
+
diff --git a/changelogs/unreleased/tag-link-size.yml b/changelogs/unreleased/tag-link-size.yml
deleted file mode 100644
index d94e415ba1f..00000000000
--- a/changelogs/unreleased/tag-link-size.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjusts tag link to avoid underlining spaces
-merge_request: 14544
-author: Guilherme Vieira
-type: fixed
diff --git a/changelogs/unreleased/tc-geo-read-only-idea.yml b/changelogs/unreleased/tc-geo-read-only-idea.yml
deleted file mode 100644
index e1b52eef2ca..00000000000
--- a/changelogs/unreleased/tc-geo-read-only-idea.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create idea of read-only database
-merge_request: 14688
-author:
-type: changed
diff --git a/changelogs/unreleased/text-utils.yml b/changelogs/unreleased/text-utils.yml
new file mode 100644
index 00000000000..b95bb82fe01
--- /dev/null
+++ b/changelogs/unreleased/text-utils.yml
@@ -0,0 +1,5 @@
+---
+title: Export text utils functions as es6 module and add tests
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/tm-feature-list-runners-jobs-api.yml b/changelogs/unreleased/tm-feature-list-runners-jobs-api.yml
new file mode 100644
index 00000000000..d75a2b68c30
--- /dev/null
+++ b/changelogs/unreleased/tm-feature-list-runners-jobs-api.yml
@@ -0,0 +1,5 @@
+---
+title: New API endpoint - list jobs for a specified runner
+merge_request: 15432
+author:
+type: added
diff --git a/changelogs/unreleased/tm-feature-namespace-by-id-api.yml b/changelogs/unreleased/tm-feature-namespace-by-id-api.yml
new file mode 100644
index 00000000000..bc4a8949d28
--- /dev/null
+++ b/changelogs/unreleased/tm-feature-namespace-by-id-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add new API endpoint - get a namespace by ID
+merge_request: 15442
+author:
+type: added
diff --git a/changelogs/unreleased/uipolish-fix-2factor-warning.yml b/changelogs/unreleased/uipolish-fix-2factor-warning.yml
deleted file mode 100644
index 9f55207d309..00000000000
--- a/changelogs/unreleased/uipolish-fix-2factor-warning.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Two factor auth messages in settings no longer overlap the button
-merge_request:
-author: Jedidiah Broadbent
-type: fixed
diff --git a/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml b/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml
deleted file mode 100644
index 34aa3d0db6f..00000000000
--- a/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Made the "remember me" check boxes have consistent styles and alignment
-merge_request:
-author: Jedidiah Broadbent
-type: fixed
diff --git a/changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml b/changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml
new file mode 100644
index 00000000000..e509a8df6bc
--- /dev/null
+++ b/changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml
@@ -0,0 +1,6 @@
+---
+title: 'Update emojis. Add :gay_pride_flag: and :speech_left:. Remove extraneous comma
+ in :cartwheel_tone4:'
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/update-merge-worker-metrics.yml b/changelogs/unreleased/update-merge-worker-metrics.yml
new file mode 100644
index 00000000000..c733675926a
--- /dev/null
+++ b/changelogs/unreleased/update-merge-worker-metrics.yml
@@ -0,0 +1,5 @@
+---
+title: Add performance logging to UpdateMergeRequestsWorker.
+merge_request: 15360
+author:
+type: performance
diff --git a/changelogs/unreleased/update-pages-0-6.yml b/changelogs/unreleased/update-pages-0-6.yml
deleted file mode 100644
index 507bb4d78e9..00000000000
--- a/changelogs/unreleased/update-pages-0-6.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update GitLab Pages to v0.6.0
-merge_request: 14630
-author:
-type: other
diff --git a/changelogs/unreleased/use-count_commits-directly.yml b/changelogs/unreleased/use-count_commits-directly.yml
new file mode 100644
index 00000000000..549e0744ea4
--- /dev/null
+++ b/changelogs/unreleased/use-count_commits-directly.yml
@@ -0,0 +1,5 @@
+---
+title: Improve the performance for counting commits
+merge_request: 15628
+author:
+type: performance
diff --git a/changelogs/unreleased/use-merge-requests-diff-id-column.yml b/changelogs/unreleased/use-merge-requests-diff-id-column.yml
new file mode 100644
index 00000000000..da4106ec8cf
--- /dev/null
+++ b/changelogs/unreleased/use-merge-requests-diff-id-column.yml
@@ -0,0 +1,5 @@
+---
+title: Make finding most recent merge request diffs more efficient
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/valid-branch-name-dash-bug.yml b/changelogs/unreleased/valid-branch-name-dash-bug.yml
deleted file mode 100644
index 89e4578b3e5..00000000000
--- a/changelogs/unreleased/valid-branch-name-dash-bug.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent branches or tags from starting with invalid characters (e.g. -, .)
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/voogsgerd-gitlab-ce-daniel-legacy-config.yml b/changelogs/unreleased/voogsgerd-gitlab-ce-daniel-legacy-config.yml
deleted file mode 100644
index faa5d3303c6..00000000000
--- a/changelogs/unreleased/voogsgerd-gitlab-ce-daniel-legacy-config.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removed two legacy config options
-merge_request:
-author: Daniel Voogsgerd
-type: deprecated
diff --git a/changelogs/unreleased/winh-delete-account-modal.yml b/changelogs/unreleased/winh-delete-account-modal.yml
deleted file mode 100644
index f1e2710fdcc..00000000000
--- a/changelogs/unreleased/winh-delete-account-modal.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show confirmation modal before deleting account
-merge_request: 14360
-author:
-type: changed
diff --git a/changelogs/unreleased/winh-protected-branch-modal-merged.yml b/changelogs/unreleased/winh-protected-branch-modal-merged.yml
deleted file mode 100644
index 63f1f424a5d..00000000000
--- a/changelogs/unreleased/winh-protected-branch-modal-merged.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display whether branch has been merged when deleting protected branch
-merge_request: 14220
-author:
-type: changed
diff --git a/changelogs/unreleased/winh-sprintf.yml b/changelogs/unreleased/winh-sprintf.yml
deleted file mode 100644
index f8ae5932ae4..00000000000
--- a/changelogs/unreleased/winh-sprintf.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add basic sprintf implementation to JavaScript
-merge_request: 14506
-author:
-type: other
diff --git a/changelogs/unreleased/winh-subgroups-api.yml b/changelogs/unreleased/winh-subgroups-api.yml
new file mode 100644
index 00000000000..c49e3621e9c
--- /dev/null
+++ b/changelogs/unreleased/winh-subgroups-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add /groups/:id/subgroups endpoint to API
+merge_request: 15142
+author: marbemac
+type: added
diff --git a/changelogs/unreleased/zj-commit-show-n-1.yml b/changelogs/unreleased/zj-commit-show-n-1.yml
new file mode 100644
index 00000000000..e536434f74a
--- /dev/null
+++ b/changelogs/unreleased/zj-commit-show-n-1.yml
@@ -0,0 +1,5 @@
+---
+title: Fetch blobs in bulk when generating diffs
+merge_request:
+author:
+type: performance
diff --git a/config/application.rb b/config/application.rb
index 31e91835b9e..6436f887d14 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -29,6 +29,7 @@ module Gitlab
#{config.root}/app/models/project_services
#{config.root}/app/workers/concerns
#{config.root}/app/services/concerns
+ #{config.root}/app/serializers/concerns
#{config.root}/app/finders/concerns])
config.generators.templates.push("#{config.root}/generator_templates")
@@ -112,7 +113,7 @@ module Gitlab
config.action_view.sanitized_allowed_protocols = %w(smb)
- config.middleware.insert_before Warden::Manager, Rack::Attack
+ config.middleware.insert_after Warden::Manager, Rack::Attack
# Allow access to GitLab API from other domains
config.middleware.insert_before Warden::Manager, Rack::Cors do
diff --git a/config/database.yml.mysql b/config/database.yml.mysql
index eb71d3f5fe1..98c2abe9f5e 100644
--- a/config/database.yml.mysql
+++ b/config/database.yml.mysql
@@ -10,7 +10,7 @@ production:
pool: 10
username: git
password: "secure password"
- # host: localhost
+ host: localhost
# socket: /tmp/mysql.sock
#
@@ -25,7 +25,22 @@ development:
pool: 5
username: root
password: "secure password"
- # host: localhost
+ host: localhost
+ # socket: /tmp/mysql.sock
+
+#
+# Staging specific
+#
+staging:
+ adapter: mysql2
+ encoding: utf8
+ collation: utf8_general_ci
+ reconnect: false
+ database: gitlabhq_staging
+ pool: 10
+ username: git
+ password: "secure password"
+ host: localhost
# socket: /tmp/mysql.sock
# Warning: The database defined as "test" will be erased and
@@ -40,6 +55,6 @@ test: &test
pool: 5
username: root
password:
- # host: localhost
+ host: localhost
# socket: /tmp/mysql.sock
prepared_statements: false
diff --git a/config/database.yml.postgresql b/config/database.yml.postgresql
index 4b30982fe82..baded682e46 100644
--- a/config/database.yml.postgresql
+++ b/config/database.yml.postgresql
@@ -6,10 +6,9 @@ production:
encoding: unicode
database: gitlabhq_production
pool: 10
- # username: git
- # password:
- # host: localhost
- # port: 5432
+ username: git
+ password: "secure password"
+ host: localhost
#
# Development specific
@@ -20,8 +19,8 @@ development:
database: gitlabhq_development
pool: 5
username: postgres
- password:
- # host: localhost
+ password: "secure password"
+ host: localhost
#
# Staging specific
@@ -30,10 +29,10 @@ staging:
adapter: postgresql
encoding: unicode
database: gitlabhq_staging
- pool: 5
- username: postgres
- password:
- # host: localhost
+ pool: 10
+ username: git
+ password: "secure password"
+ host: localhost
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
@@ -45,5 +44,5 @@ test: &test
pool: 5
username: postgres
password:
- # host: localhost
+ host: localhost
prepared_statements: false
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index db31b01a7d2..60df92a44fc 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -459,8 +459,47 @@
:versions: []
:when: 2017-09-13 17:31:16.425819400 Z
- - :approve
- - gitlab-svgs
+ - "@gitlab-org/gitlab-svgs"
- :who: Tim Zallmann
- :why: Our own library - https://gitlab.com/gitlab-org/gitlab-svgs
+ :why: Our own library - GitLab License https://gitlab.com/gitlab-org/gitlab-svgs
:versions: []
:when: 2017-09-19 14:36:32.795496000 Z
+- - :license
+ - pikaday
+ - MIT
+ - :who:
+ :why:
+ :versions: []
+ :when: 2017-10-17 17:46:12.367554000 Z
+- - :license
+ - component-emitter
+ - MIT
+ - :who: Winnie Hellmann
+ :why: package.json does not specify the license (README.md does)
+ :versions:
+ - 1.1.2
+ :when: 2017-11-13 12:23:10.502463000 Z
+- - :license
+ - json-schema
+ - BSD
+ - :who: Winnie Hellmann
+ :why: https://github.com/kriszyp/json-schema/blob/v0.2.3/package.json#L18-L19
+ :versions:
+ - 0.2.3
+ :when: 2017-11-16 12:52:18.286091000 Z
+- - :license
+ - node-forge
+ - New BSD
+ - :who: Winnie Hellmann
+ :why: https://github.com/digitalbazaar/forge/blob/0.6.33/LICENSE
+ :versions:
+ - 0.6.33
+ :when: 2017-11-16 12:56:17.974767000 Z
+- - :license
+ - sntp
+ - BSD
+ - :who: Winnie Hellmann
+ :why: https://github.com/hueniverse/sntp/blob/v1.0.9/package.json#L28-L29
+ :versions:
+ - 1.0.9
+ :when: 2017-11-16 13:02:06.765282000 Z
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 1edb6fd39b8..d09e51e766a 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,6 +1,7 @@
Rails.application.configure do
# Make sure the middleware is inserted first in middleware chain
config.middleware.insert_before('ActionDispatch::Static', 'Gitlab::Testing::RequestBlockerMiddleware')
+ config.middleware.insert_before('ActionDispatch::Static', 'Gitlab::Testing::RequestInspectorMiddleware')
# Settings specified here will take precedence over those in config/application.rb
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 1069c7be5f0..7f6e68ceed6 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -501,7 +501,7 @@ production: &base
# Gitaly settings
gitaly:
# Path to the directory containing Gitaly client executables.
- client_path: /home/git/gitaly
+ client_path: /home/git/gitaly/bin
# Default Gitaly authentication token. Can be overriden per storage. Can
# be left blank when Gitaly is running locally on a Unix socket, which
# is the normal way to deploy Gitaly.
@@ -522,11 +522,6 @@ production: &base
path: /home/git/repositories/
gitaly_address: unix:/home/git/gitlab/tmp/sockets/private/gitaly.socket # TCP connections are supported too (e.g. tcp://host:port)
# gitaly_token: 'special token' # Optional: override global gitaly.token for this storage.
- 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: 30 # Time in seconds to wait before aborting a storage access attempt
-
## Backup settings
backup:
@@ -564,8 +559,8 @@ production: &base
upload_pack: true
receive_pack: true
- # Git import/fetch timeout
- # git_timeout: 800
+ # Git import/fetch timeout, in seconds. Defaults to 3 hours.
+ # git_timeout: 10800
# If you use non-standard ssh port you need to specify it
# ssh_port: 22
@@ -659,9 +654,6 @@ test:
default:
path: tmp/tests/repositories/
gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
- failure_count_threshold: 999999
- failure_wait_time: 0
- storage_timeout: 30
broken:
path: tmp/tests/non-existent-repositories
gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index a4b7c1a3919..f10f0cdf42c 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -113,12 +113,14 @@ class Settings < Settingslogic
URI.parse(url_without_path).host
end
- # Random cron time every Sunday to load balance usage pings
- def cron_random_weekly_time
+ # Runs every minute in a random ten-minute period on Sundays, to balance the
+ # load on the server receiving these pings. The usage ping is safe to run
+ # multiple times because of a 24 hour exclusive lock.
+ def cron_for_usage_ping
hour = rand(24)
- minute = rand(60)
+ minute = rand(6)
- "#{minute} #{hour} * * 0"
+ "#{minute}0-#{minute}9 #{hour} * * 0"
end
end
end
@@ -254,7 +256,7 @@ rescue ArgumentError # no user configured
end
Settings.gitlab['time_zone'] ||= nil
Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil?
-Settings.gitlab['password_authentication_enabled'] ||= true if Settings.gitlab['password_authentication_enabled'].nil?
+Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil?
Settings.gitlab['restricted_visibility_levels'] = Settings.__send__(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil?
Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil?
@@ -398,7 +400,7 @@ Settings.cron_jobs['stuck_import_jobs_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_import_jobs_worker']['cron'] ||= '15 * * * *'
Settings.cron_jobs['stuck_import_jobs_worker']['job_class'] = 'StuckImportJobsWorker'
Settings.cron_jobs['gitlab_usage_ping_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= Settings.__send__(:cron_random_weekly_time)
+Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= Settings.__send__(:cron_for_usage_ping)
Settings.cron_jobs['gitlab_usage_ping_worker']['job_class'] = 'GitlabUsagePingWorker'
Settings.cron_jobs['schedule_update_user_activity_worker'] ||= Settingslogic.new({})
@@ -427,7 +429,7 @@ Settings.gitlab_shell['ssh_port'] ||= 22
Settings.gitlab_shell['ssh_user'] ||= Settings.gitlab.user
Settings.gitlab_shell['owner_group'] ||= Settings.gitlab.user
Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.__send__(:build_gitlab_shell_ssh_path_prefix)
-Settings.gitlab_shell['git_timeout'] ||= 800
+Settings.gitlab_shell['git_timeout'] ||= 10800
#
# Workhorse
@@ -453,17 +455,6 @@ Settings.repositories.storages.each do |key, storage|
# Expand relative paths
storage['path'] = Settings.absolute(storage['path'])
- # Set failure defaults
- storage['failure_count_threshold'] ||= 10
- storage['failure_wait_time'] ||= 30
- storage['failure_reset_time'] ||= 1800
- storage['storage_timeout'] ||= 5
- # Set turn strings into numbers
- storage['failure_count_threshold'] = storage['failure_count_threshold'].to_i
- storage['failure_wait_time'] = storage['failure_wait_time'].to_i
- storage['failure_reset_time'] = storage['failure_reset_time'].to_i
- # We might want to have a timeout shorter than 1 second.
- storage['storage_timeout'] = storage['storage_timeout'].to_f
Settings.repositories.storages[key] = storage
end
@@ -544,6 +535,7 @@ Settings.webpack.dev_server['port'] ||= 3808
Settings['monitoring'] ||= Settingslogic.new({})
Settings.monitoring['ip_whitelist'] ||= ['127.0.0.1/8']
Settings.monitoring['unicorn_sampler_interval'] ||= 10
+Settings.monitoring['ruby_sampler_interval'] ||= 60
Settings.monitoring['sidekiq_exporter'] ||= Settingslogic.new({})
Settings.monitoring.sidekiq_exporter['enabled'] ||= false
Settings.monitoring.sidekiq_exporter['address'] ||= 'localhost'
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index 31839297523..43b1e943897 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -11,7 +11,19 @@ Prometheus::Client.configure do |config|
config.multiprocess_files_dir ||= Rails.root.join('tmp/prometheus_multiproc_dir')
end
- config.pid_provider = Prometheus::Client::Support::Unicorn.method(:worker_pid_provider)
+ config.pid_provider = -> do
+ worker_id = Prometheus::Client::Support::Unicorn.worker_id
+ if worker_id.nil?
+ "process_pid_#{Process.pid}"
+ else
+ "worker_id_#{worker_id}"
+ end
+ end
+end
+
+Gitlab::Application.configure do |config|
+ # 0 should be Sentry to catch errors in this middleware
+ config.middleware.insert(1, Gitlab::Metrics::RequestsRackMiddleware)
end
Sidekiq.configure_server do |config|
@@ -19,3 +31,11 @@ Sidekiq.configure_server do |config|
Gitlab::Metrics::SidekiqMetricsExporter.instance.start
end
end
+
+if Gitlab::Metrics.prometheus_metrics_enabled?
+ unless Sidekiq.server?
+ Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
+ end
+
+ Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start
+end
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index e1a59d8c152..45b39b2a38d 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -77,7 +77,6 @@ def instrument_classes(instrumentation)
instrumentation.instrument_instance_methods(Banzai::ObjectRenderer)
instrumentation.instrument_instance_methods(Banzai::Redactor)
- instrumentation.instrument_methods(Banzai::NoteRenderer)
[Issuable, Mentionable, Participable].each do |klass|
instrumentation.instrument_instance_methods(klass)
@@ -116,20 +115,9 @@ def instrument_classes(instrumentation)
# Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/30224#note_32306159
instrumentation.instrument_instance_method(MergeRequestDiff, :load_commits)
-
- # Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/36061
- instrumentation.instrument_instance_method(MergeRequest, :ensure_ref_fetched)
- instrumentation.instrument_instance_method(MergeRequest, :fetch_ref)
end
# rubocop:enable Metrics/AbcSize
-Gitlab::Metrics::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
-
-Gitlab::Application.configure do |config|
- # 0 should be Sentry to catch errors in this middleware
- config.middleware.insert(1, Gitlab::Metrics::RequestsRackMiddleware)
-end
-
if Gitlab::Metrics.enabled?
require 'pathname'
require 'influxdb'
@@ -190,7 +178,7 @@ if Gitlab::Metrics.enabled?
GC::Profiler.enable
- Gitlab::Metrics::InfluxSampler.initialize_instance.start
+ Gitlab::Metrics::Samplers::InfluxSampler.initialize_instance.start
module TrackNewRedisConnections
def connect(*args)
diff --git a/config/initializers/ar5_batching.rb b/config/initializers/ar5_batching.rb
index 35e8b3808e2..6ebaf8834d2 100644
--- a/config/initializers/ar5_batching.rb
+++ b/config/initializers/ar5_batching.rb
@@ -34,6 +34,7 @@ module ActiveRecord
yield yielded_relation
break if ids.length < of
+
batch_relation = relation.where(arel_table[primary_key].gt(primary_key_offset))
end
end
diff --git a/config/initializers/batch_loader.rb b/config/initializers/batch_loader.rb
new file mode 100644
index 00000000000..2e2256b0eb9
--- /dev/null
+++ b/config/initializers/batch_loader.rb
@@ -0,0 +1 @@
+Rails.application.config.middleware.use(BatchLoader::Middleware)
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index c6ec0aeda7b..051ef93b205 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -195,7 +195,7 @@ Devise.setup do |config|
config.navigational_formats = [:"*/*", "*/*", :html, :zip]
# The default HTTP method used to sign out a resource. Default is :delete.
- config.sign_out_via = :delete
+ config.sign_out_via = :get
# ==> OmniAuth
# To configure a new OmniAuth provider copy and edit omniauth.rb.sample
@@ -236,6 +236,7 @@ Devise.setup do |config|
provider['args'][:on_single_sign_out] = lambda do |request|
ticket = request.params[:session_index]
raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket)
+
Gitlab::OAuth::Session.destroy(:cas3, ticket)
true
end
diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb
index 1ebe3c7a742..2fd47a3f4d3 100644
--- a/config/initializers/gollum.rb
+++ b/config/initializers/gollum.rb
@@ -10,4 +10,32 @@ module Gollum
index.send(name, *args)
end
end
+
+ class Wiki
+ def pages(treeish = nil, limit: nil)
+ tree_list((treeish || @ref), limit: limit)
+ end
+
+ def tree_list(ref, limit: nil)
+ if (sha = @access.ref_to_sha(ref))
+ commit = @access.commit(sha)
+ tree_map_for(sha).inject([]) do |list, entry|
+ next list unless @page_class.valid_page_name?(entry.name)
+
+ list << entry.page(self, commit)
+ break list if limit && list.size >= limit
+
+ list
+ end
+ else
+ []
+ end
+ end
+ end
+end
+
+Rails.application.configure do
+ config.after_initialize do
+ Gollum::Page.per_page = Kaminari.config.default_per_page
+ end
end
diff --git a/config/initializers/math_lexer.rb b/config/initializers/math_lexer.rb
deleted file mode 100644
index 8a3388a267e..00000000000
--- a/config/initializers/math_lexer.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-# Touch the lexers so it is registered with Rouge
-Rouge::Lexers::Math
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index fddb018e948..e9e1f1c4e9b 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -3,6 +3,7 @@ if Gitlab::LDAP::Config.enabled?
Gitlab::LDAP::Config.available_servers.each do |server|
# do not redeclare LDAP
next if server['provider_name'] == 'ldap'
+
const_set(server['provider_class'], Class.new(LDAP))
end
end
diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb
index a54d53cbbe2..1cff355346c 100644
--- a/config/initializers/peek.rb
+++ b/config/initializers/peek.rb
@@ -16,6 +16,7 @@ Peek.into Peek::Views::Redis
Peek.into Peek::Views::Sidekiq
Peek.into Peek::Views::Rblineprof
Peek.into Peek::Views::GC
+Peek.into Peek::Views::Gitaly
# rubocop:disable Style/ClassAndModuleCamelCase
class PEEK_DB_CLIENT
diff --git a/config/initializers/plantuml_lexer.rb b/config/initializers/plantuml_lexer.rb
deleted file mode 100644
index e8a77b146fa..00000000000
--- a/config/initializers/plantuml_lexer.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-# Touch the lexers so it is registered with Rouge
-Rouge::Lexers::Plantuml
diff --git a/config/initializers/postgresql_cte.rb b/config/initializers/postgresql_cte.rb
index 7f0df8949db..38a9cd68d57 100644
--- a/config/initializers/postgresql_cte.rb
+++ b/config/initializers/postgresql_cte.rb
@@ -61,11 +61,13 @@ module ActiveRecord
def with_values=(values)
raise ImmutableRelation if @loaded
+
@values[:with] = values
end
def recursive_value=(value)
raise ImmutableRelation if @loaded
+
@values[:recursive] = value
end
diff --git a/config/initializers/rack_attack_global.rb b/config/initializers/rack_attack_global.rb
new file mode 100644
index 00000000000..9453df2ec5a
--- /dev/null
+++ b/config/initializers/rack_attack_global.rb
@@ -0,0 +1,61 @@
+module Gitlab::Throttle
+ def self.settings
+ Gitlab::CurrentSettings.current_application_settings
+ end
+
+ def self.unauthenticated_options
+ limit_proc = proc { |req| settings.throttle_unauthenticated_requests_per_period }
+ period_proc = proc { |req| settings.throttle_unauthenticated_period_in_seconds.seconds }
+ { limit: limit_proc, period: period_proc }
+ end
+
+ def self.authenticated_api_options
+ limit_proc = proc { |req| settings.throttle_authenticated_api_requests_per_period }
+ period_proc = proc { |req| settings.throttle_authenticated_api_period_in_seconds.seconds }
+ { limit: limit_proc, period: period_proc }
+ end
+
+ def self.authenticated_web_options
+ limit_proc = proc { |req| settings.throttle_authenticated_web_requests_per_period }
+ period_proc = proc { |req| settings.throttle_authenticated_web_period_in_seconds.seconds }
+ { limit: limit_proc, period: period_proc }
+ end
+end
+
+class Rack::Attack
+ throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req|
+ Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
+ req.unauthenticated? &&
+ req.ip
+ end
+
+ throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req|
+ Gitlab::Throttle.settings.throttle_authenticated_api_enabled &&
+ req.api_request? &&
+ req.authenticated_user_id
+ end
+
+ throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
+ Gitlab::Throttle.settings.throttle_authenticated_web_enabled &&
+ req.web_request? &&
+ req.authenticated_user_id
+ end
+
+ class Request
+ def unauthenticated?
+ !authenticated_user_id
+ end
+
+ def authenticated_user_id
+ Gitlab::Auth::RequestAuthenticator.new(self).user&.id
+ end
+
+ def api_request?
+ path.start_with?('/api')
+ end
+
+ def web_request?
+ !api_request?
+ end
+ end
+end
diff --git a/config/karma.config.js b/config/karma.config.js
index e459f5cdac3..9f018d14b8f 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -67,14 +67,5 @@ module.exports = function(config) {
karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1');
}
- if (process.env.DEBUG) {
- karmaConfig.logLevel = config.LOG_DEBUG;
- process.env.CHROME_LOG_FILE = process.env.CHROME_LOG_FILE || 'chrome_debug.log';
- }
-
- if (process.env.CHROME_LOG_FILE) {
- karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1');
- }
-
config.set(karmaConfig);
};
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index 14d49885fb3..b1c71095d4f 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -58,10 +58,19 @@ en:
expired: "The access token expired"
unknown: "The access token is invalid"
scopes:
- api: Access your API
- read_user: Read user information
+ api: Access the authenticated user's API
+ read_user: Read the authenticated user's personal information
openid: Authenticate using OpenID Connect
-
+ sudo: Perform API actions as any user in the system (if the authenticated user is an admin)
+ scope_desc:
+ api:
+ Full access to GitLab as the user, including read/write on all their groups and projects
+ read_user:
+ Read-only access to the user's profile information, like username, public email and full name
+ openid:
+ The ability to authenticate using GitLab, and read-only access to the user's profile information
+ sudo:
+ Access to the Sudo feature, to perform API actions as any user in the system (only available for admins)
flash:
applications:
create:
diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml
index 33b897f46e2..601a86490d4 100644
--- a/config/prometheus/additional_metrics.yml
+++ b/config/prometheus/additional_metrics.yml
@@ -145,7 +145,7 @@
- container_memory_usage_bytes
weight: 1
queries:
- - query_range: '(sum(container_memory_usage_bytes{container_name!="POD",%{environment_filter}}) / count(container_memory_usage_bytes{container_name!="POD",%{environment_filter}})) /1024/1024'
+ - query_range: '(sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024'
label: Average
unit: MB
- title: "CPU Utilization"
@@ -154,8 +154,6 @@
- container_cpu_usage_seconds_total
weight: 1
queries:
- - query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100'
- label: CPU
- unit: "%"
- series:
- - label: cpu
+ - query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100'
+ label: Average
+ unit: "%" \ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 405bfcc2d8e..4f27fea0e92 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -44,6 +44,19 @@ Rails.application.routes.draw do
get 'readiness' => 'health#readiness'
resources :metrics, only: [:index]
mount Peek::Railtie => '/peek'
+
+ # Boards resources shared between group and projects
+ resources :boards, only: [] do
+ resources :lists, module: :boards, only: [:index, :create, :update, :destroy] do
+ collection do
+ post :generate
+ end
+
+ resources :issues, only: [:index, :create, :update]
+ end
+
+ resources :issues, module: :boards, only: [:index, :update]
+ end
end
# Koding route
@@ -74,19 +87,6 @@ Rails.application.routes.draw do
# Notification settings
resources :notification_settings, only: [:create, :update]
- # Boards resources shared between group and projects
- resources :boards do
- resources :lists, module: :boards, only: [:index, :create, :update, :destroy] do
- collection do
- post :generate
- end
-
- resources :issues, only: [:index, :create, :update]
- end
-
- resources :issues, module: :boards, only: [:index, :update]
- end
-
draw :google_api
draw :import
draw :uploads
@@ -100,7 +100,5 @@ Rails.application.routes.draw do
root to: "root#index"
- draw :test if Rails.env.test?
-
get '*unmatched_route', to: 'application#route_not_found'
end
diff --git a/config/routes/ci.rb b/config/routes/ci.rb
index cbd4c2db852..60c1724bc05 100644
--- a/config/routes/ci.rb
+++ b/config/routes/ci.rb
@@ -1,5 +1,5 @@
namespace :ci do
resource :lint, only: [:show, :create]
- root to: redirect('/')
+ root to: redirect('')
end
diff --git a/config/routes/google_api.rb b/config/routes/google_api.rb
index 3fb236d3d51..a119b47c176 100644
--- a/config/routes/google_api.rb
+++ b/config/routes/google_api.rb
@@ -1,5 +1,7 @@
-namespace :google_api do
- resource :auth, only: [], controller: :authorizations do
- match :callback, via: [:get, :post]
+scope '-' do
+ namespace :google_api do
+ resource :auth, only: [], controller: :authorizations do
+ match :callback, via: [:get, :post]
+ end
end
end
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 23052a6c6dc..db99e10bb9a 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -1,51 +1,56 @@
require 'constraints/group_url_constrainer'
-resources :groups, only: [:index, :new, :create]
-
-scope(path: 'groups/*group_id',
- module: :groups,
- as: :group,
- constraints: { group_id: Gitlab::PathRegex.full_namespace_route_regex }) do
- resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
- post :resend_invite, on: :member
- delete :leave, on: :collection
- end
+resources :groups, only: [:index, :new, :create] do
+ post :preview_markdown
+end
- resource :avatar, only: [:destroy]
- resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :edit, :update, :new, :create] do
- member do
- get :merge_requests
- get :participants
- get :labels
+constraints(GroupUrlConstrainer.new) do
+ scope(path: 'groups/*id',
+ controller: :groups,
+ constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do
+ scope(path: '-') do
+ get :edit, as: :edit_group
+ get :issues, as: :issues_group
+ get :merge_requests, as: :merge_requests_group
+ get :projects, as: :projects_group
+ get :activity, as: :activity_group
end
- end
- resources :labels, except: [:show] do
- post :toggle_subscription, on: :member
+ get '/', action: :show, as: :group_canonical
end
- scope path: '-' do
+ scope(path: 'groups/*group_id/-',
+ module: :groups,
+ as: :group,
+ constraints: { group_id: Gitlab::PathRegex.full_namespace_route_regex }) do
namespace :settings do
resource :ci_cd, only: [:show], controller: 'ci_cd'
end
resources :variables, only: [:index, :show, :update, :create, :destroy]
- end
-end
-scope(path: 'groups/*id',
- controller: :groups,
- constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ }) do
- get :edit, as: :edit_group
- get :issues, as: :issues_group
- get :merge_requests, as: :merge_requests_group
- get :projects, as: :projects_group
- get :activity, as: :activity_group
- get :subgroups, as: :subgroups_group
- get '/', action: :show, as: :group_canonical
-end
+ resources :children, only: [:index]
+
+ resources :labels, except: [:show] do
+ post :toggle_subscription, on: :member
+ end
+
+ resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :edit, :update, :new, :create] do
+ member do
+ get :merge_requests
+ get :participants
+ get :labels
+ end
+ end
+
+ resource :avatar, only: [:destroy]
+
+ resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
+ post :resend_invite, on: :member
+ delete :leave, on: :collection
+ end
+ end
-constraints(GroupUrlConstrainer.new) do
scope(path: '*id',
as: :group,
constraints: { id: Gitlab::PathRegex.full_namespace_route_regex, format: /(html|json|atom)/ },
@@ -55,4 +60,12 @@ constraints(GroupUrlConstrainer.new) do
put '/', action: :update
delete '/', action: :destroy
end
+
+ # Legacy paths should be defined last, so they would be ignored if routes with
+ # one of the previously reserved words exist.
+ scope(path: 'groups/*group_id') do
+ Gitlab::Routing.redirect_legacy_paths(self, :labels, :milestones, :group_members,
+ :edit, :issues, :merge_requests, :projects,
+ :activity)
+ end
end
diff --git a/config/routes/help.rb b/config/routes/help.rb
index d53822da9ec..2ea8bfd7aed 100644
--- a/config/routes/help.rb
+++ b/config/routes/help.rb
@@ -1,4 +1,5 @@
-get 'help' => 'help#index'
-get 'help/shortcuts' => 'help#shortcuts'
-get 'help/ui' => 'help#ui'
-get 'help/*path' => 'help#show', as: :help_page
+get 'help' => 'help#index'
+get 'help/shortcuts' => 'help#shortcuts'
+get 'help/ui' => 'help#ui'
+get 'help/instance_configuration' => 'help#instance_configuration'
+get 'help/*path' => 'help#show', as: :help_page
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index ddc852f0132..bcfc17a5f66 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -6,7 +6,6 @@ resource :profile, only: [:show, :update] do
get :audit_log
get :applications, to: 'oauth/applications#index'
- put :reset_private_token
put :reset_incoming_email_token
put :reset_rss_token
put :update_username
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 7f0e056c884..bdafaba3ab3 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -186,10 +186,15 @@ constraints(ProjectUrlConstrainer.new) do
resources :clusters, except: [:edit] do
collection do
get :login
+ get '/providers/gcp/new', action: :new_gcp
end
member do
get :status, format: :json
+
+ scope :applications do
+ post '/:application', to: 'clusters/applications#create', as: :install_applications
+ end
end
end
@@ -281,13 +286,19 @@ constraints(ProjectUrlConstrainer.new) do
namespace :registry do
resources :repository, only: [] do
- resources :tags, only: [:index, :destroy],
- constraints: { id: Gitlab::Regex.container_registry_tag_regex }
+ # We default to JSON format in the controller to avoid ambiguity.
+ # `latest.json` could either be a request for a tag named `latest`
+ # in JSON format, or a request for tag named `latest.json`.
+ scope format: false do
+ resources :tags, only: [:index, :destroy],
+ constraints: { id: Gitlab::Regex.container_registry_tag_regex }
+ end
end
end
resources :milestones, constraints: { id: /\d+/ } do
member do
+ post :promote
put :sort_issues
put :sort_merge_requests
get :merge_requests
@@ -388,7 +399,7 @@ constraints(ProjectUrlConstrainer.new) do
end
end
namespace :settings do
- get :members, to: redirect('/%{namespace_id}/%{project_id}/project_members')
+ get :members, to: redirect("%{namespace_id}/%{project_id}/project_members")
resource :ci_cd, only: [:show], controller: 'ci_cd'
resource :integrations, only: [:show]
resource :repository, only: [:show], controller: :repository
diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb
index 0a4ebac3ca3..81bc890d86b 100644
--- a/config/routes/snippets.rb
+++ b/config/routes/snippets.rb
@@ -17,5 +17,5 @@ resources :snippets, concerns: :awardable do
end
end
-get '/s/:username', to: redirect('/u/%{username}/snippets'),
+get '/s/:username', to: redirect('u/%{username}/snippets'),
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
diff --git a/config/routes/test.rb b/config/routes/test.rb
deleted file mode 100644
index ac477cdbbbc..00000000000
--- a/config/routes/test.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-get '/unicorn_test/pid' => 'unicorn_test#pid'
-post '/unicorn_test/kill' => 'unicorn_test#kill'
diff --git a/config/routes/user.rb b/config/routes/user.rb
index e682dcd6663..733a3f6ce9a 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -22,17 +22,17 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
get :contributed, as: :contributed_projects
get :snippets
get :exists
- get '/', to: redirect('/%{username}'), as: nil
+ get '/', to: redirect('%{username}'), as: nil
end
# Compatibility with old routing
# TODO (dzaporozhets): remove in 10.0
- get '/u/:username', to: redirect('/%{username}')
+ get '/u/:username', to: redirect('%{username}')
# TODO (dzaporozhets): remove in 9.0
- get '/u/:username/groups', to: redirect('/users/%{username}/groups')
- get '/u/:username/projects', to: redirect('/users/%{username}/projects')
- get '/u/:username/snippets', to: redirect('/users/%{username}/snippets')
- get '/u/:username/contributed', to: redirect('/users/%{username}/contributed')
+ get '/u/:username/groups', to: redirect('users/%{username}/groups')
+ get '/u/:username/projects', to: redirect('users/%{username}/projects')
+ get '/u/:username/snippets', to: redirect('users/%{username}/snippets')
+ get '/u/:username/contributed', to: redirect('users/%{username}/contributed')
end
constraints(UserUrlConstrainer.new) do
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index e2bb766ee47..bc7c431731a 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -28,6 +28,7 @@
- [build, 2]
- [pipeline, 2]
- [pipeline_processing, 5]
+ - [pipeline_creation, 4]
- [pipeline_default, 3]
- [pipeline_cache, 3]
- [pipeline_hooks, 2]
@@ -40,6 +41,8 @@
- [upload_checksum, 1]
- [repository_fork, 1]
- [repository_import, 1]
+ - [github_importer, 1]
+ - [github_importer_advance_stage, 1]
- [project_service, 1]
- [delete_user, 1]
- [delete_merged_branches, 1]
diff --git a/config/svg.config.js b/config/svg.config.js
index be72741abec..bb27f0caeef 100644
--- a/config/svg.config.js
+++ b/config/svg.config.js
@@ -2,8 +2,8 @@
const path = require('path');
const fs = require('fs');
-const sourcePath = path.join('node_modules', 'gitlab-svgs', 'dist');
-const sourcePathIllustrations = path.join('node_modules', 'gitlab-svgs', 'dist', 'illustrations');
+const sourcePath = path.join('node_modules', '@gitlab-org/gitlab-svgs', 'dist');
+const sourcePathIllustrations = path.join('node_modules', '@gitlab-org/gitlab-svgs', 'dist', 'illustrations');
const destPath = path.normalize(path.join('app', 'assets', 'images'));
// Actual Task copying the 2 files + all illustrations
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 8cded750a66..78ced4c3e8c 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -84,6 +84,7 @@ var config = {
vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js',
two_factor_auth: './two_factor_auth.js',
+ users: './users/index.js',
performance_bar: './performance_bar.js',
webpack_runtime: './webpack.js',
},
@@ -116,6 +117,10 @@ var config = {
options: { limit: 2048 },
},
{
+ test: /\_worker\.js$/,
+ loader: 'worker-loader',
+ },
+ {
test: /\.(worker(\.min)?\.js|pdf|bmpr)$/,
exclude: /node_modules/,
loader: 'file-loader',
@@ -215,7 +220,9 @@ var config = {
name: 'common_d3',
chunks: [
'graphs',
+ 'graphs_show',
'monitoring',
+ 'users',
],
}),
@@ -233,7 +240,7 @@ var config = {
from: path.join(ROOT_PATH, `node_modules/monaco-editor/${IS_PRODUCTION ? 'min' : 'dev'}/vs`),
to: 'monaco-editor/vs',
transform: function(content, path) {
- if (/\.js$/.test(path) && !/worker/i.test(path)) {
+ if (/\.js$/.test(path) && !/worker/i.test(path) && !/typescript/i.test(path)) {
return (
'(function(){\n' +
'var define = this.define, require = this.require;\n' +
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 383782112a8..96c6d954ff7 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -4,7 +4,7 @@ require './spec/support/test_env'
class Gitlab::Seeder::CycleAnalytics
def initialize(project, perf: false)
@project = project
- @user = User.order(:id).last
+ @user = User.admins.first
@issue_count = perf ? 1000 : 5
stub_git_pre_receive!
end
@@ -77,39 +77,41 @@ class Gitlab::Seeder::CycleAnalytics
end
def seed!
- Sidekiq::Testing.inline! do
- issues = create_issues
- puts '.'
-
- # Stage 1
- Timecop.travel 5.days.from_now
- add_milestones_and_list_labels(issues)
- print '.'
-
- # Stage 2
- Timecop.travel 5.days.from_now
- branches = mention_in_commits(issues)
- print '.'
-
- # Stage 3
- Timecop.travel 5.days.from_now
- merge_requests = create_merge_requests_closing_issues(issues, branches)
- print '.'
-
- # Stage 4
- Timecop.travel 5.days.from_now
- run_builds(merge_requests)
- print '.'
-
- # Stage 5
- Timecop.travel 5.days.from_now
- merge_merge_requests(merge_requests)
- print '.'
-
- # Stage 6 / 7
- Timecop.travel 5.days.from_now
- deploy_to_production(merge_requests)
- print '.'
+ Sidekiq::Worker.skipping_transaction_check do
+ Sidekiq::Testing.inline! do
+ issues = create_issues
+ puts '.'
+
+ # Stage 1
+ Timecop.travel 5.days.from_now
+ add_milestones_and_list_labels(issues)
+ print '.'
+
+ # Stage 2
+ Timecop.travel 5.days.from_now
+ branches = mention_in_commits(issues)
+ print '.'
+
+ # Stage 3
+ Timecop.travel 5.days.from_now
+ merge_requests = create_merge_requests_closing_issues(issues, branches)
+ print '.'
+
+ # Stage 4
+ Timecop.travel 5.days.from_now
+ run_builds(merge_requests)
+ print '.'
+
+ # Stage 5
+ Timecop.travel 5.days.from_now
+ merge_merge_requests(merge_requests)
+ print '.'
+
+ # Stage 6 / 7
+ Timecop.travel 5.days.from_now
+ deploy_to_production(merge_requests)
+ print '.'
+ end
end
print '.'
@@ -123,7 +125,7 @@ class Gitlab::Seeder::CycleAnalytics
title: "Cycle Analytics: #{FFaker::Lorem.sentence(6)}",
description: FFaker::Lorem.sentence,
state: 'opened',
- assignee: @project.team.users.sample
+ assignees: [@project.team.users.sample]
}
Issues::CreateService.new(@project, @project.team.users.sample, issue_params).execute
@@ -155,7 +157,7 @@ class Gitlab::Seeder::CycleAnalytics
issue.project.repository.add_branch(@user, branch_name, 'master')
- commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for ##{issue.iid}", branch_name: branch_name)
+ commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for #{issue.to_reference}", branch_name: branch_name)
issue.project.repository.commit(commit_sha)
GitPushService.new(issue.project,
@@ -210,6 +212,8 @@ class Gitlab::Seeder::CycleAnalytics
def deploy_to_production(merge_requests)
merge_requests.each do |merge_request|
+ next unless merge_request.head_pipeline
+
Timecop.travel 12.hours.from_now
job = merge_request.head_pipeline.builds.where.not(environment: nil).last
@@ -223,7 +227,14 @@ Gitlab::Seeder.quiet do
flag = 'SEED_CYCLE_ANALYTICS'
if ENV[flag]
- Project.all.each do |project|
+ Project.find_each do |project|
+ # This seed naively assumes that every project has a repository, and every
+ # repository has a `master` branch, which may be the case for a pristine
+ # GDK seed, but is almost never true for a GDK that's actually had
+ # development performed on it.
+ next unless project.repository_exists?
+ next unless project.repository.commit('master')
+
seeder = Gitlab::Seeder::CycleAnalytics.new(project)
seeder.seed!
end
diff --git a/db/migrate/20150827121444_add_fast_forward_option_to_project.rb b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb
index 6f22641077d..35df121519e 100644
--- a/db/migrate/20150827121444_add_fast_forward_option_to_project.rb
+++ b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb
@@ -8,7 +8,11 @@ class AddFastForwardOptionToProject < ActiveRecord::Migration
disable_ddl_transaction!
def up
- add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
+ # We put condition here because of a mistake we made a couple of years ago
+ # see https://gitlab.com/gitlab-org/gitlab-ce/issues/39382#note_45716103
+ unless column_exists?(:projects, :merge_requests_ff_only_enabled)
+ add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
+ end
end
def down
diff --git a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb
index 22bac46e25c..1716b6e8153 100644
--- a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb
+++ b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddOnlyAllowMergeIfBuildSucceedsToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb b/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb
index 915167b038d..8e9ab3f8acc 100644
--- a/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb
+++ b/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/Datetime
class AddArtifactsExpireDateToCiBuilds < ActiveRecord::Migration
def change
add_column :ci_builds, :artifacts_expire_at, :timestamp
diff --git a/db/migrate/20160608195742_add_repository_storage_to_projects.rb b/db/migrate/20160608195742_add_repository_storage_to_projects.rb
index 0f3664c13ef..e4febd1614d 100644
--- a/db/migrate/20160608195742_add_repository_storage_to_projects.rb
+++ b/db/migrate/20160608195742_add_repository_storage_to_projects.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddRepositoryStorageToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
index 5336b036bca..c58cb957df4 100644
--- a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
+++ b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# rubocop:disable Migration/UpdateColumnInBatches
class SetMissingStageOnCiBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb
index 5dc26f8982a..22c925799a3 100644
--- a/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb
+++ b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddRequestAccessEnabledToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb
index 4a317646788..4fcb29e1325 100644
--- a/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb
+++ b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddRequestAccessEnabledToGroups < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb
index 756910a1fa0..fd7a48d881e 100644
--- a/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb
+++ b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/Datetime
class AddQueuedAtToCiBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb
index abe8e701e23..58f7f2a2841 100644
--- a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb
+++ b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# rubocop:disable Migration/UpdateColumnInBatches
class DropAndReaddHasExternalWikiInProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
index 7414a28ac97..aec709aaf59 100644
--- a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
+++ b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb
@@ -1,7 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class RemoveFeaturesEnabledFromProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
index 0100e30a733..df7d922b816 100644
--- a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
+++ b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb
@@ -1,7 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class RemoveProjectsPushesSinceGc < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb
index ae37da275fd..27ebe0af33b 100644
--- a/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb
+++ b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddTwoFactorColumnsToNamespaces < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170124193205_add_two_factor_columns_to_users.rb b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb
index 8d4aefa4365..558a1837c79 100644
--- a/db/migrate/20170124193205_add_two_factor_columns_to_users.rb
+++ b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddTwoFactorColumnsToUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
index 7ad01a04815..6d43f346d4f 100644
--- a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
+++ b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
@@ -1,7 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddPrintingMergeRequestLinkEnabledToProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb b/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb
index f335e77fb5e..3c5cd95726a 100644
--- a/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb
+++ b/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class AddAutoCancelPendingPipelinesToProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
index 6c9fe19ca34..807dfcb385d 100644
--- a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
+++ b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
@@ -1,4 +1,4 @@
-# rubocop:disable Migration/AddColumnWithDefaultToLargeTable
+# rubocop:disable Migration/UpdateLargeTable
class RevertAddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20170320173259_migrate_assignees.rb b/db/migrate/20170320173259_migrate_assignees.rb
index 7b61e811317..255b5e9c4db 100644
--- a/db/migrate/20170320173259_migrate_assignees.rb
+++ b/db/migrate/20170320173259_migrate_assignees.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# rubocop:disable Migration/UpdateColumnInBatches
class MigrateAssignees < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb b/db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb
index 6ac10723c82..a5d1eca82bb 100644
--- a/db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb
+++ b/db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/Datetime
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb b/db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb
index 7a1acdcbf69..47ba6bde856 100644
--- a/db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb
+++ b/db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/Datetime
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/migrate/20170909150936_add_spent_at_to_timelogs.rb b/db/migrate/20170909150936_add_spent_at_to_timelogs.rb
new file mode 100644
index 00000000000..ffff719c289
--- /dev/null
+++ b/db/migrate/20170909150936_add_spent_at_to_timelogs.rb
@@ -0,0 +1,11 @@
+class AddSpentAtToTimelogs < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ add_column :timelogs, :spent_at, :datetime_with_timezone
+ end
+
+ def down
+ remove_column :timelogs, :spent_at
+ end
+end
diff --git a/db/migrate/20170918111708_create_project_custom_attributes.rb b/db/migrate/20170918111708_create_project_custom_attributes.rb
new file mode 100644
index 00000000000..b5bc90ec02e
--- /dev/null
+++ b/db/migrate/20170918111708_create_project_custom_attributes.rb
@@ -0,0 +1,15 @@
+class CreateProjectCustomAttributes < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :project_custom_attributes do |t|
+ t.timestamps_with_timezone null: false
+ t.references :project, null: false, foreign_key: { on_delete: :cascade }
+ t.string :key, null: false
+ t.string :value, null: false
+
+ t.index [:project_id, :key], unique: true
+ t.index [:key, :value]
+ end
+ end
+end
diff --git a/db/migrate/20170918140927_create_group_custom_attributes.rb b/db/migrate/20170918140927_create_group_custom_attributes.rb
new file mode 100644
index 00000000000..3879ea15eb6
--- /dev/null
+++ b/db/migrate/20170918140927_create_group_custom_attributes.rb
@@ -0,0 +1,19 @@
+class CreateGroupCustomAttributes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :group_custom_attributes do |t|
+ t.timestamps_with_timezone null: false
+ t.references :group, null: false
+ t.string :key, null: false
+ t.string :value, null: false
+
+ t.index [:group_id, :key], unique: true
+ t.index [:key, :value]
+ end
+
+ add_foreign_key :group_custom_attributes, :namespaces, column: :group_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
+ end
+end
diff --git a/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb b/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb
index b2009b282e9..8423bf13fd9 100644
--- a/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb
+++ b/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb
@@ -12,6 +12,7 @@ class RemoveTemporaryCiBuildsIndex < ActiveRecord::Migration
def up
return unless index_exists?(:ci_builds, :id, name: 'index_for_ci_builds_retried_migration')
+
remove_concurrent_index(:ci_builds, :id, name: "index_for_ci_builds_retried_migration")
end
diff --git a/db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb b/db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb
new file mode 100644
index 00000000000..55e822752af
--- /dev/null
+++ b/db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddGlobalRateLimitsToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :application_settings, :throttle_unauthenticated_enabled, :boolean, default: false, allow_null: false
+ add_column_with_default :application_settings, :throttle_unauthenticated_requests_per_period, :integer, default: 3600, allow_null: false
+ add_column_with_default :application_settings, :throttle_unauthenticated_period_in_seconds, :integer, default: 3600, allow_null: false
+
+ add_column_with_default :application_settings, :throttle_authenticated_api_enabled, :boolean, default: false, allow_null: false
+ add_column_with_default :application_settings, :throttle_authenticated_api_requests_per_period, :integer, default: 7200, allow_null: false
+ add_column_with_default :application_settings, :throttle_authenticated_api_period_in_seconds, :integer, default: 3600, allow_null: false
+
+ add_column_with_default :application_settings, :throttle_authenticated_web_enabled, :boolean, default: false, allow_null: false
+ add_column_with_default :application_settings, :throttle_authenticated_web_requests_per_period, :integer, default: 7200, allow_null: false
+ add_column_with_default :application_settings, :throttle_authenticated_web_period_in_seconds, :integer, default: 3600, allow_null: false
+ end
+
+ def down
+ remove_column :application_settings, :throttle_authenticated_web_period_in_seconds
+ remove_column :application_settings, :throttle_authenticated_web_requests_per_period
+ remove_column :application_settings, :throttle_authenticated_web_enabled
+
+ remove_column :application_settings, :throttle_authenticated_api_period_in_seconds
+ remove_column :application_settings, :throttle_authenticated_api_requests_per_period
+ remove_column :application_settings, :throttle_authenticated_api_enabled
+
+ remove_column :application_settings, :throttle_unauthenticated_period_in_seconds
+ remove_column :application_settings, :throttle_unauthenticated_requests_per_period
+ remove_column :application_settings, :throttle_unauthenticated_enabled
+ end
+end
diff --git a/db/migrate/20171012101043_add_circuit_breaker_properties_to_application_settings.rb b/db/migrate/20171012101043_add_circuit_breaker_properties_to_application_settings.rb
new file mode 100644
index 00000000000..bcf7dbd8e64
--- /dev/null
+++ b/db/migrate/20171012101043_add_circuit_breaker_properties_to_application_settings.rb
@@ -0,0 +1,27 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddCircuitBreakerPropertiesToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings,
+ :circuitbreaker_failure_count_threshold,
+ :integer,
+ default: 160
+ add_column :application_settings,
+ :circuitbreaker_failure_wait_time,
+ :integer,
+ default: 30
+ add_column :application_settings,
+ :circuitbreaker_failure_reset_time,
+ :integer,
+ default: 1800
+ add_column :application_settings,
+ :circuitbreaker_storage_timeout,
+ :integer,
+ default: 30
+ end
+end
diff --git a/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb b/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb
new file mode 100644
index 00000000000..9a909644a44
--- /dev/null
+++ b/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb
@@ -0,0 +1,78 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateUserAuthenticationTokenToPersonalAccessToken < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # disable_ddl_transaction!
+
+ TOKEN_NAME = 'Private Token'.freeze
+
+ def up
+ execute <<~SQL
+ INSERT INTO personal_access_tokens (user_id, token, name, created_at, updated_at, scopes)
+ SELECT id, authentication_token, '#{TOKEN_NAME}', NOW(), NOW(), '#{%w[api].to_yaml}'
+ FROM users
+ WHERE authentication_token IS NOT NULL
+ AND admin = FALSE
+ AND NOT EXISTS (
+ SELECT true
+ FROM personal_access_tokens
+ WHERE user_id = users.id
+ AND token = users.authentication_token
+ )
+ SQL
+
+ # Admins also need the `sudo` scope
+ execute <<~SQL
+ INSERT INTO personal_access_tokens (user_id, token, name, created_at, updated_at, scopes)
+ SELECT id, authentication_token, '#{TOKEN_NAME}', NOW(), NOW(), '#{%w[api sudo].to_yaml}'
+ FROM users
+ WHERE authentication_token IS NOT NULL
+ AND admin = TRUE
+ AND NOT EXISTS (
+ SELECT true
+ FROM personal_access_tokens
+ WHERE user_id = users.id
+ AND token = users.authentication_token
+ )
+ SQL
+ end
+
+ def down
+ if Gitlab::Database.postgresql?
+ execute <<~SQL
+ UPDATE users
+ SET authentication_token = pats.token
+ FROM (
+ SELECT user_id, token
+ FROM personal_access_tokens
+ WHERE name = '#{TOKEN_NAME}'
+ ) AS pats
+ WHERE id = pats.user_id
+ SQL
+ else
+ execute <<~SQL
+ UPDATE users
+ INNER JOIN personal_access_tokens AS pats
+ ON users.id = pats.user_id
+ SET authentication_token = pats.token
+ WHERE pats.name = '#{TOKEN_NAME}'
+ SQL
+ end
+
+ execute <<~SQL
+ DELETE FROM personal_access_tokens
+ WHERE name = '#{TOKEN_NAME}'
+ AND EXISTS (
+ SELECT true
+ FROM users
+ WHERE id = personal_access_tokens.user_id
+ AND authentication_token = personal_access_tokens.token
+ )
+ SQL
+ end
+end
diff --git a/db/migrate/20171013094327_create_new_clusters_architectures.rb b/db/migrate/20171013094327_create_new_clusters_architectures.rb
new file mode 100644
index 00000000000..dabb3e25e48
--- /dev/null
+++ b/db/migrate/20171013094327_create_new_clusters_architectures.rb
@@ -0,0 +1,68 @@
+class CreateNewClustersArchitectures < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :clusters do |t|
+ t.references :user, index: true, foreign_key: { on_delete: :nullify }
+
+ t.integer :provider_type
+ t.integer :platform_type
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.boolean :enabled, index: true, default: true
+
+ t.string :name, null: false # If manual, read-write. If gcp, read-only.
+ end
+
+ create_table :cluster_projects do |t|
+ t.references :project, null: false, index: true, foreign_key: { on_delete: :cascade }
+ t.references :cluster, null: false, index: true, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+ end
+
+ create_table :cluster_platforms_kubernetes do |t|
+ t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.text :api_url
+ t.text :ca_cert
+
+ t.string :namespace
+
+ t.string :username
+ t.text :encrypted_password
+ t.string :encrypted_password_iv
+
+ t.text :encrypted_token
+ t.string :encrypted_token_iv
+ end
+
+ create_table :cluster_providers_gcp do |t|
+ t.references :cluster, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+
+ t.integer :status
+ t.integer :num_nodes, null: false
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.text :status_reason
+
+ t.string :gcp_project_id, null: false
+ t.string :zone, null: false
+ t.string :machine_type
+ t.string :operation_id
+
+ t.string :endpoint
+
+ t.text :encrypted_access_token
+ t.string :encrypted_access_token_iv
+ end
+ end
+end
diff --git a/db/migrate/20171017145932_add_new_circuitbreaker_settings_to_application_settings.rb b/db/migrate/20171017145932_add_new_circuitbreaker_settings_to_application_settings.rb
new file mode 100644
index 00000000000..07eb25c0b0f
--- /dev/null
+++ b/db/migrate/20171017145932_add_new_circuitbreaker_settings_to_application_settings.rb
@@ -0,0 +1,16 @@
+class AddNewCircuitbreakerSettingsToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings,
+ :circuitbreaker_access_retries,
+ :integer,
+ default: 3
+ add_column :application_settings,
+ :circuitbreaker_backoff_threshold,
+ :integer,
+ default: 80
+ end
+end
diff --git a/db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb b/db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb
new file mode 100644
index 00000000000..74a2badc130
--- /dev/null
+++ b/db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb
@@ -0,0 +1,26 @@
+class AddLatestMergeRequestDiffIdToMergeRequests < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column :merge_requests, :latest_merge_request_diff_id, :integer
+ add_concurrent_index :merge_requests, :latest_merge_request_diff_id
+
+ add_concurrent_foreign_key :merge_requests, :merge_request_diffs,
+ column: :latest_merge_request_diff_id,
+ on_delete: :nullify
+ end
+
+ def down
+ remove_foreign_key :merge_requests, column: :latest_merge_request_diff_id
+
+ if index_exists?(:merge_requests, :latest_merge_request_diff_id)
+ remove_concurrent_index :merge_requests, :latest_merge_request_diff_id
+ end
+
+ remove_column :merge_requests, :latest_merge_request_diff_id
+ end
+end
diff --git a/db/migrate/20171031100710_create_clusters_kubernetes_helm_apps.rb b/db/migrate/20171031100710_create_clusters_kubernetes_helm_apps.rb
new file mode 100644
index 00000000000..a2ce37127ea
--- /dev/null
+++ b/db/migrate/20171031100710_create_clusters_kubernetes_helm_apps.rb
@@ -0,0 +1,18 @@
+class CreateClustersKubernetesHelmApps < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :clusters_applications_helm do |t|
+ t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.integer :status, null: false
+ t.string :version, null: false
+ t.text :status_reason
+ end
+ end
+end
diff --git a/db/migrate/20171101130535_add_gitaly_timeout_properties_to_application_settings.rb b/db/migrate/20171101130535_add_gitaly_timeout_properties_to_application_settings.rb
new file mode 100644
index 00000000000..de621e7111c
--- /dev/null
+++ b/db/migrate/20171101130535_add_gitaly_timeout_properties_to_application_settings.rb
@@ -0,0 +1,31 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddGitalyTimeoutPropertiesToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :application_settings,
+ :gitaly_timeout_default,
+ :integer,
+ default: 55
+ add_column_with_default :application_settings,
+ :gitaly_timeout_medium,
+ :integer,
+ default: 30
+ add_column_with_default :application_settings,
+ :gitaly_timeout_fast,
+ :integer,
+ default: 10
+ end
+
+ def down
+ remove_column :application_settings, :gitaly_timeout_default
+ remove_column :application_settings, :gitaly_timeout_medium
+ remove_column :application_settings, :gitaly_timeout_fast
+ end
+end
diff --git a/db/migrate/20171106101200_create_clusters_kubernetes_ingress_apps.rb b/db/migrate/20171106101200_create_clusters_kubernetes_ingress_apps.rb
new file mode 100644
index 00000000000..21f48b1d1b4
--- /dev/null
+++ b/db/migrate/20171106101200_create_clusters_kubernetes_ingress_apps.rb
@@ -0,0 +1,21 @@
+class CreateClustersKubernetesIngressApps < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :clusters_applications_ingress do |t|
+ t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.integer :status, null: false
+ t.integer :ingress_type, null: false
+
+ t.string :version, null: false
+ t.string :cluster_ip
+ t.text :status_reason
+ end
+ end
+end
diff --git a/db/migrate/20171106132212_issues_confidential_not_null.rb b/db/migrate/20171106132212_issues_confidential_not_null.rb
new file mode 100644
index 00000000000..c959d2dd938
--- /dev/null
+++ b/db/migrate/20171106132212_issues_confidential_not_null.rb
@@ -0,0 +1,23 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class IssuesConfidentialNotNull < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+ end
+
+ def up
+ Issue.where('confidential IS NULL').update_all(confidential: false)
+
+ change_column_null :issues, :confidential, false
+ end
+
+ def down
+ # There's no way / point to revert this.
+ end
+end
diff --git a/db/migrate/20171106133143_rename_application_settings_password_authentication_enabled_to_password_authentication_enabled_for_web.rb b/db/migrate/20171106133143_rename_application_settings_password_authentication_enabled_to_password_authentication_enabled_for_web.rb
new file mode 100644
index 00000000000..6d369e93361
--- /dev/null
+++ b/db/migrate/20171106133143_rename_application_settings_password_authentication_enabled_to_password_authentication_enabled_for_web.rb
@@ -0,0 +1,15 @@
+class RenameApplicationSettingsPasswordAuthenticationEnabledToPasswordAuthenticationEnabledForWeb < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ rename_column_concurrently :application_settings, :password_authentication_enabled, :password_authentication_enabled_for_web
+ end
+
+ def down
+ cleanup_concurrent_column_rename :application_settings, :password_authentication_enabled_for_web, :password_authentication_enabled
+ end
+end
diff --git a/db/migrate/20171106133911_add_password_authentication_enabled_for_git_to_application_settings.rb b/db/migrate/20171106133911_add_password_authentication_enabled_for_git_to_application_settings.rb
new file mode 100644
index 00000000000..b8aa600864e
--- /dev/null
+++ b/db/migrate/20171106133911_add_password_authentication_enabled_for_git_to_application_settings.rb
@@ -0,0 +1,9 @@
+class AddPasswordAuthenticationEnabledForGitToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :password_authentication_enabled_for_git, :boolean, default: true, null: false
+ end
+end
diff --git a/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb b/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb
new file mode 100644
index 00000000000..e6a780d0964
--- /dev/null
+++ b/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class IssuesMilestoneIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'issues'
+
+ def self.with_orphaned_milestones
+ where('NOT EXISTS (SELECT true FROM milestones WHERE milestones.id = issues.milestone_id)')
+ end
+ end
+
+ def up
+ Issue.with_orphaned_milestones.each_batch(of: 100) do |batch|
+ batch.update_all(milestone_id: nil)
+ end
+
+ add_concurrent_foreign_key(
+ :issues,
+ :milestones,
+ column: :milestone_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:issues, column: :milestone_id)
+ end
+end
diff --git a/db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb b/db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb
new file mode 100644
index 00000000000..3b8844d7d9f
--- /dev/null
+++ b/db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb
@@ -0,0 +1,45 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class IssuesUpdatedByIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'issues'
+
+ def self.with_orphaned_updaters
+ where('NOT EXISTS (SELECT true FROM users WHERE users.id = issues.updated_by_id)')
+ .where('updated_by_id IS NOT NULL')
+ end
+ end
+
+ def up
+ Issue.with_orphaned_updaters.each_batch(of: 100) do |batch|
+ batch.update_all(updated_by_id: nil)
+ end
+
+ # This index is only used for foreign keys, and those in turn will always
+ # specify a value. As such we can add a WHERE condition to make the index
+ # smaller.
+ add_concurrent_index(:issues, :updated_by_id, where: 'updated_by_id IS NOT NULL')
+
+ add_concurrent_foreign_key(
+ :issues,
+ :users,
+ column: :updated_by_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:issues, column: :updated_by_id)
+ remove_concurrent_index(:issues, :updated_by_id)
+ end
+end
diff --git a/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb b/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb
new file mode 100644
index 00000000000..8d2ceb8cc18
--- /dev/null
+++ b/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb
@@ -0,0 +1,44 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class IssuesMovedToIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'issues'
+
+ def self.with_orphaned_moved_to_issues
+ where('NOT EXISTS (SELECT true FROM issues WHERE issues.id = issues.moved_to_id)')
+ .where('moved_to_id IS NOT NULL')
+ end
+ end
+
+ def up
+ Issue.with_orphaned_moved_to_issues.each_batch(of: 100) do |batch|
+ batch.update_all(moved_to_id: nil)
+ end
+
+ add_concurrent_foreign_key(
+ :issues,
+ :issues,
+ column: :moved_to_id,
+ on_delete: :nullify
+ )
+
+ # We're using a partial index here so we only index the data we actually
+ # care about.
+ add_concurrent_index(:issues, :moved_to_id, where: 'moved_to_id IS NOT NULL')
+ end
+
+ def down
+ remove_foreign_key_without_error(:issues, column: :moved_to_id)
+ remove_concurrent_index(:issues, :moved_to_id)
+ end
+end
diff --git a/db/migrate/20171106154015_remove_issues_branch_name.rb b/db/migrate/20171106154015_remove_issues_branch_name.rb
new file mode 100644
index 00000000000..3d08225c96d
--- /dev/null
+++ b/db/migrate/20171106154015_remove_issues_branch_name.rb
@@ -0,0 +1,13 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveIssuesBranchName < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ remove_column :issues, :branch_name, :string
+ end
+end
diff --git a/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb b/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb
new file mode 100644
index 00000000000..e4bed778695
--- /dev/null
+++ b/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class TurnIssuesDueDateIndexToPartialIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ NEW_INDEX_NAME = 'idx_issues_on_project_id_and_due_date_and_id_and_state_partial'
+ OLD_INDEX_NAME = 'index_issues_on_project_id_and_due_date_and_id_and_state'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(
+ :issues,
+ [:project_id, :due_date, :id, :state],
+ where: 'due_date IS NOT NULL',
+ name: NEW_INDEX_NAME
+ )
+
+ # We set the column name to nil as otherwise Rails will ignore the custom
+ # index name and remove the wrong index.
+ remove_concurrent_index(:issues, nil, name: OLD_INDEX_NAME)
+ end
+
+ def down
+ add_concurrent_index(
+ :issues,
+ [:project_id, :due_date, :id, :state],
+ name: OLD_INDEX_NAME
+ )
+
+ remove_concurrent_index(:issues, nil, name: NEW_INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb b/db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb
new file mode 100644
index 00000000000..ad540b1e509
--- /dev/null
+++ b/db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddTimezoneToIssuesClosedAt < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ change_column_type_concurrently(:issues, :closed_at, :datetime_with_timezone)
+ end
+
+ def down
+ cleanup_concurrent_column_type_change(:issues, :closed_at)
+ end
+end
diff --git a/db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb b/db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb
new file mode 100644
index 00000000000..021eaa04a0c
--- /dev/null
+++ b/db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb
@@ -0,0 +1,43 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsAuthorIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+
+ def self.with_orphaned_authors
+ where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.author_id = users.id)')
+ .where('author_id IS NOT NULL')
+ end
+ end
+
+ def up
+ # Replacing the ghost user ID logic would be too complex, hence we don't
+ # redefine the User model here.
+ ghost_id = User.select(:id).ghost.id
+
+ MergeRequest.with_orphaned_authors.each_batch(of: 100) do |batch|
+ batch.update_all(author_id: ghost_id)
+ end
+
+ add_concurrent_foreign_key(
+ :merge_requests,
+ :users,
+ column: :author_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key(:merge_requests, column: :author_id)
+ end
+end
diff --git a/db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb b/db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb
new file mode 100644
index 00000000000..1a242f01051
--- /dev/null
+++ b/db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb
@@ -0,0 +1,39 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsAssigneeIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+
+ def self.with_orphaned_assignees
+ where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.assignee_id = users.id)')
+ .where('assignee_id IS NOT NULL')
+ end
+ end
+
+ def up
+ MergeRequest.with_orphaned_assignees.each_batch(of: 100) do |batch|
+ batch.update_all(assignee_id: nil)
+ end
+
+ add_concurrent_foreign_key(
+ :merge_requests,
+ :users,
+ column: :assignee_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key(:merge_requests, column: :assignee_id)
+ end
+end
diff --git a/db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.rb b/db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.rb
new file mode 100644
index 00000000000..eb3872e38da
--- /dev/null
+++ b/db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.rb
@@ -0,0 +1,46 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsUpdatedByIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+
+ def self.with_orphaned_updaters
+ where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.updated_by_id = users.id)')
+ .where('updated_by_id IS NOT NULL')
+ end
+ end
+
+ def up
+ MergeRequest.with_orphaned_updaters.each_batch(of: 100) do |batch|
+ batch.update_all(updated_by_id: nil)
+ end
+
+ add_concurrent_index(
+ :merge_requests,
+ :updated_by_id,
+ where: 'updated_by_id IS NOT NULL'
+ )
+
+ add_concurrent_foreign_key(
+ :merge_requests,
+ :users,
+ column: :updated_by_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:merge_requests, column: :updated_by_id)
+ remove_concurrent_index(:merge_requests, :updated_by_id)
+ end
+end
diff --git a/db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.rb b/db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.rb
new file mode 100644
index 00000000000..925b3e537d7
--- /dev/null
+++ b/db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.rb
@@ -0,0 +1,46 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsMergeUserIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+
+ def self.with_orphaned_mergers
+ where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.merge_user_id = users.id)')
+ .where('merge_user_id IS NOT NULL')
+ end
+ end
+
+ def up
+ MergeRequest.with_orphaned_mergers.each_batch(of: 100) do |batch|
+ batch.update_all(merge_user_id: nil)
+ end
+
+ add_concurrent_index(
+ :merge_requests,
+ :merge_user_id,
+ where: 'merge_user_id IS NOT NULL'
+ )
+
+ add_concurrent_foreign_key(
+ :merge_requests,
+ :users,
+ column: :merge_user_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:merge_requests, column: :merge_user_id)
+ remove_concurrent_index(:merge_requests, :merge_user_id)
+ end
+end
diff --git a/db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb b/db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb
new file mode 100644
index 00000000000..99740f64fe6
--- /dev/null
+++ b/db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb
@@ -0,0 +1,45 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsSourceProjectIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+
+ def self.with_orphaned_source_projects
+ where('NOT EXISTS (SELECT true FROM projects WHERE merge_requests.source_project_id = projects.id)')
+ .where('source_project_id IS NOT NULL')
+ end
+ end
+
+ def up
+ # We need to allow NULL values so we can nullify the column when the source
+ # project is removed. We _don't_ want to remove the merge request, instead
+ # the application will keep them but close them.
+ change_column_null(:merge_requests, :source_project_id, true)
+
+ MergeRequest.with_orphaned_source_projects.each_batch(of: 100) do |batch|
+ batch.update_all(source_project_id: nil)
+ end
+
+ add_concurrent_foreign_key(
+ :merge_requests,
+ :projects,
+ column: :source_project_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:merge_requests, column: :source_project_id)
+ change_column_null(:merge_requests, :source_project_id, false)
+ end
+end
diff --git a/db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb b/db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb
new file mode 100644
index 00000000000..c005cf7d173
--- /dev/null
+++ b/db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb
@@ -0,0 +1,39 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsMilestoneIdForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+
+ def self.with_orphaned_milestones
+ where('NOT EXISTS (SELECT true FROM milestones WHERE merge_requests.milestone_id = milestones.id)')
+ .where('milestone_id IS NOT NULL')
+ end
+ end
+
+ def up
+ MergeRequest.with_orphaned_milestones.each_batch(of: 100) do |batch|
+ batch.update_all(milestone_id: nil)
+ end
+
+ add_concurrent_foreign_key(
+ :merge_requests,
+ :milestones,
+ column: :milestone_id,
+ on_delete: :nullify
+ )
+ end
+
+ def down
+ remove_foreign_key_without_error(:merge_requests, column: :milestone_id)
+ end
+end
diff --git a/db/migrate/20171115164540_populate_merge_requests_latest_merge_request_diff_id_take_two.rb b/db/migrate/20171115164540_populate_merge_requests_latest_merge_request_diff_id_take_two.rb
new file mode 100644
index 00000000000..27b6b4ebddc
--- /dev/null
+++ b/db/migrate/20171115164540_populate_merge_requests_latest_merge_request_diff_id_take_two.rb
@@ -0,0 +1,30 @@
+# This is identical to the stolen background migration, which already has specs.
+class PopulateMergeRequestsLatestMergeRequestDiffIdTakeTwo < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 1_000
+
+ class MergeRequest < ActiveRecord::Base
+ self.table_name = 'merge_requests'
+
+ include ::EachBatch
+ end
+
+ disable_ddl_transaction!
+
+ def up
+ Gitlab::BackgroundMigration.steal('PopulateMergeRequestsLatestMergeRequestDiffId')
+
+ update = '
+ latest_merge_request_diff_id = (
+ SELECT MAX(id)
+ FROM merge_request_diffs
+ WHERE merge_requests.id = merge_request_diffs.merge_request_id
+ )'.squish
+
+ MergeRequest.where(latest_merge_request_diff_id: nil).each_batch(of: BATCH_SIZE) do |relation|
+ relation.update_all(update)
+ end
+ end
+end
diff --git a/db/migrate/20171116135628_add_environment_scope_to_clusters.rb b/db/migrate/20171116135628_add_environment_scope_to_clusters.rb
new file mode 100644
index 00000000000..cce757095dd
--- /dev/null
+++ b/db/migrate/20171116135628_add_environment_scope_to_clusters.rb
@@ -0,0 +1,15 @@
+class AddEnvironmentScopeToClusters < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:clusters, :environment_scope, :string, default: '*')
+ end
+
+ def down
+ remove_column(:clusters, :environment_scope)
+ end
+end
diff --git a/db/migrate/20171121135738_clean_up_from_merge_request_diffs_and_commits.rb b/db/migrate/20171121135738_clean_up_from_merge_request_diffs_and_commits.rb
new file mode 100644
index 00000000000..30cf08b29fc
--- /dev/null
+++ b/db/migrate/20171121135738_clean_up_from_merge_request_diffs_and_commits.rb
@@ -0,0 +1,36 @@
+class CleanUpFromMergeRequestDiffsAndCommits < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ class MergeRequestDiff < ActiveRecord::Base
+ self.table_name = 'merge_request_diffs'
+
+ include ::EachBatch
+ end
+
+ disable_ddl_transaction!
+
+ def up
+ Gitlab::BackgroundMigration.steal('DeserializeMergeRequestDiffsAndCommits')
+
+ # The literal '--- []\n' value is created by the import process and treated
+ # as null by the application, so we can ignore those - even if we were
+ # migrating, it wouldn't create any rows.
+ literal_prefix = Gitlab::Database.postgresql? ? 'E' : ''
+ non_empty = "
+ (st_commits IS NOT NULL AND st_commits != #{literal_prefix}'--- []\n')
+ OR
+ (st_diffs IS NOT NULL AND st_diffs != #{literal_prefix}'--- []\n')
+ ".squish
+
+ MergeRequestDiff.where(non_empty).each_batch(of: 500) do |relation, index|
+ range = relation.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits.new.perform(*range)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb b/db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb
new file mode 100644
index 00000000000..5a8ae6e4b57
--- /dev/null
+++ b/db/migrate/20171121144800_ci_pipelines_index_on_project_id_ref_status_id.rb
@@ -0,0 +1,35 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CiPipelinesIndexOnProjectIdRefStatusId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ TABLE = :ci_pipelines
+ OLD_COLUMNS = %i[project_id ref status].freeze
+ NEW_COLUMNS = %i[project_id ref status id].freeze
+
+ def up
+ unless index_exists?(TABLE, NEW_COLUMNS)
+ add_concurrent_index(TABLE, NEW_COLUMNS)
+ end
+
+ if index_exists?(TABLE, OLD_COLUMNS)
+ remove_concurrent_index(TABLE, OLD_COLUMNS)
+ end
+ end
+
+ def down
+ unless index_exists?(TABLE, OLD_COLUMNS)
+ add_concurrent_index(TABLE, OLD_COLUMNS)
+ end
+
+ if index_exists?(TABLE, NEW_COLUMNS)
+ remove_concurrent_index(TABLE, NEW_COLUMNS)
+ end
+ end
+end
diff --git a/db/migrate/20171124125042_add_default_values_to_merge_request_states.rb b/db/migrate/20171124125042_add_default_values_to_merge_request_states.rb
new file mode 100644
index 00000000000..d08863c3b78
--- /dev/null
+++ b/db/migrate/20171124125042_add_default_values_to_merge_request_states.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddDefaultValuesToMergeRequestStates < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ change_column_default :merge_requests, :state, :opened
+ change_column_default :merge_requests, :merge_status, :unchecked
+ end
+
+ def down
+ change_column_default :merge_requests, :state, nil
+ change_column_default :merge_requests, :merge_status, nil
+ end
+end
diff --git a/db/migrate/20171124125748_populate_missing_merge_request_statuses.rb b/db/migrate/20171124125748_populate_missing_merge_request_statuses.rb
new file mode 100644
index 00000000000..72fbab59f4c
--- /dev/null
+++ b/db/migrate/20171124125748_populate_missing_merge_request_statuses.rb
@@ -0,0 +1,50 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PopulateMissingMergeRequestStatuses < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+ end
+
+ def up
+ say 'Populating missing merge_requests.state values'
+
+ # GitLab.com has no rows where "state" is NULL, and technically this should
+ # never happen. However it doesn't hurt to be 100% certain.
+ MergeRequest.where(state: nil).each_batch do |batch|
+ batch.update_all(state: 'opened')
+ end
+
+ say 'Populating missing merge_requests.merge_status values. ' \
+ 'This will take a few minutes...'
+
+ # GitLab.com has 66 880 rows where "merge_status" is NULL, dating back all
+ # the way to 2011.
+ MergeRequest.where(merge_status: nil).each_batch(of: 10_000) do |batch|
+ batch.update_all(merge_status: 'unchecked')
+
+ # We want to give PostgreSQL some time to vacuum any dead tuples. In
+ # production we see it takes roughly 1 minute for a vacuuming run to clear
+ # out 10-20k dead tuples, so we'll wait for 90 seconds between every
+ # batch.
+ sleep(90) if sleep?
+ end
+ end
+
+ def down
+ # Reverting this makes no sense.
+ end
+
+ def sleep?
+ Rails.env.staging? || Rails.env.production?
+ end
+end
diff --git a/db/migrate/20171124132536_make_merge_request_statuses_not_null.rb b/db/migrate/20171124132536_make_merge_request_statuses_not_null.rb
new file mode 100644
index 00000000000..4bb09126036
--- /dev/null
+++ b/db/migrate/20171124132536_make_merge_request_statuses_not_null.rb
@@ -0,0 +1,14 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MakeMergeRequestStatusesNotNull < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ change_column_null :merge_requests, :state, false
+ change_column_null :merge_requests, :merge_status, false
+ end
+end
diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb
index 5cd9f3198e3..8f8d8f27410 100644
--- a/db/migrate/limits_to_mysql.rb
+++ b/db/migrate/limits_to_mysql.rb
@@ -3,8 +3,19 @@ class LimitsToMysql < ActiveRecord::Migration
def up
return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/
- change_column :merge_request_diffs, :st_commits, :text, limit: 2147483647
- change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
+ # These columns were removed in 10.3, but this is called from two places:
+ # 1. A migration run after they were added, but before they were removed.
+ # 2. A rake task which can be run at any time.
+ #
+ # Because of item 2, we need these checks.
+ if column_exists?(:merge_request_diffs, :st_commits)
+ change_column :merge_request_diffs, :st_commits, :text, limit: 2147483647
+ end
+
+ if column_exists?(:merge_request_diffs, :st_diffs)
+ change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
+ end
+
change_column :snippets, :content, :text, limit: 2147483647
change_column :notes, :st_diff, :text, limit: 2147483647
end
diff --git a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
index 82f8147547e..f1f81691f81 100644
--- a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
+++ b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# rubocop:disable Migration/UpdateColumnInBatches
class ResetUsersAuthorizedProjectsPopulated < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
index 01fff680183..49fd46b0262 100644
--- a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
+++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# rubocop:disable Migration/UpdateColumnInBatches
class ResetRelativePositionForIssue < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb
index cb1b4f1855d..78413a608f1 100644
--- a/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb
+++ b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
class MigrateUserActivitiesToUsersLastActivityOn < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170406111121_clean_upload_symlinks.rb b/db/post_migrate/20170406111121_clean_upload_symlinks.rb
index f2ce25d4524..0ab3d61730d 100644
--- a/db/post_migrate/20170406111121_clean_upload_symlinks.rb
+++ b/db/post_migrate/20170406111121_clean_upload_symlinks.rb
@@ -14,6 +14,7 @@ class CleanUploadSymlinks < ActiveRecord::Migration
DIRECTORIES_TO_MOVE.each do |dir|
symlink_location = File.join(old_upload_dir, dir)
next unless File.symlink?(symlink_location)
+
say "removing symlink: #{symlink_location}"
FileUtils.rm(symlink_location)
end
diff --git a/db/post_migrate/20170406142253_migrate_user_project_view.rb b/db/post_migrate/20170406142253_migrate_user_project_view.rb
index c4e910b3b44..d6061dd416d 100644
--- a/db/post_migrate/20170406142253_migrate_user_project_view.rb
+++ b/db/post_migrate/20170406142253_migrate_user_project_view.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb
index 765daa0a347..bba37e32c01 100644
--- a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb
+++ b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# rubocop:disable Migration/UpdateColumnInBatches
class EnableAutoCancelPendingPipelinesForAll < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb
index 9d9f36550e7..b0b58ab3011 100644
--- a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb
+++ b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
class UpdateRetriedForCiBuild < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb
index f77078ddd70..81e9d050668 100644
--- a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb
+++ b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
class AddHeadPipelineForEachMergeRequest < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb b/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb
index c78beda9d21..3e952980866 100644
--- a/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb
+++ b/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
diff --git a/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb b/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb
index 9441b236c8d..2125cc046e5 100644
--- a/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb
+++ b/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb
@@ -13,7 +13,6 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration
.well-known
abuse_reports
admin
- all
api
assets
autocomplete
@@ -24,29 +23,20 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration
groups
health_check
help
- hooks
import
invites
- issues
jwt
koding
- member
- merge_requests
- new
- notes
notification_settings
oauth
profile
projects
public
- repository
robots.txt
s
search
sent_notifications
- services
snippets
- teams
u
unicorn_test
unsubscribes
@@ -94,7 +84,6 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration
notification_setting
pipeline_quota
projects
- subgroups
].freeze
def up
diff --git a/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb
index 97cb242415d..31a73bb3b27 100644
--- a/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb
+++ b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
class MigrateBuildStageReferenceAgain < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170612071012_move_personal_snippets_files.rb b/db/post_migrate/20170612071012_move_personal_snippets_files.rb
index 2b79a87ccd8..c735dc67f44 100644
--- a/db/post_migrate/20170612071012_move_personal_snippets_files.rb
+++ b/db/post_migrate/20170612071012_move_personal_snippets_files.rb
@@ -32,6 +32,7 @@ class MovePersonalSnippetsFiles < ActiveRecord::Migration
file_name = upload['path'].split('/')[1]
next unless move_file(upload['model_id'], secret, file_name)
+
update_markdown(upload['model_id'], secret, file_name, upload['description'])
end
end
diff --git a/db/post_migrate/20170613111224_clean_appearance_symlinks.rb b/db/post_migrate/20170613111224_clean_appearance_symlinks.rb
index acb895e426f..17849b78ceb 100644
--- a/db/post_migrate/20170613111224_clean_appearance_symlinks.rb
+++ b/db/post_migrate/20170613111224_clean_appearance_symlinks.rb
@@ -13,6 +13,7 @@ class CleanAppearanceSymlinks < ActiveRecord::Migration
symlink_location = File.join(old_upload_dir, dir)
return unless File.symlink?(symlink_location)
+
say "removing symlink: #{symlink_location}"
FileUtils.rm(symlink_location)
end
diff --git a/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb b/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb
index a238216253b..b040c81b316 100644
--- a/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb
+++ b/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
class UpdateLegacyDiffNotesTypeForImport < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20170927112319_update_notes_type_for_import.rb b/db/post_migrate/20170927112319_update_notes_type_for_import.rb
index 1e70acd9868..5a400c71b02 100644
--- a/db/post_migrate/20170927112319_update_notes_type_for_import.rb
+++ b/db/post_migrate/20170927112319_update_notes_type_for_import.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/UpdateLargeTable
class UpdateNotesTypeForImport < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/post_migrate/20171012150314_remove_user_authentication_token.rb b/db/post_migrate/20171012150314_remove_user_authentication_token.rb
new file mode 100644
index 00000000000..d0f3aa06e98
--- /dev/null
+++ b/db/post_migrate/20171012150314_remove_user_authentication_token.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUserAuthenticationToken < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ remove_column :users, :authentication_token
+ end
+
+ def down
+ add_column :users, :authentication_token, :string
+
+ add_concurrent_index :users, :authentication_token, unique: true
+ end
+end
diff --git a/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb b/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb
new file mode 100644
index 00000000000..28cd0f70cc2
--- /dev/null
+++ b/db/post_migrate/20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb
@@ -0,0 +1,98 @@
+class MigrateGcpClustersToNewClustersArchitectures < ActiveRecord::Migration
+ DOWNTIME = false
+
+ class GcpCluster < ActiveRecord::Base
+ self.table_name = 'gcp_clusters'
+
+ belongs_to :project, class_name: 'Project'
+
+ include EachBatch
+ end
+
+ class Cluster < ActiveRecord::Base
+ self.table_name = 'clusters'
+
+ has_many :cluster_projects, class_name: 'ClustersProject'
+ has_many :projects, through: :cluster_projects, class_name: 'Project'
+ has_one :provider_gcp, class_name: 'ProvidersGcp'
+ has_one :platform_kubernetes, class_name: 'PlatformsKubernetes'
+
+ accepts_nested_attributes_for :provider_gcp
+ accepts_nested_attributes_for :platform_kubernetes
+
+ enum platform_type: {
+ kubernetes: 1
+ }
+
+ enum provider_type: {
+ user: 0,
+ gcp: 1
+ }
+ end
+
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ has_one :cluster_project, class_name: 'ClustersProject'
+ has_one :cluster, through: :cluster_project, class_name: 'Cluster'
+ end
+
+ class ClustersProject < ActiveRecord::Base
+ self.table_name = 'cluster_projects'
+
+ belongs_to :cluster, class_name: 'Cluster'
+ belongs_to :project, class_name: 'Project'
+ end
+
+ class ProvidersGcp < ActiveRecord::Base
+ self.table_name = 'cluster_providers_gcp'
+ end
+
+ class PlatformsKubernetes < ActiveRecord::Base
+ self.table_name = 'cluster_platforms_kubernetes'
+ end
+
+ def up
+ GcpCluster.all.find_each(batch_size: 1) do |gcp_cluster|
+ Cluster.create(
+ enabled: gcp_cluster.enabled,
+ user_id: gcp_cluster.user_id,
+ name: gcp_cluster.gcp_cluster_name,
+ provider_type: Cluster.provider_types[:gcp],
+ platform_type: Cluster.platform_types[:kubernetes],
+ projects: [gcp_cluster.project],
+ provider_gcp_attributes: {
+ status: gcp_cluster.status,
+ status_reason: gcp_cluster.status_reason,
+ gcp_project_id: gcp_cluster.gcp_project_id,
+ zone: gcp_cluster.gcp_cluster_zone,
+ num_nodes: gcp_cluster.gcp_cluster_size,
+ machine_type: gcp_cluster.gcp_machine_type,
+ operation_id: gcp_cluster.gcp_operation_id,
+ endpoint: gcp_cluster.endpoint,
+ encrypted_access_token: gcp_cluster.encrypted_gcp_token,
+ encrypted_access_token_iv: gcp_cluster.encrypted_gcp_token_iv
+ },
+ platform_kubernetes_attributes: {
+ api_url: api_url(gcp_cluster.endpoint),
+ ca_cert: gcp_cluster.ca_cert,
+ namespace: gcp_cluster.project_namespace,
+ username: gcp_cluster.username,
+ encrypted_password: gcp_cluster.encrypted_password,
+ encrypted_password_iv: gcp_cluster.encrypted_password_iv,
+ encrypted_token: gcp_cluster.encrypted_kubernetes_token,
+ encrypted_token_iv: gcp_cluster.encrypted_kubernetes_token_iv
+ } )
+ end
+ end
+
+ def down
+ execute('DELETE FROM clusters')
+ end
+
+ private
+
+ def api_url(endpoint)
+ endpoint ? 'https://' + endpoint : nil
+ end
+end
diff --git a/db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb b/db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb
new file mode 100644
index 00000000000..7a63382cc6d
--- /dev/null
+++ b/db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb
@@ -0,0 +1,29 @@
+class ScheduleMergeRequestLatestMergeRequestDiffIdMigrations < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 50_000
+ MIGRATION = 'PopulateMergeRequestsLatestMergeRequestDiffId'
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ self.table_name = 'merge_requests'
+
+ include ::EachBatch
+ end
+
+ # On GitLab.com, we saw that we generated about 500,000 dead tuples over 5 minutes.
+ # To keep replication lag from ballooning, we'll aim for 50,000 updates over 5 minutes.
+ #
+ # Assuming that there are 5 million rows affected (which is more than on
+ # GitLab.com), and that each batch of 50,000 rows takes up to 5 minutes, then
+ # we can migrate all the rows in 8.5 hours.
+ def up
+ MergeRequest.where(latest_merge_request_diff_id: nil).each_batch(of: BATCH_SIZE) do |relation, index|
+ range = relation.pluck('MIN(id)', 'MAX(id)').first
+
+ BackgroundMigrationWorker.perform_in(index * 5.minutes, MIGRATION, range)
+ end
+ end
+end
diff --git a/db/post_migrate/20171101134435_remove_ref_fetched_from_merge_requests.rb b/db/post_migrate/20171101134435_remove_ref_fetched_from_merge_requests.rb
new file mode 100644
index 00000000000..4e8f495d65d
--- /dev/null
+++ b/db/post_migrate/20171101134435_remove_ref_fetched_from_merge_requests.rb
@@ -0,0 +1,14 @@
+class RemoveRefFetchedFromMergeRequests < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # We don't need to cache this anymore: the refs are now created
+ # upon save/update and there is no more use for this flag
+ #
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/36061
+ def change
+ remove_column :merge_requests, :ref_fetched, :boolean
+ end
+end
diff --git a/db/post_migrate/20171106133144_cleanup_application_settings_password_authentication_enabled_rename.rb b/db/post_migrate/20171106133144_cleanup_application_settings_password_authentication_enabled_rename.rb
new file mode 100644
index 00000000000..d54ff3d5f5e
--- /dev/null
+++ b/db/post_migrate/20171106133144_cleanup_application_settings_password_authentication_enabled_rename.rb
@@ -0,0 +1,15 @@
+class CleanupApplicationSettingsPasswordAuthenticationEnabledRename < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_rename :application_settings, :password_authentication_enabled, :password_authentication_enabled_for_web
+ end
+
+ def down
+ rename_column_concurrently :application_settings, :password_authentication_enabled_for_web, :password_authentication_enabled
+ end
+end
diff --git a/db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb b/db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb
new file mode 100644
index 00000000000..88dd8f89ba6
--- /dev/null
+++ b/db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CleanupAddTimezoneToIssuesClosedAt < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_type_change(:issues, :closed_at)
+ end
+
+ # rubocop:disable Migration/Datetime
+ def down
+ change_column_type_concurrently(:issues, :closed_at, :datetime)
+ end
+end
diff --git a/db/post_migrate/20171114104051_remove_empty_fork_networks.rb b/db/post_migrate/20171114104051_remove_empty_fork_networks.rb
new file mode 100644
index 00000000000..2fe99a1b9c1
--- /dev/null
+++ b/db/post_migrate/20171114104051_remove_empty_fork_networks.rb
@@ -0,0 +1,36 @@
+class RemoveEmptyForkNetworks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 10_000
+
+ class MigrationForkNetwork < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'fork_networks'
+ end
+
+ class MigrationForkNetworkMembers < ActiveRecord::Base
+ self.table_name = 'fork_network_members'
+ end
+
+ disable_ddl_transaction!
+
+ def up
+ say 'Deleting empty ForkNetworks in batches'
+
+ has_members = MigrationForkNetworkMembers
+ .where('fork_network_members.fork_network_id = fork_networks.id')
+ .select(1)
+ MigrationForkNetwork.where('NOT EXISTS (?)', has_members)
+ .each_batch(of: BATCH_SIZE) do |networks|
+ deleted = networks.delete_all
+
+ say "Deleted #{deleted} rows in batch"
+ end
+ end
+
+ def down
+ # nothing
+ end
+end
diff --git a/db/post_migrate/20171121160421_remove_merge_request_diff_st_commits_and_st_diffs.rb b/db/post_migrate/20171121160421_remove_merge_request_diff_st_commits_and_st_diffs.rb
new file mode 100644
index 00000000000..3a7b2a7fac0
--- /dev/null
+++ b/db/post_migrate/20171121160421_remove_merge_request_diff_st_commits_and_st_diffs.rb
@@ -0,0 +1,10 @@
+class RemoveMergeRequestDiffStCommitsAndStDiffs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ remove_column :merge_request_diffs, :st_commits, :text
+ remove_column :merge_request_diffs, :st_diffs, :text
+ end
+end
diff --git a/db/post_migrate/20171124095655_add_index_on_merge_request_diffs_merge_request_id_and_id.rb b/db/post_migrate/20171124095655_add_index_on_merge_request_diffs_merge_request_id_and_id.rb
new file mode 100644
index 00000000000..698df712c11
--- /dev/null
+++ b/db/post_migrate/20171124095655_add_index_on_merge_request_diffs_merge_request_id_and_id.rb
@@ -0,0 +1,17 @@
+class AddIndexOnMergeRequestDiffsMergeRequestIdAndId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:merge_request_diffs, [:merge_request_id, :id])
+ end
+
+ def down
+ if index_exists?(:merge_request_diffs, [:merge_request_id, :id])
+ remove_concurrent_index(:merge_request_diffs, [:merge_request_id, :id])
+ end
+ end
+end
diff --git a/db/post_migrate/20171124100152_remove_index_on_merge_request_diffs_merge_request_diff_id.rb b/db/post_migrate/20171124100152_remove_index_on_merge_request_diffs_merge_request_diff_id.rb
new file mode 100644
index 00000000000..038e4807000
--- /dev/null
+++ b/db/post_migrate/20171124100152_remove_index_on_merge_request_diffs_merge_request_diff_id.rb
@@ -0,0 +1,17 @@
+class RemoveIndexOnMergeRequestDiffsMergeRequestDiffId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ if index_exists?(:merge_request_diffs, :merge_request_id)
+ remove_concurrent_index(:merge_request_diffs, :merge_request_id)
+ end
+ end
+
+ def down
+ add_concurrent_index(:merge_request_diffs, :merge_request_id)
+ end
+end
diff --git a/db/post_migrate/20171124150326_reschedule_fork_network_creation.rb b/db/post_migrate/20171124150326_reschedule_fork_network_creation.rb
new file mode 100644
index 00000000000..05430efe1f6
--- /dev/null
+++ b/db/post_migrate/20171124150326_reschedule_fork_network_creation.rb
@@ -0,0 +1,27 @@
+class RescheduleForkNetworkCreation < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ MIGRATION = 'PopulateForkNetworksRange'.freeze
+ BATCH_SIZE = 100
+ DELAY_INTERVAL = 15.seconds
+
+ disable_ddl_transaction!
+
+ class ForkedProjectLink < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'forked_project_links'
+ end
+
+ def up
+ say 'Populating the `fork_networks` based on existing `forked_project_links`'
+
+ queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # nothing
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index aac37b6b455..effb2604af2 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: 20171006091000) do
+ActiveRecord::Schema.define(version: 20171124150326) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -129,11 +129,30 @@ ActiveRecord::Schema.define(version: 20171006091000) do
t.boolean "prometheus_metrics_enabled", default: false, null: false
t.boolean "help_page_hide_commercial_content", default: false
t.string "help_page_support_url"
- t.boolean "password_authentication_enabled"
t.integer "performance_bar_allowed_group_id"
t.boolean "hashed_storage_enabled", default: false, null: false
t.boolean "project_export_enabled", default: true, null: false
t.boolean "auto_devops_enabled", default: false, null: false
+ t.integer "circuitbreaker_failure_count_threshold", default: 160
+ t.integer "circuitbreaker_failure_wait_time", default: 30
+ t.integer "circuitbreaker_failure_reset_time", default: 1800
+ t.integer "circuitbreaker_storage_timeout", default: 30
+ t.integer "circuitbreaker_access_retries", default: 3
+ t.integer "circuitbreaker_backoff_threshold", default: 80
+ t.boolean "throttle_unauthenticated_enabled", default: false, null: false
+ t.integer "throttle_unauthenticated_requests_per_period", default: 3600, null: false
+ t.integer "throttle_unauthenticated_period_in_seconds", default: 3600, null: false
+ t.boolean "throttle_authenticated_api_enabled", default: false, null: false
+ t.integer "throttle_authenticated_api_requests_per_period", default: 7200, null: false
+ t.integer "throttle_authenticated_api_period_in_seconds", default: 3600, null: false
+ t.boolean "throttle_authenticated_web_enabled", default: false, null: false
+ t.integer "throttle_authenticated_web_requests_per_period", default: 7200, null: false
+ t.integer "throttle_authenticated_web_period_in_seconds", default: 3600, null: false
+ t.boolean "password_authentication_enabled_for_web"
+ t.boolean "password_authentication_enabled_for_git", default: true
+ t.integer "gitaly_timeout_default", default: 55, null: false
+ t.integer "gitaly_timeout_medium", default: 30, null: false
+ t.integer "gitaly_timeout_fast", default: 10, null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -367,7 +386,7 @@ ActiveRecord::Schema.define(version: 20171006091000) do
add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
add_index "ci_pipelines", ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree
- add_index "ci_pipelines", ["project_id", "ref", "status"], name: "index_ci_pipelines_on_project_id_and_ref_and_status", using: :btree
+ add_index "ci_pipelines", ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree
add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree
add_index "ci_pipelines", ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree
add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree
@@ -456,6 +475,84 @@ ActiveRecord::Schema.define(version: 20171006091000) do
add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree
+ create_table "cluster_platforms_kubernetes", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.text "api_url"
+ t.text "ca_cert"
+ t.string "namespace"
+ t.string "username"
+ t.text "encrypted_password"
+ t.string "encrypted_password_iv"
+ t.text "encrypted_token"
+ t.string "encrypted_token_iv"
+ end
+
+ add_index "cluster_platforms_kubernetes", ["cluster_id"], name: "index_cluster_platforms_kubernetes_on_cluster_id", unique: true, using: :btree
+
+ create_table "cluster_projects", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ end
+
+ add_index "cluster_projects", ["cluster_id"], name: "index_cluster_projects_on_cluster_id", using: :btree
+ add_index "cluster_projects", ["project_id"], name: "index_cluster_projects_on_project_id", using: :btree
+
+ create_table "cluster_providers_gcp", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.integer "status"
+ t.integer "num_nodes", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.text "status_reason"
+ t.string "gcp_project_id", null: false
+ t.string "zone", null: false
+ t.string "machine_type"
+ t.string "operation_id"
+ t.string "endpoint"
+ t.text "encrypted_access_token"
+ t.string "encrypted_access_token_iv"
+ end
+
+ add_index "cluster_providers_gcp", ["cluster_id"], name: "index_cluster_providers_gcp_on_cluster_id", unique: true, using: :btree
+
+ create_table "clusters", force: :cascade do |t|
+ t.integer "user_id"
+ t.integer "provider_type"
+ t.integer "platform_type"
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.boolean "enabled", default: true
+ t.string "name", null: false
+ t.string "environment_scope", default: "*", null: false
+ end
+
+ add_index "clusters", ["enabled"], name: "index_clusters_on_enabled", using: :btree
+ add_index "clusters", ["user_id"], name: "index_clusters_on_user_id", using: :btree
+
+ create_table "clusters_applications_helm", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "status", null: false
+ t.string "version", null: false
+ t.text "status_reason"
+ end
+
+ create_table "clusters_applications_ingress", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "status", null: false
+ t.integer "ingress_type", null: false
+ t.string "version", null: false
+ t.string "cluster_ip"
+ t.text "status_reason"
+ end
+
create_table "container_repositories", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
@@ -687,6 +784,17 @@ ActiveRecord::Schema.define(version: 20171006091000) do
add_index "gpg_signatures", ["gpg_key_subkey_id"], name: "index_gpg_signatures_on_gpg_key_subkey_id", using: :btree
add_index "gpg_signatures", ["project_id"], name: "index_gpg_signatures_on_project_id", using: :btree
+ create_table "group_custom_attributes", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.integer "group_id", null: false
+ t.string "key", null: false
+ t.string "value", null: false
+ end
+
+ add_index "group_custom_attributes", ["group_id", "key"], name: "index_group_custom_attributes_on_group_id_and_key", unique: true, using: :btree
+ add_index "group_custom_attributes", ["key", "value"], name: "index_group_custom_attributes_on_key_and_value", using: :btree
+
create_table "identities", force: :cascade do |t|
t.string "extern_uid"
t.string "provider"
@@ -723,13 +831,12 @@ ActiveRecord::Schema.define(version: 20171006091000) do
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "branch_name"
t.text "description"
t.integer "milestone_id"
t.string "state"
t.integer "iid"
t.integer "updated_by_id"
- t.boolean "confidential", default: false
+ t.boolean "confidential", default: false, null: false
t.datetime "deleted_at"
t.date "due_date"
t.integer "moved_to_id"
@@ -738,11 +845,11 @@ ActiveRecord::Schema.define(version: 20171006091000) do
t.text "description_html"
t.integer "time_estimate"
t.integer "relative_position"
- t.datetime "closed_at"
t.integer "cached_markdown_version"
t.datetime "last_edited_at"
t.integer "last_edited_by_id"
t.boolean "discussion_locked"
+ t.datetime_with_timezone "closed_at"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -751,13 +858,15 @@ ActiveRecord::Schema.define(version: 20171006091000) do
add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
+ add_index "issues", ["moved_to_id"], name: "index_issues_on_moved_to_id", where: "(moved_to_id IS NOT NULL)", using: :btree
add_index "issues", ["project_id", "created_at", "id", "state"], name: "index_issues_on_project_id_and_created_at_and_id_and_state", using: :btree
- add_index "issues", ["project_id", "due_date", "id", "state"], name: "index_issues_on_project_id_and_due_date_and_id_and_state", using: :btree
+ add_index "issues", ["project_id", "due_date", "id", "state"], name: "idx_issues_on_project_id_and_due_date_and_id_and_state_partial", where: "(due_date IS NOT NULL)", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
add_index "issues", ["project_id", "updated_at", "id", "state"], name: "index_issues_on_project_id_and_updated_at_and_id_and_state", using: :btree
add_index "issues", ["relative_position"], name: "index_issues_on_relative_position", using: :btree
add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
+ add_index "issues", ["updated_by_id"], name: "index_issues_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
create_table "keys", force: :cascade do |t|
t.integer "user_id"
@@ -905,8 +1014,6 @@ ActiveRecord::Schema.define(version: 20171006091000) do
create_table "merge_request_diffs", force: :cascade do |t|
t.string "state"
- t.text "st_commits"
- t.text "st_diffs"
t.integer "merge_request_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
@@ -916,7 +1023,7 @@ ActiveRecord::Schema.define(version: 20171006091000) do
t.string "start_commit_sha"
end
- add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", using: :btree
+ add_index "merge_request_diffs", ["merge_request_id", "id"], name: "index_merge_request_diffs_on_merge_request_id_and_id", using: :btree
create_table "merge_request_metrics", force: :cascade do |t|
t.integer "merge_request_id", null: false
@@ -936,15 +1043,15 @@ ActiveRecord::Schema.define(version: 20171006091000) do
create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false
t.string "source_branch", null: false
- t.integer "source_project_id", null: false
+ t.integer "source_project_id"
t.integer "author_id"
t.integer "assignee_id"
t.string "title"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "milestone_id"
- t.string "state"
- t.string "merge_status"
+ t.string "state", default: "opened", null: false
+ t.string "merge_status", default: "unchecked", null: false
t.integer "target_project_id", null: false
t.integer "iid"
t.text "description"
@@ -964,9 +1071,9 @@ ActiveRecord::Schema.define(version: 20171006091000) do
t.datetime "last_edited_at"
t.integer "last_edited_by_id"
t.integer "head_pipeline_id"
- t.boolean "ref_fetched"
t.string "merge_jid"
t.boolean "discussion_locked"
+ t.integer "latest_merge_request_diff_id"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -975,6 +1082,8 @@ ActiveRecord::Schema.define(version: 20171006091000) do
add_index "merge_requests", ["deleted_at"], name: "index_merge_requests_on_deleted_at", using: :btree
add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree
+ add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree
+ add_index "merge_requests", ["merge_user_id"], name: "index_merge_requests_on_merge_user_id", where: "(merge_user_id IS NOT NULL)", using: :btree
add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
@@ -983,6 +1092,7 @@ ActiveRecord::Schema.define(version: 20171006091000) do
add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
+ add_index "merge_requests", ["updated_by_id"], name: "index_merge_requests_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
create_table "merge_requests_closing_issues", force: :cascade do |t|
t.integer "merge_request_id", null: false
@@ -1206,6 +1316,17 @@ ActiveRecord::Schema.define(version: 20171006091000) do
add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
+ create_table "project_custom_attributes", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.integer "project_id", null: false
+ t.string "key", null: false
+ t.string "value", null: false
+ end
+
+ add_index "project_custom_attributes", ["key", "value"], name: "index_project_custom_attributes_on_key_and_value", using: :btree
+ add_index "project_custom_attributes", ["project_id", "key"], name: "index_project_custom_attributes_on_project_id_and_key", unique: true, using: :btree
+
create_table "project_features", force: :cascade do |t|
t.integer "project_id"
t.integer "merge_requests_access_level"
@@ -1550,6 +1671,7 @@ ActiveRecord::Schema.define(version: 20171006091000) do
t.datetime "updated_at", null: false
t.integer "issue_id"
t.integer "merge_request_id"
+ t.datetime_with_timezone "spent_at"
end
add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree
@@ -1663,7 +1785,6 @@ ActiveRecord::Schema.define(version: 20171006091000) do
t.string "skype", default: "", null: false
t.string "linkedin", default: "", null: false
t.string "twitter", default: "", null: false
- t.string "authentication_token"
t.string "bio"
t.integer "failed_attempts", default: 0
t.datetime "locked_at"
@@ -1713,7 +1834,6 @@ ActiveRecord::Schema.define(version: 20171006091000) do
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
- add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
@@ -1803,6 +1923,12 @@ ActiveRecord::Schema.define(version: 20171006091000) do
add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade
+ add_foreign_key "cluster_platforms_kubernetes", "clusters", on_delete: :cascade
+ add_foreign_key "cluster_projects", "clusters", on_delete: :cascade
+ add_foreign_key "cluster_projects", "projects", on_delete: :cascade
+ add_foreign_key "cluster_providers_gcp", "clusters", on_delete: :cascade
+ add_foreign_key "clusters", "users", on_delete: :nullify
+ add_foreign_key "clusters_applications_helm", "clusters", on_delete: :cascade
add_foreign_key "container_repositories", "projects"
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
@@ -1822,11 +1948,15 @@ ActiveRecord::Schema.define(version: 20171006091000) do
add_foreign_key "gpg_signatures", "gpg_key_subkeys", on_delete: :nullify
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
+ add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
+ add_foreign_key "issues", "issues", column: "moved_to_id", name: "fk_a194299be1", on_delete: :nullify
+ add_foreign_key "issues", "milestones", name: "fk_96b1dd429c", on_delete: :nullify
add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade
add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify
+ add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
@@ -1839,7 +1969,14 @@ ActiveRecord::Schema.define(version: 20171006091000) do
add_foreign_key "merge_request_metrics", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify
+ add_foreign_key "merge_requests", "merge_request_diffs", column: "latest_merge_request_diff_id", name: "fk_06067f5644", on_delete: :nullify
+ add_foreign_key "merge_requests", "milestones", name: "fk_6a5165a692", on_delete: :nullify
+ add_foreign_key "merge_requests", "projects", column: "source_project_id", name: "fk_3308fe130c", on_delete: :nullify
add_foreign_key "merge_requests", "projects", column: "target_project_id", name: "fk_a6963e8447", on_delete: :cascade
+ add_foreign_key "merge_requests", "users", column: "assignee_id", name: "fk_6149611a04", on_delete: :nullify
+ add_foreign_key "merge_requests", "users", column: "author_id", name: "fk_e719a85f8a", on_delete: :nullify
+ add_foreign_key "merge_requests", "users", column: "merge_user_id", name: "fk_ad525e1f87", on_delete: :nullify
+ add_foreign_key "merge_requests", "users", column: "updated_by_id", name: "fk_641731faff", on_delete: :nullify
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade
@@ -1851,6 +1988,7 @@ ActiveRecord::Schema.define(version: 20171006091000) do
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade
add_foreign_key "project_auto_devops", "projects", on_delete: :cascade
+ add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade
add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index c6500a37aa9..d4119d35162 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,5 +1,6 @@
---
toc: false
+comments: false
---
# GitLab Documentation
@@ -85,7 +86,7 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
### Issues and Merge Requests (MRs)
-- [Discussions](user/discussions/index.md) Threads, comments, and resolvable discussions in issues, commits, and merge requests.
+- [Discussions](user/discussions/index.md): Threads, comments, and resolvable discussions in issues, commits, and merge requests.
- [Issues](user/project/issues/index.md)
- [Project issue Board](user/project/issue_board.md)
- [Issues and merge requests templates](user/project/description_templates.md): Create templates for submitting new issues and merge requests.
@@ -155,7 +156,7 @@ have access to GitLab administration tools and settings.
- [Git LFS configuration](workflow/lfs/lfs_administration.md): Learn how to use LFS under GitLab.
- [GitLab Pages configuration](administration/pages/index.md): Configure GitLab Pages.
- [High Availability](administration/high_availability/README.md): Configure multiple servers for scaling or high availability.
-- [User cohorts](user/admin_area/user_cohorts.md) View user activity over time.
+- [User cohorts](user/admin_area/user_cohorts.md): View user activity over time.
- [Web terminals](administration/integration/terminal.md): Provide terminal access to environments from within GitLab.
- GitLab CI
- [CI admin settings](user/admin_area/settings/continuous_integration.md): Define max artifacts size and expiration time.
@@ -170,6 +171,7 @@ have access to GitLab administration tools and settings.
- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md): Configure GitLab and InfluxDB for measuring performance metrics.
- [GitLab performance monitoring with Prometheus](administration/monitoring/prometheus/index.md): Configure GitLab and Prometheus for measuring performance metrics.
- [Monitoring uptime](user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint.
+- [Monitoring GitHub imports](administration/monitoring/github_imports.md)
### Performance
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
index 13bd501e397..ee9b9a9466a 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Authentication and Authorization
GitLab integrates with the following external authentication and authorization
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index ad903aef896..6b5a0f139c5 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -30,6 +30,12 @@ immediately block all access.
>**Note**: GitLab EE supports a configurable sync time, with a default
of one hour.
+## Git password authentication
+
+LDAP-enabled users can always authenticate with Git using their GitLab username
+or email and LDAP password, even if password authentication for Git is disabled
+in the application settings.
+
## Configuration
To enable LDAP integration you need to add your LDAP server settings in
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index 4d3be0ab8f6..a88e67bfeb5 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -53,7 +53,9 @@ or in different cloud availability zones.
> **Note:** GitLab recommends against choosing this HA method because of the
complexity of managing DRBD and crafting automatic failover. This is
- *compatible* with GitLab, but not officially *supported*.
+ *compatible* with GitLab, but not officially *supported*. If you are
+ an EE customer, support will help you with GitLab related problems, but if the
+ root cause is identified as DRBD, we will not troubleshoot further.
Components/Servers Required: 2 servers/virtual machines (one active/one passive)
diff --git a/doc/administration/img/circuitbreaker_config.png b/doc/administration/img/circuitbreaker_config.png
new file mode 100644
index 00000000000..e811d173634
--- /dev/null
+++ b/doc/administration/img/circuitbreaker_config.png
Binary files differ
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index 652ca9cf454..93c3642a1f1 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -56,29 +56,34 @@ that, login with an Admin account and do following:
With PlantUML integration enabled and configured, we can start adding diagrams to
our AsciiDoc snippets, wikis and repos using delimited blocks:
-```
-[plantuml, format="png", id="myDiagram", width="200px"]
---
-Bob->Alice : hello
-Alice -> Bob : Go Away
---
-```
+- **Markdown**
+
+ ```plantuml
+ Bob -> Alice : hello
+ Alice -> Bob : Go Away
+ ```
-And in Markdown using fenced code blocks:
+- **AsciiDoc**
- ```plantuml
- Bob -> Alice : hello
+ ```
+ [plantuml, format="png", id="myDiagram", width="200px"]
+ --
+ Bob->Alice : hello
Alice -> Bob : Go Away
+ --
```
-And in reStructuredText using a directive:
+- **reStructuredText**
-```
-.. plantuml::
+ ```
+ .. plantuml::
+ :caption: Caption with **bold** and *italic*
- Bob -> Alice: hello
- Alice -> Bob: Go Away
-```
+ Bob -> Alice: hello
+ Alice -> Bob: Go Away
+ ```
+
+ You can also use the `uml::` directive for compatibility with [sphinxcontrib-plantuml](https://pypi.python.org/pypi/sphinxcontrib-plantuml), but please note that we currently only support the `caption` option.
The above blocks will be converted to an HTML img tag with source pointing to the
PlantUML instance. If the PlantUML server is correctly configured, this should
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 76e071dc673..debaa2330d0 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -18,8 +18,7 @@ other than production, the corresponding logfile is shown here.)
It contains a structured log for Rails controller requests received from
GitLab, thanks to [Lograge](https://github.com/roidrage/lograge/). Note that
-requests from the API [are not yet logged to this
-file](https://gitlab.com/gitlab-org/gitlab-ce/issues/36189).
+requests from the API are logged to a separate file in `api_json.log`.
Each line contains a JSON line that can be ingested by Elasticsearch, Splunk, etc. For example:
@@ -73,6 +72,27 @@ In this example we can see that server processed an HTTP request with URL
19:34:53 +0200. Also we can see that request was processed by
`Projects::TreeController`.
+## `api_json.log`
+
+Introduced in GitLab 10.0, this file lives in
+`/var/log/gitlab/gitlab-rails/api_json.log` for Omnibus GitLab packages or in
+`/home/git/gitlab/log/api_json.log` for installations from source.
+
+It helps you see requests made directly to the API. For example:
+
+```json
+{"time":"2017-10-10T12:30:11.579Z","severity":"INFO","duration":16.84,"db":1.57,"view":15.27,"status":200,"method":"POST","path":"/api/v4/internal/allowed","params":{"action":"git-upload-pack","changes":"_any","gl_repository":null,"project":"root/foobar.git","protocol":"ssh","env":"{}","key_id":"[FILTERED]","secret_token":"[FILTERED]"},"host":"127.0.0.1","ip":"127.0.0.1","ua":"Ruby"}
+```
+
+This entry above shows an access to an internal endpoint to check whether an
+associated SSH key can download the project in question via a `git fetch` or
+`git clone`. In this example, we see:
+
+1. `method`: The HTTP method used to make the request
+1. `path`: The relative path of the query
+1. `params`: Key-value pairs passed in a query string or HTTP body. Sensitive parameters (e.g. passwords, tokens, etc.) are filtered out.
+1. `ua`: The User-Agent of the requester
+
## `application.log`
This file lives in `/var/log/gitlab/gitlab-rails/application.log` for
@@ -172,4 +192,13 @@ installations from source.
It logs information whenever a [repository check is run][repocheck] on a project.
+## Reconfigure Logs
+
+Reconfigure log files live in `/var/log/gitlab/reconfigure` for Omnibus GitLab
+packages. Installations from source don't have reconfigure logs. A reconfigure log
+is populated whenever `gitlab-ctl reconfigure` is run manually or as part of an upgrade.
+
+Reconfigure logs files are named according to the UNIX timestamp of when the reconfigure
+was initiated, such as `1509705644.log`
+
[repocheck]: repository_checks.md
diff --git a/doc/administration/monitoring/github_imports.md b/doc/administration/monitoring/github_imports.md
new file mode 100644
index 00000000000..5592e0a9e9a
--- /dev/null
+++ b/doc/administration/monitoring/github_imports.md
@@ -0,0 +1,101 @@
+# Monitoring GitHub imports
+
+>**Note:**
+Available since [GitLab 10.2][14731].
+
+The GitHub importer exposes various Prometheus metrics that you can use to
+monitor the health and progress of the importer.
+
+## Import Duration Times
+
+| Name | Type |
+|------------------------------------------|-----------|
+| `github_importer_total_duration_seconds` | histogram |
+
+This metric tracks the total time spent (in seconds) importing a project (from
+project creation until the import process finishes), for every imported project.
+
+The name of the project is stored in the `project` label in the format
+`namespace/name` (e.g. `gitlab-org/gitlab-ce`).
+
+## Number of imported projects
+
+| Name | Type |
+|-------------------------------------|---------|
+| `github_importer_imported_projects` | counter |
+
+This metric tracks the total number of projects imported over time. This metric
+does not expose any labels.
+
+## Number of GitHub API calls
+
+| Name | Type |
+|---------------------------------|---------|
+| `github_importer_request_count` | counter |
+
+This metric tracks the total number of GitHub API calls performed over time, for
+all projects. This metric does not expose any labels.
+
+## Rate limit errors
+
+| Name | Type |
+|-----------------------------------|---------|
+| `github_importer_rate_limit_hits` | counter |
+
+This metric tracks the number of times we hit the GitHub rate limit, for all
+projects. This metric does not expose any labels.
+
+## Number of imported issues
+
+| Name | Type |
+|-----------------------------------|---------|
+| `github_importer_imported_issues` | counter |
+
+This metric tracks the number of imported issues across all projects.
+
+The name of the project is stored in the `project` label in the format
+`namespace/name` (e.g. `gitlab-org/gitlab-ce`).
+
+## Number of imported pull requests
+
+| Name | Type |
+|------------------------------------------|---------|
+| `github_importer_imported_pull_requests` | counter |
+
+This metric tracks the number of imported pull requests across all projects.
+
+The name of the project is stored in the `project` label in the format
+`namespace/name` (e.g. `gitlab-org/gitlab-ce`).
+
+## Number of imported comments
+
+| Name | Type |
+|----------------------------------|---------|
+| `github_importer_imported_notes` | counter |
+
+This metric tracks the number of imported comments across all projects.
+
+The name of the project is stored in the `project` label in the format
+`namespace/name` (e.g. `gitlab-org/gitlab-ce`).
+
+## Number of imported pull request review comments
+
+| Name | Type |
+|---------------------------------------|---------|
+| `github_importer_imported_diff_notes` | counter |
+
+This metric tracks the number of imported comments across all projects.
+
+The name of the project is stored in the `project` label in the format
+`namespace/name` (e.g. `gitlab-org/gitlab-ce`).
+
+## Number of imported repositories
+
+| Name | Type |
+|-----------------------------------------|---------|
+| `github_importer_imported_repositories` | counter |
+
+This metric tracks the number of imported repositories across all projects. This
+metric does not expose any labels.
+
+[14731]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14731
diff --git a/doc/administration/monitoring/performance/performance_bar.md b/doc/administration/monitoring/performance/performance_bar.md
index 68efe0aae5c..b9464945cea 100644
--- a/doc/administration/monitoring/performance/performance_bar.md
+++ b/doc/administration/monitoring/performance/performance_bar.md
@@ -28,6 +28,12 @@ will be allowed to display the Performance Bar.
Make sure _Enable the Performance Bar_ is checked and hit
**Save** to save the changes.
+Once the Performance Bar is enabled, you will need to press the [<kbd>p</kbd> +
+<kbd>b</kbd> keyboard shortcut](../../../workflow/shortcuts.md) to actually
+display it.
+
+You can toggle the Bar using the same shortcut.
+
---
![GitLab Performance Bar Admin Settings](img/performance_bar_configuration_settings.png)
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 6baae20d16a..11d5e077a36 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -20,7 +20,7 @@ it, the client IP needs to be [included in a whitelist][whitelist].
Currently the embedded Prometheus server is not automatically configured to
collect metrics from this endpoint. We recommend setting up another Prometheus
server, because the embedded server configuration is overwritten once every
-[reconfigure of GitLab][reconfigure]. In the future this will not be required.
+[reconfigure of GitLab][reconfigure]. In the future this will not be required.
## Metrics available
@@ -45,6 +45,8 @@ In this experimental phase, only a few metrics are available:
| redis_ping_success | Gauge | 9.4 | Whether or not the last redis ping succeeded |
| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping |
| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in |
+| filesystem_circuitbreaker_latency_seconds | Histogram | 9.5 | Latency of the stat check the circuitbreaker uses to probe a shard |
+| filesystem_circuitbreaker | Gauge | 9.5 | Wether or not the circuit for a certain shard is broken or not |
## Metrics shared directory
diff --git a/doc/administration/operations/sidekiq_memory_killer.md b/doc/administration/operations/sidekiq_memory_killer.md
index b5e78348989..cbffd883774 100644
--- a/doc/administration/operations/sidekiq_memory_killer.md
+++ b/doc/administration/operations/sidekiq_memory_killer.md
@@ -28,7 +28,7 @@ The MemoryKiller is controlled using environment variables.
delayed shutdown is triggered. The default value for Omnibus packages is set
[in the omnibus-gitlab
repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb).
-- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults 900 seconds (15 minutes). When
+- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults to 900 seconds (15 minutes). When
a shutdown is triggered, the Sidekiq process will keep working normally for
another 15 minutes.
- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defaults to 30 seconds. When the grace
@@ -36,5 +36,3 @@ The MemoryKiller is controlled using environment variables.
Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells
Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must
restart Sidekiq.
-- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to `SIGKILL`. The name of
- the final signal sent to the Sidekiq process when we want it to shut down.
diff --git a/doc/administration/raketasks/github_import.md b/doc/administration/raketasks/github_import.md
index 04c70c3644e..6b8ad1b039b 100644
--- a/doc/administration/raketasks/github_import.md
+++ b/doc/administration/raketasks/github_import.md
@@ -7,6 +7,7 @@
> projects. You can get it from: https://github.com/settings/tokens
> - You also need to pass an username as the second argument to the rake task
> which will become the owner of the project.
+> - You can also resume an import with the same command.
To import a project from the list of your GitHub projects available:
diff --git a/doc/administration/raketasks/storage.md b/doc/administration/raketasks/storage.md
index bac8fa4bd9d..6ec5baeb6e3 100644
--- a/doc/administration/raketasks/storage.md
+++ b/doc/administration/raketasks/storage.md
@@ -1,10 +1,43 @@
# Repository Storage Rake Tasks
This is a collection of rake tasks you can use to help you list and migrate
-existing projects from Legacy storage to the new Hashed storage type.
+existing projects and attachments associated with it from Legacy storage to
+the new Hashed storage type.
You can read more about the storage types [here][storage-types].
+## Migrate existing projects to Hashed storage
+
+Before migrating your existing projects, you should
+[enable hashed storage][storage-migration] for the new projects as well.
+
+This task will schedule all your existing projects and attachments associated with it to be migrated to the
+**Hashed** storage type:
+
+**Omnibus Installation**
+
+```bash
+gitlab-rake gitlab:storage:migrate_to_hashed
+```
+
+**Source Installation**
+
+```bash
+rake gitlab:storage:migrate_to_hashed
+
+```
+
+You can monitor the progress in the _Admin > Monitoring > Background jobs_ screen.
+There is a specific Queue you can watch to see how long it will take to finish: **project_migrate_hashed_storage**
+
+After it reaches zero, you can confirm every project has been migrated by running the commands bellow.
+If you find it necessary, you can run this migration script again to schedule missing projects.
+
+Any error or warning will be logged in the sidekiq's log file.
+
+You only need the `gitlab:storage:migrate_to_hashed` rake task to migrate your repositories, but we have additional
+commands below that helps you inspect projects and attachments in both legacy and hashed storage.
+
## List projects on Legacy storage
To have a simple summary of projects using **Legacy** storage:
@@ -73,35 +106,73 @@ rake gitlab:storage:list_hashed_projects
```
-## Migrate existing projects to Hashed storage
+## List attachments on Legacy storage
-Before migrating your existing projects, you should
-[enable hashed storage][storage-migration] for the new projects as well.
+To have a simple summary of project attachments using **Legacy** storage:
-This task will schedule all your existing projects to be migrated to the
-**Hashed** storage type:
+**Omnibus Installation**
+
+```bash
+gitlab-rake gitlab:storage:legacy_attachments
+```
+
+**Source Installation**
+
+```bash
+rake gitlab:storage:legacy_attachments
+
+```
+
+------
+
+To list project attachments using **Legacy** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:migrate_to_hashed
+gitlab-rake gitlab:storage:list_legacy_attachments
```
**Source Installation**
```bash
-rake gitlab:storage:migrate_to_hashed
+rake gitlab:storage:list_legacy_attachments
```
-You can monitor the progress in the _Admin > Monitoring > Background jobs_ screen.
-There is a specific Queue you can watch to see how long it will take to finish: **project_migrate_hashed_storage**
+## List attachments on Hashed storage
-After it reaches zero, you can confirm every project has been migrated by running the commands above.
-If you find it necessary, you can run this migration script again to schedule missing projects.
+To have a simple summary of project attachments using **Hashed** storage:
+
+**Omnibus Installation**
+
+```bash
+gitlab-rake gitlab:storage:hashed_attachments
+```
-Any error or warning will be logged in the sidekiq log file.
+**Source Installation**
+
+```bash
+rake gitlab:storage:hashed_attachments
+
+```
+
+------
+
+To list project attachments using **Hashed** storage:
+
+**Omnibus Installation**
+```bash
+gitlab-rake gitlab:storage:list_hashed_attachments
+```
+
+**Source Installation**
+
+```bash
+rake gitlab:storage:list_hashed_attachments
+
+```
[storage-types]: ../repository_storage_types.md
[storage-migration]: ../repository_storage_types.md#how-to-migrate-to-hashed-storage
diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md
index 624a908b3a3..96f436fa7c3 100644
--- a/doc/administration/repository_storage_paths.md
+++ b/doc/administration/repository_storage_paths.md
@@ -105,61 +105,40 @@ When GitLab detects access to the repositories storage fails repeatedly, it can
gracefully prevent attempts to access the storage. This might be useful when
the repositories are stored somewhere on the network.
-The configuration could look as follows:
+This can be configured from the admin interface:
-**For Omnibus installations**
-
-1. Edit `/etc/gitlab/gitlab.rb`:
+![circuitbreaker configuration](img/circuitbreaker_config.png)
- ```ruby
- git_data_dirs({
- "default" => {
- "path" => "/mnt/nfs-01/git-data",
- "failure_count_threshold" => 10,
- "failure_wait_time" => 30,
- "failure_reset_time" => 1800,
- "storage_timeout" => 5
- }
- })
- ```
-
-1. Save the file and [reconfigure GitLab][reconfigure-gitlab] for the changes to take effect.
-
----
-
-**For installations from source**
+**Number of access attempts**: The number of attempts GitLab will make to access a
+storage when probing a shard.
-1. Edit `config/gitlab.yml`:
+**Number of failures before backing off**: The number of failures after which
+GitLab will start temporarily disabling access to a storage shard on a host.
- ```yaml
- repositories:
- storages: # You must have at least a `default` storage path.
- default:
- path: /home/git/repositories/
- failure_count_threshold: 10 # number of failures before stopping attempts
- failure_wait_time: 30 # Seconds after last access failure before trying 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
- ```
-
-1. Save the file and [restart GitLab][restart-gitlab] for the changes to take effect.
-
-
-**`failure_count_threshold`:** The number of failures of after which GitLab will
+**Maximum git storage failures:** The number of failures of after which GitLab will
completely prevent access to the storage. The number of failures can be reset in
the admin interface: `https://gitlab.example.com/admin/health_check` or using the
[api](../api/repository_storage_health.md) to allow access to the storage again.
-**`failure_wait_time`:** When access to a storage fails. GitLab will prevent
-access to the storage for the time specified here. This allows the filesystem to
-recover without.
+**Seconds to wait after a storage failure:** When access to a storage fails. GitLab
+will prevent access to the storage for the time specified here. This allows the
+filesystem to recover.
-**`failure_reset_time`:** The time in seconds GitLab will keep failure
-information. When no failures occur during this time, information about the
+**Seconds before reseting failure information:** The time in seconds GitLab will
+keep failure information. When no failures occur during this time, information about the
mount is reset.
-**`storage_timeout`:** The time in seconds GitLab will try to access storage.
-After this time a timeout error will be raised.
+**Seconds to wait for a storage access attempt:** The time in seconds GitLab will
+try to access storage. After this time a timeout error will be raised.
+
+To enable the circuitbreaker for repository storage you can flip the feature flag from a rails console:
+
+```
+Feature.enable('git_storage_circuit_breaker')
+```
+
+Alternatively it can be enabled by setting `true` in the `GIT_STORAGE_CIRCUIT_BREAKER` environment variable.
+This approach would be used when enabling the circuit breaker on a single host.
When storage failures occur, this will be visible in the admin interface like this:
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index fa882bbe28a..21184fed6e9 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -9,7 +9,7 @@ mapping structure from the projects URLs:
* Project's repository: `#{namespace}/#{project_name}.git`
* Project's wiki: `#{namespace}/#{project_name}.wiki.git`
-
+
This structure made simple to migrate from existing solutions to GitLab and easy for Administrators to find where the
repository is stored.
@@ -25,7 +25,10 @@ Any change in the URL will need to be reflected on disk (when groups / users or
of load in big installations, and can be even worst if they are using any type of network based filesystem.
Last, for GitLab Geo, this storage type means we have to synchronize the disk state, replicate renames in the correct
-order or we may end-up with wrong repository or missing data temporarily.
+order or we may end-up with wrong repository or missing data temporarily.
+
+This pattern also exists in other objects stored in GitLab, like issue Attachments, GitLab Pages artifacts,
+Docker Containers for the integrated Registry, etc.
## Hashed Storage
@@ -59,11 +62,31 @@ you will never mistakenly restore a repository in the wrong project (considering
### How to migrate to Hashed Storage
-In GitLab, go to **Admin > Settings**, find the **Repository Storage** section and select
+In GitLab, go to **Admin > Settings**, find the **Repository Storage** section and select
"_Create new projects using hashed storage paths_".
-
+
To migrate your existing projects to the new storage type, check the specific [rake tasks].
[ce-28283]: https://gitlab.com/gitlab-org/gitlab-ce/issues/28283
[rake tasks]: raketasks/storage.md#migrate-existing-projects-to-hashed-storage
[storage-paths]: repository_storage_types.md
+
+### Hashed Storage coverage
+
+We are incrementally moving every storable object in GitLab to the Hashed Storage pattern. You can check the current
+coverage status below.
+
+Note that things stored in an S3 compatible endpoint will not have the downsides mentioned earlier, if they are not
+prefixed with `#{namespace}/#{project_name}`, which is true for CI Cache and LFS Objects.
+
+| Storable Object | Legacy Storage | Hashed Storage | S3 Compatible | GitLab Version |
+| --------------- | -------------- | -------------- | ------------- | -------------- |
+| Repository | Yes | Yes | - | 10.0 |
+| Attachments | Yes | Yes | - | 10.2 |
+| Avatars | Yes | No | - | - |
+| Pages | Yes | No | - | - |
+| Docker Registry | Yes | No | - | - |
+| CI Build Logs | No | No | - | - |
+| CI Artifacts | No | No | Yes (EEP) | - |
+| CI Cache | No | No | Yes | - |
+| LFS Objects | Yes | No | Yes (EEP) | - |
diff --git a/doc/administration/repository_storages.md b/doc/administration/repository_storages.md
index 9d41ba77f34..cf6de15743f 100644
--- a/doc/administration/repository_storages.md
+++ b/doc/administration/repository_storages.md
@@ -1,3 +1 @@
-# Repository storages
-
-This document was moved to a [new location](repository_storage_paths.md).
+This document was moved to [another location](repository_storage_paths.md).
diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md
index 6f1356ddf8f..83a714810c1 100644
--- a/doc/administration/troubleshooting/debug.md
+++ b/doc/administration/troubleshooting/debug.md
@@ -141,7 +141,7 @@ separate Rails process to debug the issue:
1. Log in to your GitLab account.
1. Copy the URL that is causing problems (e.g. https://gitlab.com/ABC).
-1. Obtain the private token for your user (Profile Settings -> Account).
+1. Create a Personal Access Token for your user (Profile Settings -> Access Tokens).
1. Bring up the GitLab Rails console. For omnibus users, run:
```
@@ -163,6 +163,34 @@ separate Rails process to debug the issue:
1. In a new window, run `top`. It should show this ruby process using 100% CPU. Write down the PID.
1. Follow step 2 from the previous section on using gdb.
+### GitLab: API is not accessible
+
+This often occurs when gitlab-shell attempts to request authorization via the
+internal API (e.g., `http://localhost:8080/api/v4/internal/allowed`), and
+something in the check fails. There are many reasons why this may happen:
+
+1. Timeout connecting to a database (e.g., PostgreSQL or Redis)
+1. Error in Git hooks or push rules
+1. Error accessing the repository (e.g., stale NFS handles)
+
+To diagnose this problem, try to reproduce the problem and then see if there
+is a unicorn worker that is spinning via `top`. Try to use the `gdb`
+techniques above. In addition, using `strace` may help isolate issues:
+
+```shell
+strace -tt -T -f -s 1024 -p <PID of unicorn worker> -o /tmp/unicorn.txt
+```
+
+If you cannot isolate which Unicorn worker is the issue, try to run `strace`
+on all the Unicorn workers to see where the `/internal/allowed` endpoint gets
+stuck:
+
+```shell
+ps auwx | grep unicorn | awk '{ print " -p " $2}' | xargs strace -tt -T -f -s 1024 -o /tmp/unicorn.txt
+```
+
+The output in `/tmp/unicorn.txt` may help diagnose the root cause.
+
# More information
* [Debugging Stuck Ruby Processes](https://blog.newrelic.com/2013/04/29/debugging-stuck-ruby-processes-what-to-do-before-you-kill-9/)
diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md
index b71f8fabbc8..9d157720ad2 100644
--- a/doc/administration/troubleshooting/sidekiq.md
+++ b/doc/administration/troubleshooting/sidekiq.md
@@ -11,7 +11,7 @@ troubleshooting steps that will help you diagnose the bottleneck.
debug steps with GitLab Support so the backtraces can be analyzed by our team.
It may reveal a bug or necessary improvement in GitLab.
-> **Note:** In any of the backtraces, be weary of suspecting cases where every
+> **Note:** In any of the backtraces, be wary of suspecting cases where every
thread appears to be waiting in the database, Redis, or waiting to acquire
a mutex. This **may** mean there's contention in the database, for example,
but look for one thread that is different than the rest. This other thread
diff --git a/doc/api/README.md b/doc/api/README.md
index 3fd4c97e536..f226716c3b5 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -37,6 +37,7 @@ following locations:
- [Notes](notes.md) (comments)
- [Notification settings](notification_settings.md)
- [Open source license templates](templates/licenses.md)
+- [Pages Domains](pages_domains.md)
- [Pipelines](pipelines.md)
- [Pipeline Triggers](pipeline_triggers.md)
- [Pipeline Schedules](pipeline_schedules.md)
@@ -49,7 +50,6 @@ following locations:
- [Repository Files](repository_files.md)
- [Runners](runners.md)
- [Services](services.md)
-- [Session](session.md)
- [Settings](settings.md)
- [Sidekiq metrics](sidekiq_metrics.md)
- [System Hooks](system_hooks.md)
@@ -63,7 +63,21 @@ following locations:
## Road to 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.
+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.
+
+Although there were some patenting and licensing concerns with GraphQL, these
+have been resolved to our satisfaction by the relicensing of the reference
+implementations under MIT, and the use of the OWF license for the GraphQL
+specification.
## Basic usage
@@ -71,27 +85,10 @@ API requests should be prefixed with `api` and the API version. The API version
is defined in [`lib/api.rb`][lib-api-url]. For example, the root of the v4 API
is at `/api/v4`.
-For endpoints that require [authentication](#authentication), you need to pass
-a `private_token` parameter via query string or header. If passed as a header,
-the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of
-an underscore).
-
-Example of a valid API request:
-
-```
-GET /projects?private_token=9koXpg98eAheJpvBs5tK
-```
-
-Example of a valid API request using cURL and authentication via header:
-
-```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects"
-```
-
-Example of a valid API request using cURL and authentication via a query string:
+Example of a valid API request using cURL:
```shell
-curl "https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK"
+curl "https://gitlab.example.com/api/v4/projects"
```
The API uses JSON to serialize data. You don't need to specify `.json` at the
@@ -99,15 +96,20 @@ end of an API URL.
## Authentication
-Most API requests require authentication via a session cookie or token. For
+Most API requests require authentication, or will only return public data when
+authentication is not provided. For
those cases where it is not required, this will be mentioned in the documentation
for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md).
-There are three types of access tokens available:
+There are three ways to authenticate with the GitLab API:
1. [OAuth2 tokens](#oauth2-tokens)
-1. [Private tokens](#private-tokens)
1. [Personal access tokens](#personal-access-tokens)
+1. [Session cookie](#session-cookie)
+
+For admins who want to authenticate with the API as a specific user, or who want to build applications or scripts that do so, two options are available:
+1. [Impersonation tokens](#impersonation-tokens)
+2. [Sudo](#sudo)
If authentication information is invalid or omitted, an error message will be
returned with status code `401`:
@@ -118,74 +120,84 @@ returned with status code `401`:
}
```
-### Session cookie
+### OAuth2 tokens
-When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is
-set. The API will use this cookie for authentication if it is present, but using
-the API to generate a new session cookie is currently not supported.
+You can use an [OAuth2 token](oauth2.md) to authenticate with the API by passing it in either the
+`access_token` parameter or the `Authorization` header.
-### OAuth2 tokens
+Example of using the OAuth2 token in a parameter:
-You can use an OAuth 2 token to authenticate with the API by passing it either in the
-`access_token` parameter or in the `Authorization` header.
+```shell
+curl https://gitlab.example.com/api/v4/projects?access_token=OAUTH-TOKEN
+```
-Example of using the OAuth2 token in the header:
+Example of using the OAuth2 token in a header:
```shell
curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/projects
```
-Read more about [GitLab as an OAuth2 client](oauth2.md).
+Read more about [GitLab as an OAuth2 provider](oauth2.md).
-### Private tokens
+### Personal access tokens
-Private tokens provide full access to the GitLab API. Anyone with access to
-them can interact with GitLab as if they were you. You can find or reset your
-private token in your account page (`/profile/account`).
+You can use a [personal access token][pat] to authenticate with the API by passing it in either the
+`private_token` parameter or the `Private-Token` header.
-For examples of usage, [read the basic usage section](#basic-usage).
+Example of using the personal access token in a parameter:
-### Personal access tokens
+```shell
+curl https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK
+```
-Instead of using your private token which grants full access to your account,
-personal access tokens could be a better fit because of their granular
-permissions.
+Example of using the personal access token in a header:
-Once you have your token, pass it to the API using either the `private_token`
-parameter or the `PRIVATE-TOKEN` header. For examples of usage,
-[read the basic usage section](#basic-usage).
+```shell
+curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects
+```
+
+Read more about [personal access tokens][pat].
+
+### Session cookie
+
+When signing in to the main GitLab application, a `_gitlab_session` cookie is
+set. The API will use this cookie for authentication if it is present, but using
+the API to generate a new session cookie is currently not supported.
-[Read more about personal access tokens.][pat]
+The primary user of this authentication method is the web frontend of GitLab itself,
+which can use the API as the authenticated user to get a list of their projects,
+for example, without needing to explicitly pass an access token.
### Impersonation tokens
> [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions.
Impersonation tokens are a type of [personal access token][pat]
-that can only be created by an admin for a specific user.
+that can only be created by an admin for a specific user. They are a great fit
+if you want to build applications or scripts that authenticate with the API as a specific user.
-They are a better alternative to using the user's password/private token
-or using the [Sudo](#sudo) feature which also requires the admin's password
-or private token, since the password/token can change over time. Impersonation
-tokens are a great fit if you want to build applications or tools which
-authenticate with the API as a specific user.
+They are an alternative to directly using the user's password or one of their
+personal access tokens, and to using the [Sudo](#sudo) feature, since the user's (or admin's, in the case of Sudo)
+password/token may not be known or may change over time.
For more information, refer to the
[users API](users.md#retrieve-user-impersonation-tokens) docs.
-For examples of usage, [read the basic usage section](#basic-usage).
+Impersonation tokens are used exactly like regular personal access tokens, and can be passed in either the
+`private_token` parameter or the `Private-Token` header.
### Sudo
> Needs admin permissions.
All API requests support performing an API call as if you were another user,
-provided your private token is from an administrator account. You need to pass
-the `sudo` parameter either via query string or a header with an ID/username of
+provided you are authenticated as an administrator with an OAuth or Personal Access Token that has the `sudo` scope.
+
+You need to pass the `sudo` parameter either via query string or a header with an ID/username of
the user you want to perform the operation as. If passed as a header, the
-header name must be `SUDO` (uppercase).
+header name must be `Sudo`.
-If a non administrative `private_token` is provided, then an error message will
+If a non administrative access token is provided, an error message will
be returned with status code `403`:
```json
@@ -194,12 +206,23 @@ be returned with status code `403`:
}
```
+If an access token without the `sudo` scope is provided, an error message will
+be returned with status code `403`:
+
+```json
+{
+ "error": "insufficient_scope",
+ "error_description": "The request requires higher privileges than provided by the access token.",
+ "scope": "sudo"
+}
+```
+
If the sudo user ID or username cannot be found, an error message will be
returned with status code `404`:
```json
{
- "message": "404 Not Found: No user id or username for: <id/username>"
+ "message": "404 User with ID or username '123' Not Found"
}
```
@@ -213,7 +236,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username
```
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v4/projects"
+curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" --header "Sudo: username" "https://gitlab.example.com/api/v4/projects"
```
Example of a valid API call and a request using cURL with sudo request,
@@ -224,7 +247,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23
```
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects"
+curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" --header "Sudo: 23" "https://gitlab.example.com/api/v4/projects"
```
## Status codes
@@ -441,6 +464,23 @@ Content-Type: application/json
}
```
+## Encoding `+` in ISO 8601 dates
+
+If you need to include a `+` in a query parameter, you may need to use `%2B` instead due
+a [W3 recommendation](http://www.w3.org/Addressing/URL/4_URI_Recommentations.html) that
+causes a `+` to be interpreted as a space. For example, in an ISO 8601 date, you may want to pass
+a time in Mountain Standard Time, such as:
+
+```
+2017-10-17T23:11:13.000+05:30
+```
+
+The correct encoding for the query parameter would be:
+
+```
+2017-10-17T23:11:13.000%2B05:30
+```
+
## Clients
There are many unofficial GitLab API Clients for most of the popular
diff --git a/doc/api/custom_attributes.md b/doc/api/custom_attributes.md
index 8b26f7093ab..91d1b0e1520 100644
--- a/doc/api/custom_attributes.md
+++ b/doc/api/custom_attributes.md
@@ -2,17 +2,22 @@
Every API call to custom attributes must be authenticated as administrator.
+Custom attributes are currently available on users, groups, and projects,
+which will be referred to as "resource" in this documentation.
+
## List custom attributes
-Get all custom attributes on a user.
+Get all custom attributes on a resource.
```
GET /users/:id/custom_attributes
+GET /groups/:id/custom_attributes
+GET /projects/:id/custom_attributes
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a user |
+| `id` | integer | yes | The ID of a resource |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/custom_attributes
@@ -35,15 +40,17 @@ Example response:
## Single custom attribute
-Get a single custom attribute on a user.
+Get a single custom attribute on a resource.
```
GET /users/:id/custom_attributes/:key
+GET /groups/:id/custom_attributes/:key
+GET /projects/:id/custom_attributes/:key
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a user |
+| `id` | integer | yes | The ID of a resource |
| `key` | string | yes | The key of the custom attribute |
```bash
@@ -61,16 +68,18 @@ Example response:
## Set custom attribute
-Set a custom attribute on a user. The attribute will be updated if it already exists,
+Set a custom attribute on a resource. The attribute will be updated if it already exists,
or newly created otherwise.
```
PUT /users/:id/custom_attributes/:key
+PUT /groups/:id/custom_attributes/:key
+PUT /projects/:id/custom_attributes/:key
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a user |
+| `id` | integer | yes | The ID of a resource |
| `key` | string | yes | The key of the custom attribute |
| `value` | string | yes | The value of the custom attribute |
@@ -89,15 +98,17 @@ Example response:
## Delete custom attribute
-Delete a custom attribute on a user.
+Delete a custom attribute on a resource.
```
DELETE /users/:id/custom_attributes/:key
+DELETE /groups/:id/custom_attributes/:key
+DELETE /projects/:id/custom_attributes/:key
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a user |
+| `id` | integer | yes | The ID of a resource |
| `key` | string | yes | The key of the custom attribute |
```bash
diff --git a/doc/api/environments.md b/doc/api/environments.md
index e8deb3e07e9..6e20781f51a 100644
--- a/doc/api/environments.md
+++ b/doc/api/environments.md
@@ -36,7 +36,7 @@ Creates a new environment with the given name and external_url.
It returns `201` if the environment was successfully created, `400` for wrong parameters.
```
-POST /projects/:id/environment
+POST /projects/:id/environments
```
| Attribute | Type | Required | Description |
diff --git a/doc/api/groups.md b/doc/api/groups.md
index c2daa8bc029..c1b5737c247 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -9,13 +9,13 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `skip_groups` | array of integers | no | Skip the group IDs passes |
-| `all_available` | boolean | no | Show all the groups you have access to |
-| `search` | string | no | Return list of authorized groups matching the search criteria |
+| `skip_groups` | array of integers | no | Skip the group IDs passed |
+| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
+| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
-| `owned` | boolean | no | Limit by groups owned by the current user |
+| `owned` | boolean | no | Limit to groups owned by the current user |
```
GET /groups
@@ -40,8 +40,89 @@ GET /groups
]
```
+When adding the parameter `statistics=true` and the authenticated user is an admin, additional group statistics are returned.
+
+```
+GET /groups?statistics=true
+```
+
+```json
+[
+ {
+ "id": 1,
+ "name": "Foobar Group",
+ "path": "foo-bar",
+ "description": "An interesting group",
+ "visibility": "public",
+ "lfs_enabled": true,
+ "avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg",
+ "web_url": "http://localhost:3000/groups/foo-bar",
+ "request_access_enabled": false,
+ "full_name": "Foobar Group",
+ "full_path": "foo-bar",
+ "parent_id": null,
+ "statistics": {
+ "storage_size" : 212,
+ "repository_size" : 33,
+ "lfs_objects_size" : 123,
+ "job_artifacts_size" : 57
+
+ }
+ }
+]
+```
+
You can search for groups by name or path, see below.
+You can filter by [custom attributes](custom_attributes.md) with:
+
+```
+GET /groups?custom_attributes[key]=value&custom_attributes[other_key]=other_value
+```
+
+## List a groups's subgroups
+
+> [Introduced][ce-15142] in GitLab 10.3.
+
+Get a list of visible direct subgroups in this group.
+When accessed without authentication, only public groups are returned.
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the parent group |
+| `skip_groups` | array of integers | no | Skip the group IDs passed |
+| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
+| `search` | string | no | Return the list of authorized groups matching the search criteria |
+| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
+| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
+| `statistics` | boolean | no | Include group statistics (admins only) |
+| `owned` | boolean | no | Limit to groups owned by the current user |
+
+```
+GET /groups/:id/subgroups
+```
+
+```json
+[
+ {
+ "id": 1,
+ "name": "Foobar Group",
+ "path": "foo-bar",
+ "description": "An interesting group",
+ "visibility": "public",
+ "lfs_enabled": true,
+ "avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/foo.jpg",
+ "web_url": "http://gitlab.example.com/groups/foo-bar",
+ "request_access_enabled": false,
+ "full_name": "Foobar Group",
+ "full_path": "foo-bar",
+ "parent_id": 123
+ }
+]
+```
+
## List a group's projects
Get a list of projects in this group. When accessed without authentication, only
@@ -434,3 +515,5 @@ And to switch pages add:
```
/groups?per_page=100&page=2
```
+
+[ce-15142]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15142
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 50a971102fb..b2e4b6d0955 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -15,6 +15,11 @@ given state (`opened`, `closed`, or `merged`) or all of them (`all`).
The pagination parameters `page` and `per_page` can be used to
restrict the list of merge requests.
+**Note**: the `changes_count` value in the response is a string, not an
+integer. This is because when an MR has too many changes to display and store,
+it will be capped at 1,000. In that case, the API will return the string
+`"1000+"` for the changes count.
+
```
GET /merge_requests
GET /merge_requests?state=opened
@@ -53,6 +58,8 @@ Parameters:
"project_id": 3,
"title": "test1",
"state": "opened",
+ "created_at": "2017-04-29T08:46:00Z",
+ "updated_at": "2017-04-29T08:46:00Z",
"upvotes": 0,
"downvotes": 0,
"author": {
@@ -92,6 +99,7 @@ Parameters:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -130,6 +138,11 @@ will be the same. In the case of a merge request from a fork,
`target_project_id` and `project_id` will be the same and
`source_project_id` will be the fork project's ID.
+**Note**: the `changes_count` value in the response is a string, not an
+integer. This is because when an MR has too many changes to display and store,
+it will be capped at 1,000. In that case, the API will return the string
+`"1000+"` for the changes count.
+
Parameters:
| Attribute | Type | Required | Description |
@@ -159,6 +172,8 @@ Parameters:
"project_id": 3,
"title": "test1",
"state": "opened",
+ "created_at": "2017-04-29T08:46:00Z",
+ "updated_at": "2017-04-29T08:46:00Z",
"upvotes": 0,
"downvotes": 0,
"author": {
@@ -198,6 +213,7 @@ Parameters:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -234,6 +250,8 @@ Parameters:
"project_id": 3,
"title": "test1",
"state": "merged",
+ "created_at": "2017-04-29T08:46:00Z",
+ "updated_at": "2017-04-29T08:46:00Z",
"upvotes": 0,
"downvotes": 0,
"author": {
@@ -274,6 +292,7 @@ Parameters:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": "9999999999999999999999999999999999999999",
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -386,6 +405,7 @@ Parameters:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -480,6 +500,7 @@ POST /projects/:id/merge_requests
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 0,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -565,6 +586,7 @@ Must include at least one non-required attribute from above.
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -670,6 +692,7 @@ Parameters:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": "9999999999999999999999999999999999999999",
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -747,6 +770,7 @@ Parameters:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
@@ -822,7 +846,8 @@ Example response when the GitLab issue tracker is used:
"created_at" : "2016-01-04T15:31:51.081Z",
"iid" : 6,
"labels" : [],
- "user_notes_count": 1
+ "user_notes_count": 1,
+ "changes_count": "1"
},
]
```
@@ -1077,6 +1102,7 @@ Example response:
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 7,
+ "changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1"
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index 5c0bebbaeb0..25cae5ce1f9 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -89,3 +89,55 @@ Example response:
}
]
```
+
+## Get namespace by ID
+
+Get a namespace by ID.
+
+```
+GET /namespaces/:id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | ID or path of the namespace |
+
+Example request:
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/namespaces/2
+```
+
+Example response:
+
+```json
+{
+ "id": 2,
+ "name": "group1",
+ "path": "group1",
+ "kind": "group",
+ "full_path": "group1",
+ "parent_id": "null",
+ "members_count_with_descendants": 2
+}
+```
+
+Example request:
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/namespaces/group1
+```
+
+Example response:
+
+```json
+{
+ "id": 2,
+ "name": "group1",
+ "path": "group1",
+ "kind": "group",
+ "full_path": "group1",
+ "parent_id": "null",
+ "members_count_with_descendants": 2
+}
+```
diff --git a/doc/api/notes.md b/doc/api/notes.md
index e627369e17b..d02ef84d0bd 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -10,12 +10,15 @@ Gets a list of all notes for a single issue.
```
GET /projects/:id/issues/:issue_iid/notes
+GET /projects/:id/issues/:issue_iid/notes?sort=asc&order_by=updated_at
```
-Parameters:
-
-- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
-- `issue_iid` (required) - The IID of an issue
+| 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
+| `issue_iid` | integer | yes | The IID of an issue
+| `sort` | string | no | Return issue notes sorted in `asc` or `desc` order. Default is `desc`
+| `order_by` | string | no | Return issue notes ordered by `created_at` or `updated_at` fields. Default is `created_at`
```json
[
@@ -133,12 +136,15 @@ Gets a list of all notes for a single snippet. Snippet notes are comments users
```
GET /projects/:id/snippets/:snippet_id/notes
+GET /projects/:id/snippets/:snippet_id/notes?sort=asc&order_by=updated_at
```
-Parameters:
-
-- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
-- `snippet_id` (required) - The ID of a project snippet
+| 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
+| `snippet_id` | integer | yes | The ID of a project snippet
+| `sort` | string | no | Return snippet notes sorted in `asc` or `desc` order. Default is `desc`
+| `order_by` | string | no | Return snippet notes ordered by `created_at` or `updated_at` fields. Default is `created_at`
### Get single snippet note
@@ -231,12 +237,15 @@ Gets a list of all notes for a single merge request.
```
GET /projects/:id/merge_requests/:merge_request_iid/notes
+GET /projects/:id/merge_requests/:merge_request_iid/notes?sort=asc&order_by=updated_at
```
-Parameters:
-
-- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
-- `merge_request_iid` (required) - The IID of a project merge request
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
+| `merge_request_iid` | integer | yes | The IID of a project merge request
+| `sort` | string | no | Return merge request notes sorted in `asc` or `desc` order. Default is `desc`
+| `order_by` | string | no | Return merge request notes ordered by `created_at` or `updated_at` fields. Default is `created_at`
### Get single merge request note
diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md
new file mode 100644
index 00000000000..50685f335f7
--- /dev/null
+++ b/doc/api/pages_domains.md
@@ -0,0 +1,195 @@
+# Pages domains API
+
+Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages](https://about.gitlab.com/features/pages/).
+
+The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature.
+
+## List all pages domains
+
+Get a list of all pages domains. The user must have admin permissions.
+
+```http
+GET /pages/domains
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/pages/domains
+```
+
+```json
+[
+ {
+ "domain": "ssl.domain.example",
+ "url": "https://ssl.domain.example",
+ "certificate": {
+ "expired": false,
+ "expiration": "2020-04-12T14:32:00.000Z"
+ }
+ }
+]
+```
+
+## List pages domains
+
+Get a list of project pages domains. The user must have permissions to view pages domains.
+
+```http
+GET /projects/:id/pages/domains
+```
+
+| 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 |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains
+```
+
+```json
+[
+ {
+ "domain": "www.domain.example",
+ "url": "http://www.domain.example"
+ },
+ {
+ "domain": "ssl.domain.example",
+ "url": "https://ssl.domain.example",
+ "certificate": {
+ "subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
+ "expired": false,
+ "certificate": "-----BEGIN CERTIFICATE-----\n … \n-----END CERTIFICATE-----",
+ "certificate_text": "Certificate:\n … \n"
+ }
+ }
+]
+```
+
+## Single pages domain
+
+Get a single project pages domain. The user must have permissions to view pages domains.
+
+```http
+GET /projects/:id/pages/domains/:domain
+```
+
+| 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 |
+| `domain` | string | yes | The domain |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains/www.domain.example
+```
+
+```json
+{
+ "domain": "www.domain.example",
+ "url": "http://www.domain.example"
+}
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
+```
+
+```json
+{
+ "domain": "ssl.domain.example",
+ "url": "https://ssl.domain.example",
+ "certificate": {
+ "subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
+ "expired": false,
+ "certificate": "-----BEGIN CERTIFICATE-----\n … \n-----END CERTIFICATE-----",
+ "certificate_text": "Certificate:\n … \n"
+ }
+}
+```
+
+## Create new pages domain
+
+Creates a new pages domain. The user must have permissions to create new pages domains.
+
+```http
+POST /projects/:id/pages/domains
+```
+
+| 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 |
+| `domain` | string | yes | The domain |
+| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.|
+| `key` | file/string | no | The certificate key in PEM format. |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="domain=ssl.domain.example" --form="certificate=@/path/to/cert.pem" --form="key=@/path/to/key.pem" https://gitlab.example.com/api/v4/projects/5/pages/domains
+```
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="domain=ssl.domain.example" --form="certificate=$CERT_PEM" --form="key=$KEY_PEM" https://gitlab.example.com/api/v4/projects/5/pages/domains
+```
+
+```json
+{
+ "domain": "ssl.domain.example",
+ "url": "https://ssl.domain.example",
+ "certificate": {
+ "subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
+ "expired": false,
+ "certificate": "-----BEGIN CERTIFICATE-----\n … \n-----END CERTIFICATE-----",
+ "certificate_text": "Certificate:\n … \n"
+ }
+}
+```
+
+## Update pages domain
+
+Updates an existing project pages domain. The user must have permissions to change an existing pages domains.
+
+```http
+PUT /projects/:id/pages/domains/:domain
+```
+
+| 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 |
+| `domain` | string | yes | The domain |
+| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.|
+| `key` | file/string | no | The certificate key in PEM format. |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="certificate=@/path/to/cert.pem" --form="key=@/path/to/key.pem" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
+```
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="certificate=$CERT_PEM" --form="key=$KEY_PEM" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
+```
+
+```json
+{
+ "domain": "ssl.domain.example",
+ "url": "https://ssl.domain.example",
+ "certificate": {
+ "subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
+ "expired": false,
+ "certificate": "-----BEGIN CERTIFICATE-----\n … \n-----END CERTIFICATE-----",
+ "certificate_text": "Certificate:\n … \n"
+ }
+}
+```
+
+## Delete pages domain
+
+Deletes an existing project pages domain.
+
+```http
+DELETE /projects/:id/pages/domains/:domain
+```
+
+| 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 |
+| `domain` | string | yes | The domain |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
+```
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index 890945cfc7e..a6631cab8c3 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -57,7 +57,7 @@ GET /projects/:id/pipelines/:pipeline_id
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipeline/46"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines/46"
```
Example of response
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 07331d05231..5a403f7593a 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -192,6 +192,12 @@ GET /projects
]
```
+You can filter by [custom attributes](custom_attributes.md) with:
+
+```
+GET /projects?custom_attributes[key]=value&custom_attributes[other_key]=other_value
+```
+
## List user projects
Get a list of visible projects for the given user. When accessed without
diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md
index 10faa95d7e8..81fe854060a 100644
--- a/doc/api/protected_branches.md
+++ b/doc/api/protected_branches.md
@@ -4,7 +4,7 @@
**Valid access levels**
-The access levels are defined in the `ProtectedBranchAccess::ALLOWED_ACCESS_LEVELS` constant. Currently, these levels are recognized:
+The access levels are defined in the `ProtectedRefAccess::ALLOWED_ACCESS_LEVELS` constant. Currently, these levels are recognized:
```
0 => No access
30 => Developer access
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index bccef924375..594babc74be 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -85,7 +85,7 @@ GET /projects/:id/repository/blobs/:sha
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
-- `sha` (required) - The commit or branch name
+- `sha` (required) - The blob SHA
## Raw blob content
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 6304a496f94..015b09a745e 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -215,6 +215,91 @@ DELETE /runners/:id
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6"
```
+## List runner's jobs
+
+List jobs that are being processed or were processed by specified Runner.
+
+```
+GET /runners/:id/jobs
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a runner |
+| `status` | string | no | Status of the job; one of: `running`, `success`, `failed`, `canceled` |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/1/jobs?status=running"
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 2,
+ "status": "running",
+ "stage": "test",
+ "name": "test",
+ "ref": "master",
+ "tag": false,
+ "coverage": null,
+ "created_at": "2017-11-16T08:50:29.000Z",
+ "started_at": "2017-11-16T08:51:29.000Z",
+ "finished_at": "2017-11-16T08:53:29.000Z",
+ "duration": 120,
+ "user": {
+ "id": 1,
+ "name": "John Doe2",
+ "username": "user2",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
+ "web_url": "http://localhost/user2",
+ "created_at": "2017-11-16T18:38:46.000Z",
+ "bio": null,
+ "location": null,
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": "",
+ "organization": null
+ },
+ "commit": {
+ "id": "97de212e80737a608d939f648d959671fb0a0142",
+ "short_id": "97de212e",
+ "title": "Update configuration\r",
+ "created_at": "2017-11-16T08:50:28.000Z",
+ "parent_ids": [
+ "1b12f15a11fc6e62177bef08f47bc7b5ce50b141",
+ "498214de67004b1da3d820901307bed2a68a8ef6"
+ ],
+ "message": "See merge request !123",
+ "author_name": "John Doe2",
+ "author_email": "user2@example.org",
+ "authored_date": "2017-11-16T08:50:27.000Z",
+ "committer_name": "John Doe2",
+ "committer_email": "user2@example.org",
+ "committed_date": "2017-11-16T08:50:27.000Z"
+ },
+ "pipeline": {
+ "id": 2,
+ "sha": "97de212e80737a608d939f648d959671fb0a0142",
+ "ref": "master",
+ "status": "running"
+ },
+ "project": {
+ "id": 1,
+ "description": null,
+ "name": "project1",
+ "name_with_namespace": "John Doe2 / project1",
+ "path": "project1",
+ "path_with_namespace": "namespace1/project1",
+ "created_at": "2017-11-16T18:38:46.620Z"
+ }
+ }
+]
+```
+
## List project's runners
List all runners (specific and shared) available in the project. Shared runners
diff --git a/doc/api/services.md b/doc/api/services.md
index 49b87a4228c..08df26db3ec 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -478,8 +478,8 @@ PUT /projects/:id/services/jira
| --------- | ---- | -------- | ----------- |
| `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. |
| `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
-| `username` | string | no | The username of the user created to be used with GitLab/JIRA. |
-| `password` | string | no | The password of the user created to be used with GitLab/JIRA. |
+| `username` | string | yes | The username of the user created to be used with GitLab/JIRA. |
+| `password` | string | yes | The password of the user created to be used with GitLab/JIRA. |
| `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
### Delete JIRA service
@@ -490,6 +490,41 @@ Remove all previously JIRA settings from a project.
DELETE /projects/:id/services/jira
```
+## Kubernetes
+
+Kubernetes / Openshift integration
+
+### Create/Edit Kubernetes service
+
+Set Kubernetes service for a project.
+
+```
+PUT /projects/:id/services/kubernetes
+```
+
+Parameters:
+
+- `namespace` (**required**) - The Kubernetes namespace to use
+- `api_url` (**required**) - The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com
+- `token` (**required**) - The service token to authenticate against the Kubernetes cluster with
+- `ca_pem` (optional) - A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)
+
+### Delete Kubernetes service
+
+Delete Kubernetes service for a project.
+
+```
+DELETE /projects/:id/services/kubernetes
+```
+
+### Get Kubernetes service settings
+
+Get Kubernetes service settings for a project.
+
+```
+GET /projects/:id/services/kubernetes
+```
+
## Slack slash commands
Ability to receive slash commands from a Slack chat instance.
@@ -572,7 +607,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `token` | string | yes | The Mattermost token |
-
+| `username` | string | no | The username to use to post the message |
### Delete Mattermost slash command service
@@ -582,6 +617,40 @@ Delete Mattermost slash command service for a project.
DELETE /projects/:id/services/mattermost-slash-commands
```
+## Packagist
+
+Update your project on Packagist, the main Composer repository, when commits or tags are pushed to GitLab.
+
+### Create/Edit Packagist service
+
+Set Packagist service for a project.
+
+```
+PUT /projects/:id/services/packagist
+```
+
+Parameters:
+
+- `username` (**required**)
+- `token` (**required**)
+- `server` (optional)
+
+### Delete Packagist service
+
+Delete Packagist service for a project.
+
+```
+DELETE /projects/:id/services/packagist
+```
+
+### Get Packagist service settings
+
+Get Packagist service settings for a project.
+
+```
+GET /projects/:id/services/packagist
+```
+
## Pipeline-Emails
Get emails for GitLab CI pipelines.
diff --git a/doc/api/session.md b/doc/api/session.md
deleted file mode 100644
index b97e26f34a2..00000000000
--- a/doc/api/session.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# Session API
-
->**Deprecation notice:**
-Starting in GitLab 8.11, this feature has been **disabled** for users with
-[two-factor authentication][2fa] turned on. These users can access the API
-using [personal access tokens] instead.
-
-You can login with both GitLab and LDAP credentials in order to obtain the
-private token.
-
-```
-POST /session
-```
-
-| Attribute | Type | Required | Description |
-| ---------- | ------- | -------- | -------- |
-| `login` | string | yes | The username of the user|
-| `email` | string | yes if login is not provided | The email of the user |
-| `password` | string | yes | The password of the user |
-
-```bash
-curl --request POST "https://gitlab.example.com/api/v4/session?login=john_smith&password=strongpassw0rd"
-```
-
-Example response:
-
-```json
-{
- "name": "John Smith",
- "username": "john_smith",
- "id": 32,
- "state": "active",
- "avatar_url": null,
- "created_at": "2015-01-29T21:07:19.440Z",
- "is_admin": true,
- "bio": null,
- "skype": "",
- "linkedin": "",
- "twitter": "",
- "website_url": "",
- "email": "john@example.com",
- "theme_id": 1,
- "color_scheme_id": 1,
- "projects_limit": 10,
- "current_sign_in_at": "2015-07-07T07:10:58.392Z",
- "identities": [],
- "can_create_group": true,
- "can_create_project": true,
- "two_factor_enabled": false,
- "private_token": "9koXpg98eAheJpvBs5tK"
-}
-```
-
-[2fa]: ../user/profile/account/two_factor_authentication.md
-[personal access tokens]: ../user/profile/personal_access_tokens.md
diff --git a/doc/api/settings.md b/doc/api/settings.md
index b78f1252108..22fb2baa8ec 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -25,7 +25,7 @@ Example response:
"id" : 1,
"default_branch_protection" : 2,
"restricted_visibility_levels" : [],
- "password_authentication_enabled" : true,
+ "password_authentication_enabled_for_web" : true,
"after_sign_out_path" : null,
"max_attachment_size" : 10,
"user_oauth_applications" : true,
@@ -64,38 +64,96 @@ PUT /application/settings
| Attribute | Type | Required | Description |
| --------- | ---- | :------: | ----------- |
-| `default_projects_limit` | integer | no | Project limit per user. Default is `100000` |
-| `signup_enabled` | boolean | no | Enable registration. Default is `true`. |
-| `password_authentication_enabled` | boolean | no | Enable authentication via a GitLab account password. Default is `true`. |
-| `gravatar_enabled` | boolean | no | Enable Gravatar |
-| `sign_in_text` | string | no | Text on login page |
-| `home_page_url` | string | no | Redirect to this URL when not logged in |
-| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `2`. |
-| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is null which means there is no restriction. |
-| `max_attachment_size` | integer | no | Limit attachment size in MB |
-| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes |
-| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.|
-| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.|
-| `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.|
-| `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. |
-| `domain_blacklist_enabled` | boolean | no | Enable/disable the `domain_blacklist` |
-| `domain_blacklist` | array of strings | yes (if `domain_blacklist_enabled` is `true`) | People trying to sign-up with emails from this domain will not be allowed to do so. |
-| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider |
-| `after_sign_out_path` | string | no | Where to redirect users after logout |
-| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
-| `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. |
-| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
-| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. |
-| `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. |
-| `disabled_oauth_sign_in_sources` | Array of strings | no | Disabled OAuth sign-in sources |
-| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. |
-| `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.
+| `admin_notification_email` | string | no | Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. |
+| `after_sign_out_path` | string | no | Where to redirect users after logout |
+| `after_sign_up_text` | string | no | Text shown to the user after signing up |
+| `akismet_api_key` | string | no | API key for akismet spam protection |
+| `akismet_enabled` | boolean | no | Enable or disable akismet spam protection |
+| `circuitbreaker_access_retries | integer | no | The number of attempts GitLab will make to access a storage. |
+| `circuitbreaker_backoff_threshold | integer | no | The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host. |
+| `circuitbreaker_failure_count_threshold` | integer | no | The number of failures of after which GitLab will completely prevent access to the storage. |
+| `circuitbreaker_failure_reset_time` | integer | no | Time in seconds GitLab will keep storage failure information. When no failures occur during this time, the failure information is reset. |
+| `circuitbreaker_failure_wait_time` | integer | no | Time in seconds GitLab will block access to a failing storage to allow it to recover. |
+| `circuitbreaker_storage_timeout` | integer | no | Seconds to wait for a storage access attempt |
+| `clientside_sentry_dsn` | string | no | Required if `clientside_sentry_dsn` is enabled |
+| `clientside_sentry_enabled` | boolean | no | Enable Sentry error reporting for the client side |
+| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
+| `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts |
+| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push, or delete the branch)_, `1` _(partially protected, developers and masters can push new commits, but cannot force push or delete the branch)_ or `2` _(fully protected, developers cannot push new commits, but masters can; no-one can force push or delete the branch)_ as a parameter. Default is `2`. |
+| `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
+| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
+| `default_projects_limit` | integer | no | Project limit per user. Default is `100000` |
+| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
+| `disabled_oauth_sign_in_sources` | Array of strings | no | Disabled OAuth sign-in sources |
+| `domain_blacklist_enabled` | boolean | no | Enable/disable the `domain_blacklist` |
+| `domain_blacklist` | array of strings | yes (if `domain_blacklist_enabled` is `true`) | People trying to sign-up with emails from this domain will not be allowed to do so. |
+| `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. |
+| `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. |
+| `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. |
+| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
+| `gravatar_enabled` | boolean | no | Enable Gravatar |
+| `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help |
+| `help_page_support_url` | string | no | Alternate support URL for help page |
+| `home_page_url` | string | no | Redirect to this URL when not logged in |
+| `housekeeping_bitmaps_enabled` | boolean | no | Enable Git pack file bitmap creation |
+| `housekeeping_enabled` | boolean | no | Enable or disable git housekeeping |
+| `housekeeping_full_repack_period` | integer | no | Number of Git pushes after which an incremental 'git repack' is run. |
+| `housekeeping_gc_period` | integer | no | Number of Git pushes after which 'git gc' is run. |
+| `housekeeping_incremental_repack_period` | integer | no | Number of Git pushes after which an incremental 'git repack' is run. |
+| `html_emails_enabled` | boolean | no | Enable HTML emails |
+| `import_sources` | Array of strings | no | Sources to allow project import from, possible values: "github bitbucket gitlab google_code fogbugz git gitlab_project |
+| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. |
+| `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. |
+| `max_artifacts_size` | integer | no | Maximum artifacts size in MB |
+| `max_attachment_size` | integer | no | Limit attachment size in MB |
+| `max_pages_size` | integer | no | Maximum size of pages repositories in MB |
+| `metrics_enabled` | boolean | no | Enable influxDB metrics |
+| `metrics_host` | string | yes (if `metrics_enabled` is `true`) | InfluxDB host |
+| `metrics_method_call_threshold` | integer | yes (if `metrics_enabled` is `true`) | A method call is only tracked when it takes longer than the given amount of milliseconds |
+| `metrics_packet_size` | integer | yes (if `metrics_enabled` is `true`) | The amount of datapoints to send in a single UDP packet. |
+| `metrics_pool_size` | integer | yes (if `metrics_enabled` is `true`) | The amount of InfluxDB connections to keep open |
+| `metrics_port` | integer | no | The UDP port to use for connecting to InfluxDB |
+| `metrics_sample_interval` | integer | yes (if `metrics_enabled` is `true`) | The sampling interval in seconds. |
+| `metrics_timeout` | integer | yes (if `metrics_enabled` is `true`) | The amount of seconds after which InfluxDB will time out. |
+| `password_authentication_enabled_for_web` | boolean | no | Enable authentication for the web interface via a GitLab account password. Default is `true`. |
+| `password_authentication_enabled_for_git` | boolean | no | Enable authentication for Git over HTTP(S) via a GitLab account password. Default is `true`. |
+| `performance_bar_allowed_group_id` | string | no | The group that is allowed to enable the performance bar |
+| `performance_bar_enabled` | boolean | no | Allow enabling the performance bar |
+| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. |
+| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. |
+| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling. |
+| `project_export_enabled` | boolean | no | Enable project export |
+| `prometheus_metrics_enabled` | boolean | no | Enable prometheus metrics |
+| `recaptcha_enabled` | boolean | no | Enable recaptcha |
+| `recaptcha_private_key` | string | yes (if `recaptcha_enabled` is true) | Private key for recaptcha |
+| `recaptcha_site_key` | string | yes (if `recaptcha_enabled` is true) | Site key for recaptcha |
+| `repository_checks_enabled` | boolean | no | GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues. |
+| `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. |
+| `require_two_factor_authentication` | boolean | no | Require all users to setup Two-factor authentication |
+| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is null which means there is no restriction. |
+| `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys. |
+| `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up |
+| `sentry_dsn` | string | yes (if `sentry_enabled` is true) | Sentry Data Source Name |
+| `sentry_enabled` | boolean | no | Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: https://getsentry.com |
+| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes |
+| `shared_runners_enabled` | true | no | Enable shared runners for new projects |
+| `shared_runners_text` | string | no | Shared runners text |
+| `sidekiq_throttling_enabled` | boolean | no | Enable Sidekiq Job Throttling |
+| `sidekiq_throttling_factor` | decimal | yes (if `sidekiq_throttling_enabled` is true) | The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive. |
+| `sidekiq_throttling_queues` | array of strings | yes (if `sidekiq_throttling_enabled` is true) | Choose which queues you wish to throttle |
+| `sign_in_text` | string | no | Text on login page |
+| `signup_enabled` | boolean | no | Enable registration. Default is `true`. |
+| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. |
+| `two_factor_grace_period` | integer | no | Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication |
+| `unique_ips_limit_enabled` | boolean | no | Limit sign in from multiple ips |
+| `unique_ips_limit_per_user` | integer | yes (if `unique_ips_limit_enabled` is true) | Maximum number of ips per user |
+| `unique_ips_limit_time_window` | integer | yes (if `unique_ips_limit_enabled` is true) | How many seconds an IP will be counted towards the limit |
+| `usage_ping_enabled` | boolean | no | Every week GitLab will report license usage back to GitLab, Inc. |
+| `user_default_external` | boolean | no | Newly registered users will by default be external |
+| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider |
+| `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal
@@ -108,7 +166,7 @@ Example response:
"id": 1,
"default_projects_limit": 100000,
"signup_enabled": true,
- "password_authentication_enabled": true,
+ "password_authentication_enabled_for_web": true,
"gravatar_enabled": true,
"sign_in_text": "",
"created_at": "2015-06-12T15:51:55.432Z",
diff --git a/doc/api/users.md b/doc/api/users.md
index 1643c584244..478d747a50d 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -297,6 +297,7 @@ Parameters:
- `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false
+- `skip_reconfirmation` (optional) - Skip reconfirmation - true or false (default)
- `external` (optional) - Flags the user as external - true or false(default)
- `avatar` (optional) - Image file for user's avatar
@@ -410,8 +411,7 @@ GET /user
"can_create_group": true,
"can_create_project": true,
"two_factor_enabled": true,
- "external": false,
- "private_token": "dd34asd13as"
+ "external": false
}
```
diff --git a/doc/articles/index.md b/doc/articles/index.md
index 798d4cbf4ff..862fe0868a6 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 |
| :------------ | :------: | --------------: |
+| [Autoscaling GitLab Runners on AWS](runner_autoscale_aws/index.md) | Admin guide | 2017-11-24 |
| [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 |
diff --git a/doc/articles/runner_autoscale_aws/index.md b/doc/articles/runner_autoscale_aws/index.md
new file mode 100644
index 00000000000..9d4c4a57ce5
--- /dev/null
+++ b/doc/articles/runner_autoscale_aws/index.md
@@ -0,0 +1,410 @@
+---
+last_updated: 2017-11-24
+---
+
+> **[Article Type](../../development/writing_documentation.html#types-of-technical-articles):** Admin guide ||
+> **Level:** intermediary ||
+> **Author:** [Achilleas Pipinellis](https://gitlab.com/axil) ||
+> **Publication date:** 2017/11/24
+
+# Autoscaling GitLab Runner on AWS
+
+One of the biggest advantages of GitLab Runner is its ability to automatically
+spin up and down VMs to make sure your builds get processed immediately. It's a
+great feature, and if used correctly, it can be extremely useful in situations
+where you don't use your Runners 24/7 and want to have a cost-effective and
+scalable solution.
+
+## Introduction
+
+In this tutorial, we'll explore how to properly configure a GitLab Runner in
+AWS that will serve as the bastion where it will spawn new Docker machines on
+demand.
+
+In addition, we'll make use of [Amazon's EC2 Spot instances][spot] which will
+greatly reduce the costs of the Runner instances while still using quite
+powerful autoscaling machines.
+
+## Prerequisites
+
+NOTE: **Note:**
+A familiarity with Amazon Web Services (AWS) is required as this is where most
+of the configuration will take place.
+
+Your GitLab instance is going to need to talk to the Runners over the network,
+and that is something you need think about when configuring any AWS security
+groups or when setting up your DNS configuration.
+
+For example, you can keep the EC2 resources segmented away from public traffic
+in a different VPC to better strengthen your network security. Your environment
+is likely different, so consider what works best for your situation.
+
+### AWS security groups
+
+Docker Machine will attempt to use a
+[default security group](https://docs.docker.com/machine/drivers/aws/#security-group)
+with rules for port `2376`, which is required for communication with the Docker
+daemon. Instead of relying on Docker, you can create a security group with the
+rules you need and provide that in the Runner options as we will
+[see below](#the-runners-machine-section). This way, you can customize it to your
+liking ahead of time based on your networking environment.
+
+### AWS credentials
+
+You'll need an [AWS Access Key](https://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html)
+tied to a user with permission to scale (EC2) and update the cache (via S3).
+Create a new user with [policies](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-policies-for-amazon-ec2.html)
+for EC2 (AmazonEC2FullAccess) and S3 (AmazonS3FullAccess). To be more secure,
+you can disable console login for that user. Keep the tab open or copy paste the
+security credentials in an editor as we'll use them later during the
+[Runner configuration](#the-runners-machine-section).
+
+## Prepare the bastion instance
+
+The first step is to install GitLab Runner in an EC2 instance that will serve
+as the bastion that spawns new machines. This doesn't have to be a powerful
+machine since it will not run any jobs itself, a `t2.micro` instance will do.
+This machine will be a dedicated host since we need it always up and running,
+thus it will be the only standard cost.
+
+NOTE: **Note:**
+For the bastion instance, choose a distribution that both Docker and GitLab
+Runner support, for example either Ubuntu, Debian, CentOS or RHEL will work fine.
+
+Install the prerequisites:
+
+1. Log in to your server
+1. [Install GitLab Runner from the official GitLab repository](https://docs.gitlab.com/runner/install/linux-repository.html)
+1. [Install Docker](https://docs.docker.com/engine/installation/#server)
+1. [Install Docker Machine](https://docs.docker.com/machine/install-machine/)
+
+Now that the Runner is installed, it's time to register it.
+
+## Registering the GitLab Runner
+
+Before configuring the GitLab Runner, you need to first register it, so that
+it connects with your GitLab instance:
+
+1. [Obtain a Runner token](../../ci/runners/README.md)
+1. [Register the Runner](https://docs.gitlab.com/runner/register/index.html#gnu-linux)
+1. When asked the executor type, enter `docker+machine`
+
+You can now move on to the most important part, configuring the GitLab Runner.
+
+TIP: **Tip:**
+If you want every user in your instance to be able to use the autoscaled Runners,
+register the Runner as a shared one.
+
+## Configuring the GitLab Runner
+
+Now that the Runner is registered, you need to edit its configuration file and
+add the required options for the AWS machine driver.
+
+Let's first break it down to pieces.
+
+### The global section
+
+In the global section, you can define the limit of the jobs that can be run
+concurrently across all Runners (`concurrent`). This heavily depends on your
+needs, like how many users your Runners will accommodate, how much time your
+builds take, etc. You can start with something low like `10`, and increase or
+decrease its value going forward.
+
+The `check_interval` option defines how often the Runner should check GitLab
+for new jobs, in seconds.
+
+Example:
+
+```toml
+concurrent = 10
+check_interval = 0
+```
+
+[Read more](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section)
+about all the options you can use.
+
+### The `runners` section
+
+From the `[[runners]]` section, the most important part is the `executor` which
+must be set to `docker+machine`. Most of those settings are taken care of when
+you register the Runner for the first time.
+
+`limit` sets the maximum number of machines (running and idle) that this Runner
+will spawn. For more info check the [relationship between `limit`, `concurrent`
+and `IdleCount`](https://docs.gitlab.com/runner/configuration/autoscale.html#how-concurrent-limit-and-idlecount-generate-the-upper-limit-of-running-machines).
+
+Example:
+
+```toml
+[[runners]]
+ name = "gitlab-aws-autoscaler"
+ url = "<URL of your GitLab instance>"
+ token = "<Runner's token>"
+ executor = "docker+machine"
+ limit = 20
+```
+
+[Read more](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section)
+about all the options you can use under `[[runners]]`.
+
+### The `runners.docker` section
+
+In the `[runners.docker]` section you can define the default Docker image to
+be used by the child Runners if it's not defined in [`.gitlab-ci.yml`](../../ci/yaml/README.md).
+By using `privileged = true`, all Runners will be able to run
+[Docker in Docker](../../ci/docker/using_docker_build.md#use-docker-in-docker-executor)
+which is useful if you plan to build your own Docker images via GitLab CI/CD.
+
+Next, we use `disable_cache = true` to disable the Docker executor's inner
+cache mechanism since we will use the distributed cache mode as described
+in the following section.
+
+Example:
+
+```toml
+ [runners.docker]
+ image = "alpine"
+ privileged = true
+ disable_cache = true
+```
+
+[Read more](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-docker-section)
+about all the options you can use under `[runners.docker]`.
+
+### The `runners.cache` section
+
+To speed up your jobs, GitLab Runner provides a cache mechanism where selected
+directories and/or files are saved and shared between subsequent jobs.
+While not required for this setup, it is recommended to use the distributed cache
+mechanism that GitLab Runner provides. Since new instances will be created on
+demand, it is essential to have a common place where the cache is stored.
+
+In the following example, we use Amazon S3:
+
+```toml
+ [runners.cache]
+ Type = "s3"
+ ServerAddress = "s3.amazonaws.com"
+ AccessKey = "<your AWS Access Key ID>"
+ SecretKey = "<your AWS Secret Access Key>"
+ BucketName = "<the bucket where your cache should be kept>"
+ BucketLocation = "us-east-1"
+ Shared = true
+```
+
+Here's some more info to further explore the cache mechanism:
+
+- [Reference for `runners.cache`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-cache-section)
+- [Deploying and using a cache server for GitLab Runner](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching)
+- [How cache works](../../ci/yaml/README.md#cache)
+
+### The `runners.machine` section
+
+This is the most important part of the configuration and it's the one that
+tells GitLab Runner how and when to spawn new or remove old Docker Machine
+instances.
+
+We will focus on the AWS machine options, for the rest of the settings read
+about the:
+
+- [Autoscaling algorithm and the parameters it's based on](https://docs.gitlab.com/runner/configuration/autoscale.html#autoscaling-algorithm-and-parameters) - depends on the needs of your organization
+- [Off peak time configuration](https://docs.gitlab.com/runner/configuration/autoscale.html#off-peak-time-mode-configuration) - useful when there are regular time periods in your organization when no work is done, for example weekends
+
+Here's an example of the `runners.machine` section:
+
+```toml
+ [runners.machine]
+ IdleCount = 1
+ IdleTime = 1800
+ MaxBuilds = 10
+ OffPeakPeriods = [
+ "* * 0-9,18-23 * * mon-fri *",
+ "* * * * * sat,sun *"
+ ]
+ OffPeakIdleCount = 0
+ OffPeakIdleTime = 1200
+ MachineDriver = "amazonec2"
+ MachineName = "gitlab-docker-machine-%s"
+ MachineOptions = [
+ "amazonec2-access-key=XXXX",
+ "amazonec2-secret-key=XXXX",
+ "amazonec2-region=us-central-1",
+ "amazonec2-vpc-id=vpc-xxxxx",
+ "amazonec2-subnet-id=subnet-xxxxx",
+ "amazonec2-use-private-address=true",
+ "amazonec2-tags=runner-manager-name,gitlab-aws-autoscaler,gitlab,true,gitlab-runner-autoscale,true",
+ "amazonec2-security-group=docker-machine-scaler",
+ "amazonec2-instance-type=m4.2xlarge",
+ ]
+```
+
+The Docker Machine driver is set to `amazonec2` and the machine name has a
+standard prefix followed by `%s` (required) that is replaced by the ID of the
+child Runner: `gitlab-docker-machine-%s`.
+
+Now, depending on your AWS infrastructure, there are many options you can set up
+under `MachineOptions`. Below you can see the most common ones.
+
+| Machine option | Description |
+| -------------- | ----------- |
+| `amazonec2-access-key=XXXX` | The AWS access key of the user that has permissions to create EC2 instances, see [AWS credentials](#aws-credentials). |
+| `amazonec2-secret-key=XXXX` | The AWS secret key of the user that has permissions to create EC2 instances, see [AWS credentials](#aws-credentials). |
+| `amazonec2-region=eu-central-1` | The region to use when launching the instance. You can omit this entirely and the default `us-east-1` will be used. |
+| `amazonec2-vpc-id=vpc-xxxxx` | Your [VPC ID](https://docs.docker.com/machine/drivers/aws/#vpc-id) to launch the instance in. |
+| `amazonec2-subnet-id=subnet-xxxx` | The AWS VPC subnet ID. |
+| `amazonec2-use-private-address=true` | Use the private IP address of Docker Machines, but still create a public IP address. Useful to keep the traffic internal and avoid extra costs.|
+| `amazonec2-tags=runner-manager-name,gitlab-aws-autoscaler,gitlab,true,gitlab-runner-autoscale,true` | AWS extra tag key-value pairs, useful to identify the instances on the AWS console. The "Name" tag is set to the machine name by default. We set the "runner-manager-name" to match the Runner name set in `[[runners]]`, so that we can filter all the EC2 instances created by a specific manager setup. Read more about [using tags in AWS](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html). |
+| `amazonec2-security-group=docker-machine-scaler` | AWS VPC security group name, see [AWS security groups](#aws-security-groups). |
+| `amazonec2-instance-type=m4.2xlarge` | The instance type that the child Runners will run on. |
+
+TIP: **Tip:**
+Under `MachineOptions` you can add anything that the [AWS Docker Machine driver
+supports](https://docs.docker.com/machine/drivers/aws/#options). You are highly
+encouraged to read Docker's docs as your infrastructure setup may warrant
+different options to be applied.
+
+NOTE: **Note:**
+The child instances will use by default Ubuntu 16.04 unless you choose a
+different AMI ID by setting `amazonec2-ami`.
+
+NOTE: **Note:**
+If you specify `amazonec2-private-address-only=true` as one of the machine
+options, your EC2 instance won't get assigned a public IP. This is ok if your
+VPC is configured correctly with an Internet Gateway (IGW) and routing is fine,
+but it’s something to consider if you've got a more complex configuration. Read
+more in [Docker docs about VPC connectivity](https://docs.docker.com/machine/drivers/aws/#vpc-connectivity).
+
+[Read more](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-machine-section)
+about all the options you can use under `[runners.machine]`.
+
+### Getting it all together
+
+Here's the full example of `/etc/gitlab-runner/config.toml`:
+
+```toml
+concurrent = 10
+check_interval = 0
+
+[[runners]]
+ name = "gitlab-aws-autoscaler"
+ url = "<URL of your GitLab instance>"
+ token = "<Runner's token>"
+ executor = "docker+machine"
+ limit = 20
+ [runners.docker]
+ image = "alpine"
+ privileged = true
+ disable_cache = true
+ [runners.cache]
+ Type = "s3"
+ ServerAddress = "s3.amazonaws.com"
+ AccessKey = "<your AWS Access Key ID>"
+ SecretKey = "<your AWS Secret Access Key>"
+ BucketName = "<the bucket where your cache should be kept>"
+ BucketLocation = "us-east-1"
+ Shared = true
+ [runners.machine]
+ IdleCount = 1
+ IdleTime = 1800
+ MaxBuilds = 100
+ OffPeakPeriods = [
+ "* * 0-9,18-23 * * mon-fri *",
+ "* * * * * sat,sun *"
+ ]
+ OffPeakIdleCount = 0
+ OffPeakIdleTime = 1200
+ MachineDriver = "amazonec2"
+ MachineName = "gitlab-docker-machine-%s"
+ MachineOptions = [
+ "amazonec2-access-key=XXXX",
+ "amazonec2-secret-key=XXXX",
+ "amazonec2-region=us-central-1",
+ "amazonec2-vpc-id=vpc-xxxxx",
+ "amazonec2-subnet-id=subnet-xxxxx",
+ "amazonec2-use-private-address=true",
+ "amazonec2-tags=runner-manager-name,gitlab-aws-autoscaler,gitlab,true,gitlab-runner-autoscale,true",
+ "amazonec2-security-group=docker-machine-scaler",
+ "amazonec2-instance-type=m4.2xlarge",
+ ]
+```
+
+## Cutting down costs with Amazon EC2 Spot instances
+
+As [described by][spot] Amazon:
+
+>
+Amazon EC2 Spot instances allow you to bid on spare Amazon EC2 computing capacity.
+Since Spot instances are often available at a discount compared to On-Demand
+pricing, you can significantly reduce the cost of running your applications,
+grow your application’s compute capacity and throughput for the same budget,
+and enable new types of cloud computing applications.
+
+In addition to the [`runners.machine`](#the-runners-machine-section) options
+you picked above, in `/etc/gitlab-runner/config.toml` under the `MachineOptions`
+section, add the following:
+
+```toml
+ MachineOptions = [
+ "amazonec2-request-spot-instance=true",
+ "amazonec2-spot-price=0.03",
+ "amazonec2-block-duration-minutes=60"
+ ]
+```
+
+With this configuration, Docker Machines are created on Spot instances with a
+maximum bid price of $0.03 per hour and the duration of the Spot instance is
+capped at 60 minutes. The `0.03` number mentioned above is just an example, so
+be sure to check on the current pricing based on the region you picked.
+
+To learn more about Amazon EC2 Spot instances, visit the following links:
+
+- https://aws.amazon.com/ec2/spot/
+- https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html
+- https://aws.amazon.com/blogs/aws/focusing-on-spot-instances-lets-talk-about-best-practices/
+
+### Caveats of Spot instances
+
+While Spot instances is a great way to use unused resources and minimize the
+costs of your infrastructure, you must be aware of the implications.
+
+Running CI jobs on Spot instances may increase the failure rates because of the
+Spot instances pricing model. If the price exceeds your bid, the existing Spot
+instances will be immediately terminated and all your jobs on that host will fail.
+
+As a consequence, the auto-scale Runner would fail to create new machines while
+it will continue to request new instances. This eventually will make 60 requests
+and then AWS won't accept any more. Then once the Spot price is acceptable, you
+are locked out for a bit because the call amount limit is exceeded.
+
+If you encounter that case, you can use the following command in the bastion
+machine to see the Docker Machines state:
+
+```sh
+docker-machine ls -q --filter state=Error --format "{{.NAME}}"
+```
+
+NOTE: **Note:**
+There are some issues regarding making GitLab Runner gracefully handle Spot
+price changes, and there are reports of `docker-machine` attempting to
+continually remove a Docker Machine. GitLab has provided patches for both cases
+in the upstream project. For more information, see issues
+[#2771](https://gitlab.com/gitlab-org/gitlab-runner/issues/2771) and
+[#2772](https://gitlab.com/gitlab-org/gitlab-runner/issues/2772).
+
+## Conclusion
+
+In this guide we learned how to install and configure a GitLab Runner in
+autoscale mode on AWS.
+
+Using the autoscale feature of GitLab Runner can save you both time and money.
+Using the Spot instances that AWS provides can save you even more, but you must
+be aware of the implications. As long as your bid is high enough, there shouldn't
+be an issue.
+
+You can read the following use cases from which this tutorial was (heavily)
+influenced:
+
+- [HumanGeo - Scaling GitLab CI](http://blog.thehumangeo.com/gitlab-autoscale-runners.html)
+- [subtrakt Health - Autoscale GitLab CI Runners and save 90% on EC2 costs](https://substrakthealth.com/news/gitlab-ci-cost-savings/)
+
+[spot]: https://aws.amazon.com/ec2/spot/
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 5cfd82de381..12404eddbe2 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Continuous Integration (GitLab CI)
![Pipeline graph](img/cicd_pipeline_infograph.png)
@@ -42,7 +46,7 @@ digging into specific reference guides.
- **The permissions model** - Learn about the access levels a user can have for
performing certain CI actions
- [User permissions](../user/permissions.md#gitlab-ci)
- - [Jobs permissions](../user/permissions.md#jobs-permissions)
+ - [Job permissions](../user/permissions.md#job-permissions)
## Auto DevOps
diff --git a/doc/ci/docker/README.md b/doc/ci/docker/README.md
index 99669a9272a..b0e01d74f7e 100644
--- a/doc/ci/docker/README.md
+++ b/doc/ci/docker/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Docker integration
- [Using Docker Images](using_docker_images.md)
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 4586caa457d..0a2419b7ed2 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -31,12 +31,12 @@ There are three methods to enable the use of `docker build` and `docker run` dur
The simplest approach is to install GitLab Runner in `shell` execution mode.
GitLab Runner then executes job scripts as the `gitlab-runner` user.
-1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
+1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner/#installation).
1. During GitLab Runner installation select `shell` as method of executing job scripts or use command:
```bash
- sudo gitlab-ci-multi-runner register -n \
+ sudo gitlab-runner register -n \
--url https://gitlab.com/ \
--registration-token REGISTRATION_TOKEN \
--executor shell \
@@ -93,7 +93,7 @@ In order to do that, follow the steps:
mode:
```bash
- sudo gitlab-ci-multi-runner register -n \
+ sudo gitlab-runner register -n \
--url https://gitlab.com/ \
--registration-token REGISTRATION_TOKEN \
--executor docker \
@@ -178,7 +178,7 @@ In order to do that, follow the steps:
1. Register GitLab Runner from the command line to use `docker` and share `/var/run/docker.sock`:
```bash
- sudo gitlab-ci-multi-runner register -n \
+ sudo gitlab-runner register -n \
--url https://gitlab.com/ \
--registration-token REGISTRATION_TOKEN \
--executor docker \
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index fa823ea4721..ecb8f15c851 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -327,10 +327,6 @@ means, that when starting the container without additional options, it will run
the database's process, while Runner expects that the image will have no
entrypoint or at least will start with a shell as its entrypoint.
-Previously we would need to create our own image based on the
-`super/sql:experimental` image, set the entrypoint to a shell, and then use
-it in job's configuration, e.g.:
-
Before the new extended Docker configuration options, you would need to create
your own image based on the `super/sql:experimental` image, set the entrypoint
to a shell and then use it in job's configuration, like:
@@ -505,8 +501,8 @@ First start with creating a file named `build_script`:
```bash
cat <<EOF > build_script
-git clone https://gitlab.com/gitlab-org/gitlab-ci-multi-runner.git /builds/gitlab-org/gitlab-ci-multi-runner
-cd /builds/gitlab-org/gitlab-ci-multi-runner
+git clone https://gitlab.com/gitlab-org/gitlab-runner.git /builds/gitlab-org/gitlab-runner
+cd /builds/gitlab-org/gitlab-runner
make
EOF
```
diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md
index b8f9988e3ef..7aa7de97c43 100644
--- a/doc/ci/enable_or_disable_ci.md
+++ b/doc/ci/enable_or_disable_ci.md
@@ -1,4 +1,4 @@
-## Enable or disable GitLab CI/CD
+# How to enable or disable GitLab CI/CD
To effectively use GitLab CI/CD, you need a valid [`.gitlab-ci.yml`](yaml/README.md)
file present at the root directory of your project and a
@@ -21,7 +21,7 @@ individually under each project's settings, or site-wide by modifying the
settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations
respectively.
-### Per-project user setting
+## Per-project user setting
The setting to enable or disable GitLab CI/CD can be found under your project's
**Settings > General > Permissions**. Choose one of "Disabled", "Only team members"
@@ -29,7 +29,7 @@ or "Everyone with access" and hit **Save changes** for the settings to take effe
![Sharing & Permissions settings](../user/project/settings/img/sharing_and_permissions_settings.png)
-### Site-wide admin setting
+## Site-wide admin setting
You can disable GitLab CI/CD site-wide, by modifying the settings in `gitlab.yml`
and `gitlab.rb` for source and Omnibus installations respectively.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index f094546c3bd..d05b4db953a 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab CI Examples
A collection of `.gitlab-ci.yml` files is maintained at the [GitLab CI Yml project][gitlab-ci-templates].
diff --git a/doc/ci/examples/deployment/composer-npm-deploy.md b/doc/ci/examples/deployment/composer-npm-deploy.md
index b9f0485290e..bed379b0254 100644
--- a/doc/ci/examples/deployment/composer-npm-deploy.md
+++ b/doc/ci/examples/deployment/composer-npm-deploy.md
@@ -1,4 +1,4 @@
-## Running Composer and NPM scripts with deployment via SCP
+# Running Composer and NPM scripts with deployment via SCP in GitLab CI/CD
This guide covers the building dependencies of a PHP project while compiling assets via an NPM script.
@@ -39,13 +39,13 @@ In this particular case, the `npm deploy` script is a Gulp script that does the
All these operations will put all files into a `build` folder, which is ready to be deployed to a live server.
-### How to transfer files to a live server?
+## How to transfer files to a live server
You have multiple options: rsync, scp, sftp and so on. For now, we will use scp.
To make this work, you need to add a GitLab Secret Variable (accessible on _gitlab.example/your-project-name/variables_). That variable will be called `STAGING_PRIVATE_KEY` and it's the **private** ssh key of your server.
-#### Security tip
+### Security tip
Create a user that has access **only** to the folder that needs to be updated!
@@ -69,7 +69,7 @@ In order, this means that:
And this is basically all you need in the `before_script` section.
-## How to deploy things?
+## How to deploy things
As we stated above, we need to deploy the `build` folder from the docker image to our server. To do so, we create a new job:
@@ -88,7 +88,7 @@ stage_deploy:
- ssh -p22 server_user@server_host "rm -rf htdocs/wp-content/themes/_old"
```
-### What's going on here?
+Here's the breakdown:
1. `only:dev` means that this build will run only when something is pushed to the `dev` branch. You can remove this block completely and have everything be ran on every push (but probably this is something you don't want)
2. `ssh-add ...` we will add that private key you added on the web UI to the docker container
@@ -99,7 +99,7 @@ stage_deploy:
What's the deal with the artifacts? We just tell GitLab CI to keep the `build` directory (later on, you can download that as needed).
-#### Why we do it this way?
+### Why we do it this way
If you're using this only for stage server, you could do this in two steps:
@@ -112,7 +112,7 @@ The problem is that there will be a small period of time when you won't have the
So we use so many steps because we want to make sure that at any given time we have a functional app in place.
-## Where to go next?
+## Where to go next
Since this was a WordPress project, I gave real life code snippets. Some ideas you can pursuit:
diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md
index f2dd12b67d3..6768a2e012f 100644
--- a/doc/ci/examples/php.md
+++ b/doc/ci/examples/php.md
@@ -267,10 +267,10 @@ terminal execute:
```bash
# Check using docker executor
-gitlab-ci-multi-runner exec docker test:app
+gitlab-runner exec docker test:app
# Check using shell executor
-gitlab-ci-multi-runner exec shell test:app
+gitlab-runner exec shell test:app
```
## Example project
diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
index 73aebaf6d7f..a6ed1c54e16 100644
--- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
@@ -1,10 +1,13 @@
-## Test and Deploy a python application
+# Test and Deploy a python application with GitLab CI/CD
+
This example will guide you how to run tests in your Python application and deploy it automatically as Heroku application.
-You can checkout the example [source](https://gitlab.com/ayufan/python-getting-started) and check [CI status](https://gitlab.com/ayufan/python-getting-started/builds?scope=all).
+You can checkout the [example source](https://gitlab.com/ayufan/python-getting-started).
+
+## Configure project
-### Configure project
This is what the `.gitlab-ci.yml` file looks like for this project:
+
```yaml
test:
script:
@@ -41,23 +44,27 @@ This project has three jobs:
2. `staging` - used to automatically deploy staging environment every push to `master` branch
3. `production` - used to automatically deploy production environmnet for every created tag
-### Store API keys
+## Store API keys
+
You'll need to create two variables in `Project > Variables`:
1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account).
-### Create Heroku application
+## Create Heroku application
+
For each of your environments, you'll need to create a new Heroku application.
You can do this through the [Dashboard](https://dashboard.heroku.com/).
-### Create runner
+## Create Runner
+
First install [Docker Engine](https://docs.docker.com/installation/).
-To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner).
+To build this project you also need to have [GitLab Runner](https://docs.gitlab.com/runner).
You can use public runners available on `gitlab.com`, but you can register your own:
+
```
-gitlab-ci-multi-runner register \
+gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
index 6fa64a67e82..10fd2616fab 100644
--- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -1,10 +1,13 @@
-## Test and Deploy a ruby application
+# Test and Deploy a ruby application with GitLab CI/CD
+
This example will guide you how to run tests in your Ruby on Rails application and deploy it automatically as Heroku application.
You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://gitlab.com/ayufan/ruby-getting-started/builds?scope=all).
-### Configure project
+## Configure the project
+
This is what the `.gitlab-ci.yml` file looks like for this project:
+
```yaml
test:
script:
@@ -36,23 +39,28 @@ This project has three jobs:
2. `staging` - used to automatically deploy staging environment every push to `master` branch
3. `production` - used to automatically deploy production environment for every created tag
-### Store API keys
-You'll need to create two variables in `Project > Variables`:
+## Store API keys
+
+You'll need to create two variables in your project's **Settings > CI/CD > Variables**:
+
1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account).
-### Create Heroku application
+## Create Heroku application
+
For each of your environments, you'll need to create a new Heroku application.
You can do this through the [Dashboard](https://dashboard.heroku.com/).
-### Create runner
+## Create Runner
+
First install [Docker Engine](https://docs.docker.com/installation/).
To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner).
You can use public runners available on `gitlab.com`, but you can register your own:
+
```
-gitlab-ci-multi-runner register \
+gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
@@ -62,6 +70,6 @@ gitlab-ci-multi-runner register \
--docker-postgres latest
```
-With the command above, you create a runner that uses [ruby:2.2](https://hub.docker.com/r/_/ruby/) image and uses [postgres](https://hub.docker.com/r/_/postgres/) database.
+With the command above, you create a Runner that uses [ruby:2.2](https://hub.docker.com/r/_/ruby/) image and uses [postgres](https://hub.docker.com/r/_/postgres/) database.
To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password.
diff --git a/doc/ci/examples/test-clojure-application.md b/doc/ci/examples/test-clojure-application.md
index 56b746ce025..3b1026d174f 100644
--- a/doc/ci/examples/test-clojure-application.md
+++ b/doc/ci/examples/test-clojure-application.md
@@ -1,10 +1,10 @@
-## Test a Clojure application
+# Test a Clojure application with GitLab CI/CD
This example will guide you how to run tests in your Clojure application.
You can checkout the example [source](https://gitlab.com/dzaporozhets/clojure-web-application) and check [CI status](https://gitlab.com/dzaporozhets/clojure-web-application/builds?scope=all).
-### Configure project
+## Configure the project
This is what the `.gitlab-ci.yml` file looks like for this project:
@@ -23,13 +23,13 @@ before_script:
- lein deps
- lein migratus migrate
-test:
- script:
+test:
+ script:
- lein test
```
-In before script we install JRE and [Leiningen](http://leiningen.org/).
-Sample project uses [migratus](https://github.com/yogthos/migratus) library to manage database migrations.
+In before script we install JRE and [Leiningen](http://leiningen.org/).
+Sample project uses [migratus](https://github.com/yogthos/migratus) library to manage database migrations.
So we added database migration as last step of `before_script` section
You can use public runners available on `gitlab.com` for testing your application with such configuration.
diff --git a/doc/ci/examples/test-phoenix-application.md b/doc/ci/examples/test-phoenix-application.md
index 150698ca04b..f6c81b076bc 100644
--- a/doc/ci/examples/test-phoenix-application.md
+++ b/doc/ci/examples/test-phoenix-application.md
@@ -1,9 +1,9 @@
-## Test a Phoenix application
+# Test a Phoenix application with GitLab CI/CD
This example demonstrates the integration of Gitlab CI with Phoenix, Elixir and
Postgres.
-### Add `.gitlab-ci.yml` file to project
+## Add `.gitlab-ci.yml` to project
The following `.gitlab-ci.yml` should be added in the root of your
repository to trigger CI:
@@ -36,7 +36,7 @@ run your migrations.
Finally, the test `script` will run your tests.
-### Update the Config Settings
+## Update the Config Settings
In `config/test.exs`, update the database hostname:
@@ -45,12 +45,12 @@ config :my_app, MyApp.Repo,
hostname: if(System.get_env("CI"), do: "postgres", else: "localhost"),
```
-### Add the Migrations Folder
+## Add the Migrations Folder
If you do not have any migrations yet, you will need to create an empty
`.gitkeep` file in `priv/repo/migrations`.
-### Sources
+## Sources
- https://medium.com/@nahtnam/using-phoenix-on-gitlab-ci-5a51eec81142
- https://davejlong.com/ci-with-phoenix-and-gitlab/
diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md
index 36c6e153d95..286f3dee665 100644
--- a/doc/ci/git_submodules.md
+++ b/doc/ci/git_submodules.md
@@ -8,7 +8,7 @@
with the use of [SSH keys](ssh_keys/README.md).
- With GitLab 8.12 onward, your permissions are used to evaluate what a CI job
can access. More information about how this system works can be found in the
- [Jobs permissions model](../user/permissions.md#jobs-permissions).
+ [Jobs permissions model](../user/permissions.md#job-permissions).
- The HTTP(S) Git protocol [must be enabled][gitpro] in your GitLab instance.
## Configuring the `.gitmodules` file
@@ -61,7 +61,7 @@ correctly with your CI jobs:
1. First, make sure you have used [relative URLs](#configuring-the-gitmodules-file)
for the submodules located in the same GitLab server.
-1. Next, if you are using `gitlab-ci-multi-runner` v1.10+, you can set the
+1. Next, if you are using `gitlab-runner` v1.10+, you can set the
`GIT_SUBMODULE_STRATEGY` variable to either `normal` or `recursive` to tell
the runner to fetch your submodules before the job:
```yaml
@@ -71,7 +71,7 @@ correctly with your CI jobs:
See the [`.gitlab-ci.yml` reference](yaml/README.md#git-submodule-strategy)
for more details about `GIT_SUBMODULE_STRATEGY`.
-1. If you are using an older version of `gitlab-ci-multi-runner`, then use
+1. If you are using an older version of `gitlab-runner`, then use
`git submodule sync/update` in `before_script`:
```yaml
diff --git a/doc/ci/permissions/README.md b/doc/ci/permissions/README.md
index 42eb59f84c8..80d8e46f29c 100644
--- a/doc/ci/permissions/README.md
+++ b/doc/ci/permissions/README.md
@@ -1,3 +1 @@
-# Users Permissions
-
This document was moved to [user/permissions.md](../../user/permissions.md#gitlab-ci).
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 2d56b2540ef..f621bf07251 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -1,4 +1,4 @@
-# Getting started with GitLab CI
+# Getting started with GitLab CI/CD
>**Note:** Starting from version 8.0, GitLab [Continuous Integration][ci] (CI)
is fully integrated into GitLab itself and is [enabled] by default on all
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 8b51d112a2c..df66810a838 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -1,4 +1,4 @@
-# Runners
+# Configuring GitLab Runners
In GitLab CI, Runners run the code defined in [`.gitlab-ci.yml`](../yaml/README.md).
They are isolated (virtual) machines that pick up jobs through the coordinator
diff --git a/doc/ci/services/README.md b/doc/ci/services/README.md
index 4b79461d55c..d94b472b768 100644
--- a/doc/ci/services/README.md
+++ b/doc/ci/services/README.md
@@ -1,4 +1,8 @@
-## GitLab CI Services
+---
+comments: false
+---
+
+# GitLab CI Services
GitLab CI uses the `services` keyword to define what docker containers should
be linked with your base image. Below is a list of examples you may use.
diff --git a/doc/ci/services/docker-services.md b/doc/ci/services/docker-services.md
index df36ebaf7d4..787c5e462e4 100644
--- a/doc/ci/services/docker-services.md
+++ b/doc/ci/services/docker-services.md
@@ -1,5 +1,9 @@
-## GitLab CI Services
+---
+comments: false
+---
-+ [Using MySQL](mysql.md)
-+ [Using PostgreSQL](postgres.md)
-+ [Using Redis](redis.md)
+# GitLab CI Services
+
+- [Using MySQL](mysql.md)
+- [Using PostgreSQL](postgres.md)
+- [Using Redis](redis.md)
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 17839cbaef1..a9e6bda9916 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -1,4 +1,4 @@
-# Variables
+# GitLab CI/CD Variables
When receiving a job from GitLab CI, the [Runner] prepares the build environment.
It starts by setting a list of **predefined variables** (environment variables)
@@ -43,6 +43,7 @@ future GitLab releases.**
| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
| **CI_CONFIG_PATH** | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` |
| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
+| **CI_DISPOSABLE_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. |
| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job |
| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
| **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job |
@@ -65,6 +66,7 @@ future GitLab releases.**
| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
| **CI_PROJECT_PATH_SLUG** | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
+| **CI_PROJECT_VISIBILITY** | 10.3 | all | The project visibility (internal, private, public) |
| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
| **CI_REGISTRY_PASSWORD** | 9.0 | all | The password to use to push containers to the GitLab Container Registry |
@@ -73,6 +75,7 @@ future GitLab releases.**
| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs |
| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs |
| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs |
+| **CI_SHARED_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. |
| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job |
| **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a job |
| **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 38bd0450a09..f40d2c5e347 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -95,6 +95,12 @@ be an array or a multi-line string.
`after_script` is used to define the command that will be run after for all
jobs. This has to be an array or a multi-line string.
+> **Note:**
+The `before_script` and the main `script` are concatenated and run in a single context/container.
+The `after_script` is run separately, so depending on the executor, changes done
+outside of the working tree might not be visible, e.g. software installed in the
+`before_script`.
+
### stages
`stages` is used to define stages that can be used by jobs.
@@ -118,7 +124,7 @@ stages:
1. First, all jobs of `build` are executed in parallel.
1. If all jobs of `build` succeed, the `test` jobs are executed in parallel.
1. If all jobs of `test` succeed, the `deploy` jobs are executed in parallel.
-1. If all jobs of `deploy` succeed, the commit is marked as `success`.
+1. If all jobs of `deploy` succeed, the commit is marked as `passed`.
1. If any of the previous jobs fails, the commit is marked as `failed` and no
jobs of further stage are executed.
diff --git a/doc/development/README.md b/doc/development/README.md
index b648c7ce086..6892838be7f 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -1,75 +1,100 @@
-# Development
+---
+comments: false
+---
-## Outside of docs
+# GitLab development guides
-- [CONTRIBUTING.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) main contributing guide
-- [PROCESS.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) contributing process
-- [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md) to install a development version
+## Get started!
-## Styleguides
-
-- [API styleguide](api_styleguide.md) Use this styleguide if you are
- contributing to the API.
-- [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are
- contributing to documentation.
-- [Writing documentation](writing_documentation.md)
- - [Distinction between general documentation and technical articles](writing_documentation.md#distinction-between-general-documentation-and-technical-articles)
-- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
-- [Testing standards and style guidelines](testing.md)
-- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
-- [Frontend guidelines](fe_guide/index.md)
-- [SQL guidelines](sql.md) for working with SQL queries
-- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
-- [`Gemfile` guidelines](gemfile.md)
+- Setup GitLab's development environment with [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md)
+- [GitLab contributing guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md)
+- [Architecture](architecture.md) of GitLab
+- [Rake tasks](rake_tasks.md) for development
-## Process
+## Processes
+- [GitLab core team & GitLab Inc. contribution process](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md)
- [Generate a changelog entry with `bin/changelog`](changelog.md)
-- [Limit conflicts with EE when developing on CE](limit_ee_conflicts.md)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
-- [Merge request performance guidelines](merge_request_performance_guidelines.md)
- for ensuring merge requests do not negatively impact GitLab performance
+- [Limit conflicts with EE when developing on CE](limit_ee_conflicts.md)
-## Backend howtos
+## UX and frontend guides
-- [Architecture](architecture.md) of GitLab
+- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
+- [Frontend guidelines](fe_guide/index.md)
+- [Emoji guide](fe_guide/emojis.md)
+
+## Backend guides
+
+- [API styleguide](api_styleguide.md) Use this styleguide if you are
+ contributing to the API.
+- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
+- [Working with Gitaly](gitaly.md)
+- [Manage feature flags](feature_flags.md)
+- [View sent emails or preview mailers](emails.md)
+- [Shell commands](shell_commands.md) in the GitLab codebase
+- [`Gemfile` guidelines](gemfile.md)
+- [Sidekiq debugging](sidekiq_debugging.md)
- [Gotchas](gotchas.md) to avoid
+- [Issue and merge requests state models](object_state_models.md)
- [How to dump production data to staging](db_dump.md)
+- [Working with the GitHub importer](github_importer.md)
+
+## Performance guides
+
- [Instrumentation](instrumentation.md)
- [Performance guidelines](performance.md)
-- [Rake tasks](rake_tasks.md) for development
-- [Shell commands](shell_commands.md) in the GitLab codebase
-- [Sidekiq debugging](sidekiq_debugging.md)
-- [Object state models](object_state_models.md)
-- [Building a package for testing purposes](build_test_package.md)
-- [Manage feature flags](feature_flags.md)
-- [View sent emails or preview mailers](emails.md)
-- [Working with Gitaly](gitaly.md)
+- [Merge request performance guidelines](merge_request_performance_guidelines.md)
+ for ensuring merge requests do not negatively impact GitLab performance
+
+## Databases guides
-## Databases
+### Migrations
-- [Merge Request Checklist](database_merge_request_checklist.md)
- [What requires downtime?](what_requires_downtime.md)
+- [SQL guidelines](sql.md) for working with SQL queries
+- [Migrations style guide](migration_style_guide.md) for creating safe SQL migrations
+- [Post deployment migrations](post_deployment_migrations.md)
+- [Background migrations](background_migrations.md)
+- [Swapping tables](swapping_tables.md)
+
+### Best practices
+
+- [Merge Request checklist](database_merge_request_checklist.md)
- [Adding database indexes](adding_database_indexes.md)
-- [Post Deployment Migrations](post_deployment_migrations.md)
-- [Foreign Keys & Associations](foreign_keys.md)
-- [Serializing Data](serializing_data.md)
-- [Polymorphic Associations](polymorphic_associations.md)
-- [Single Table Inheritance](single_table_inheritance.md)
-- [Background Migrations](background_migrations.md)
-- [Storing SHA1 Hashes As Binary](sha1_as_binary.md)
-- [Iterating Tables In Batches](iterating_tables_in_batches.md)
-- [Ordering Table Columns](ordering_table_columns.md)
-- [Verifying Database Capabilities](verifying_database_capabilities.md)
-- [Hash Indexes](hash_indexes.md)
-- [Swapping Tables](swapping_tables.md)
-
-## Internationalization (i18n)
+- [Foreign keys & associations](foreign_keys.md)
+- [Single table inheritance](single_table_inheritance.md)
+- [Polymorphic associations](polymorphic_associations.md)
+- [Serializing data](serializing_data.md)
+- [Hash indexes](hash_indexes.md)
+- [Storing SHA1 hashes as binary](sha1_as_binary.md)
+- [Iterating tables in batches](iterating_tables_in_batches.md)
+- [Ordering table columns](ordering_table_columns.md)
+- [Verifying database capabilities](verifying_database_capabilities.md)
+- [Database Debugging and Troubleshooting](database_debugging.md)
+
+## Testing guides
+
+- [Testing standards and style guidelines](testing_guide/index.md)
+- [Frontend testing standards and style guidelines](testing_guide/frontend_testing.md)
+
+## Documentation guides
+
+- [Documentation styleguide](doc_styleguide.md): Use this styleguide if you are
+ contributing to the documentation.
+- [Writing documentation](writing_documentation.md)
+ - [Distinction between general documentation and technical articles](writing_documentation.md#distinction-between-general-documentation-and-technical-articles)
+
+## Internationalization (i18n) guides
- [Introduction](i18n/index.md)
- [Externalization](i18n/externalization.md)
- [Translation](i18n/translation.md)
+## Build guides
+
+- [Building a package for testing purposes](build_test_package.md)
+
## Compliance
- [Licensing](licensing.md) for ensuring license compliance
diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md
new file mode 100644
index 00000000000..50eb8005b44
--- /dev/null
+++ b/doc/development/database_debugging.md
@@ -0,0 +1,55 @@
+# Database Debugging and Troubleshooting
+
+This section is to help give some copy-pasta you can use as a reference when you
+run into some head-banging database problems.
+
+An easy first step is to search for your error in Slack or google "GitLab <my error>".
+
+---
+
+Available `RAILS_ENV`
+
+ - `production` (generally not for your main GDK db, but you may need this for e.g. omnibus)
+ - `development` (this is your main GDK db)
+ - `test` (used for tests like rspec and spinach)
+
+
+## Nuke everything and start over
+
+If you just want to delete everything and start over with an empty DB (~1 minute):
+
+ - `bundle exec rake db:reset RAILS_ENV=development`
+
+If you just want to delete everything and start over with dummy data (~40 minutes). This also does `db:reset` and runs DB-specific migrations:
+
+ - `bundle exec rake dev:setup RAILS_ENV=development`
+
+If your test DB is giving you problems, it is safe to nuke it because it doesn't contain important data:
+
+ - `bundle exec rake db:reset RAILS_ENV=test`
+
+## Migration wrangling
+
+ - `bundle exec rake db:migrate RAILS_ENV=development`: Execute any pending migrations that you may have picked up from a MR
+ - `bundle exec rake db:migrate:status RAILS_ENV=development`: Check if all migrations are `up` or `down`
+ - `bundle exec rake db:migrate:down VERSION=20170926203418 RAILS_ENV=development`: Tear down a migration
+ - `bundle exec rake db:migrate:up VERSION=20170926203418 RAILS_ENV=development`: Setup a migration
+ - `bundle exec rake db:migrate:redo VERSION=20170926203418 RAILS_ENV=development`: Re-run a specific migration
+
+
+## Manually access the database
+
+Access the database via one of these commands (they all get you to the same place)
+
+```
+gdk psql -d gitlabhq_development
+bundle exec rails dbconsole RAILS_ENV=development
+bundle exec rails db RAILS_ENV=development
+```
+
+ - `\q`: Quit/exit
+ - `\dt`: List all tables
+ - `\d+ issues`: List columns for `issues` table
+ - `CREATE TABLE board_labels();`: Create a table called `board_labels`
+ - `SELECT * FROM schema_migrations WHERE version = '20170926203418';`: Check if a migration was run
+ - `DELETE FROM schema_migrations WHERE version = '20170926203418';`: Manually remove a migration
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 798f40eef3d..aaa7032cadb 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -303,10 +303,10 @@ GitLab.com or http://docs.gitlab.com. Make sure this is discussed with the
Documentation team beforehand.
If you indeed need to change a document's location, do NOT remove the old
-document, but rather put a text in it that points to the new location, like:
+document, but rather replace all of its contents with a new line:
```
-This document was moved to [path/to/new_doc.md](path/to/new_doc.md).
+This document was moved to [another location](path/to/new_doc.md).
```
where `path/to/new_doc.md` is the relative path to the root directory `doc/`.
@@ -320,7 +320,7 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
1. Replace the contents of `doc/workflow/lfs/lfs_administration.md` with:
```
- This document was moved to [administration/lfs.md](../../administration/lfs.md).
+ This document was moved to [another location](../../administration/lfs.md).
```
1. Find and replace any occurrences of the old location with the new one.
@@ -459,11 +459,11 @@ Rendered example:
### cURL commands
- Use `https://gitlab.example.com/api/v4/` as an endpoint.
-- Wherever needed use this private token: `9koXpg98eAheJpvBs5tK`.
+- Wherever needed use this personal access token: `9koXpg98eAheJpvBs5tK`.
- Always put the request first. `GET` is the default so you don't have to
include it.
- Use double quotes to the URL when it includes additional parameters.
-- Prefer to use examples using the private token and don't pass data of
+- Prefer to use examples using the personal access token and don't pass data of
username and password.
| Methods | Description |
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
new file mode 100644
index 00000000000..932a44f65e4
--- /dev/null
+++ b/doc/development/ee_features.md
@@ -0,0 +1,382 @@
+# Guidelines for implementing Enterprise Edition feature
+
+- **Write the code and the tests.**: As with any code, EE features should have
+ good test coverage to prevent regressions.
+- **Write documentation.**: Add documentation to the `doc/` directory. Describe
+ the feature and include screenshots, if applicable.
+- **Submit a MR to the `www-gitlab-com` project.**: Add the new feature to the
+ [EE features list][ee-features-list].
+
+## Act as CE when unlicensed
+
+Since the implementation of [GitLab CE features to work with unlicensed EE instance][ee-as-ce]
+GitLab Enterprise Edition should work like GitLab Community Edition
+when no license is active. So EE features always should be guarded by
+`project.feature_available?` or `group.feature_available?` (or
+`License.feature_available?` if it is a system-wide feature).
+
+CE specs should remain untouched as much as possible and extra specs
+should be added for EE. Licensed features can be stubbed using the
+spec helper `stub_licensed_features` in `EE::LicenseHelpers`.
+
+[ee-as-ce]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2500
+
+## Separation of EE code
+
+We want a [single code base][] eventually, but before we reach the goal,
+we still need to merge changes from GitLab CE to EE. To help us get there,
+we should make sure that we no longer edit CE files in place in order to
+implement EE features.
+
+Instead, all EE codes should be put inside the `ee/` top-level directory, and
+tests should be put inside `spec/ee/`. We don't use `ee/spec` for now due to
+technical limitation. The rest of codes should be as close as to the CE files.
+
+[single code base]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2952#note_41016454
+
+### EE-only features
+
+If the feature being developed is not present in any form in CE, we don't
+need to put the codes under `EE` namespace. For example, an EE model could
+go into: `ee/app/models/awesome.rb` using `Awesome` as the class name. This
+is applied not only to models. Here's a list of other examples:
+
+- `ee/app/controllers/foos_controller.rb`
+- `ee/app/finders/foos_finder.rb`
+- `ee/app/helpers/foos_helper.rb`
+- `ee/app/mailers/foos_mailer.rb`
+- `ee/app/models/foo.rb`
+- `ee/app/policies/foo_policy.rb`
+- `ee/app/serializers/foo_entity.rb`
+- `ee/app/serializers/foo_serializer.rb`
+- `ee/app/services/foo/create_service.rb`
+- `ee/app/validators/foo_attr_validator.rb`
+- `ee/app/workers/foo_worker.rb`
+
+### EE features based on CE features
+
+For features that build on existing CE features, write a module in the
+`EE` namespace and `prepend` it in the CE class. This makes conflicts
+less likely to happen during CE to EE merges because only one line is
+added to the CE class - the `prepend` line.
+
+Since the module would require an `EE` namespace, the file should also be
+put in an `ee/` sub-directory. For example, we want to extend the user model
+in EE, so we have a module called `::EE::User` put inside
+`ee/app/models/ee/user.rb`.
+
+This is also not just applied to models. Here's a list of other examples:
+
+- `ee/app/controllers/ee/foos_controller.rb`
+- `ee/app/finders/ee/foos_finder.rb`
+- `ee/app/helpers/ee/foos_helper.rb`
+- `ee/app/mailers/ee/foos_mailer.rb`
+- `ee/app/models/ee/foo.rb`
+- `ee/app/policies/ee/foo_policy.rb`
+- `ee/app/serializers/ee/foo_entity.rb`
+- `ee/app/serializers/ee/foo_serializer.rb`
+- `ee/app/services/ee/foo/create_service.rb`
+- `ee/app/validators/ee/foo_attr_validator.rb`
+- `ee/app/workers/ee/foo_worker.rb`
+
+#### Overriding CE methods
+
+To override a method present in the CE codebase, use `prepend`. It
+lets you override a method in a class with a method from a module, while
+still having access the class's implementation with `super`.
+
+There are a few gotchas with it:
+
+- you should always add a `raise NotImplementedError unless defined?(super)`
+ guard clause in the "overrider" method to ensure that if the method gets
+ renamed in CE, the EE override won't be silently forgotten.
+- when the "overrider" would add a line in the middle of the CE
+ implementation, you should refactor the CE method and split it in
+ smaller methods. Or create a "hook" method that is empty in CE,
+ and with the EE-specific implementation in EE.
+- when the original implementation contains a guard clause (e.g.
+ `return unless condition`), we cannot easily extend the behaviour by
+ overriding the method, because we can't know when the overridden method
+ (i.e. calling `super` in the overriding method) would want to stop early.
+ In this case, we shouldn't just override it, but update the original method
+ to make it call the other method we want to extend, like a [template method
+ pattern](https://en.wikipedia.org/wiki/Template_method_pattern).
+ For example, given this base:
+ ``` ruby
+ class Base
+ def execute
+ return unless enabled?
+
+ # ...
+ # ...
+ end
+ end
+ ```
+ Instead of just overriding `Base#execute`, we should update it and extract
+ the behaviour into another method:
+ ``` ruby
+ class Base
+ def execute
+ return unless enabled?
+
+ do_something
+ end
+
+ private
+
+ def do_something
+ # ...
+ # ...
+ end
+ end
+ ```
+ Then we're free to override that `do_something` without worrying about the
+ guards:
+ ``` ruby
+ module EE::Base
+ def do_something
+ # Follow the above pattern to call super and extend it
+ end
+ end
+ ```
+ This would require updating CE first, or make sure this is back ported to CE.
+
+When prepending, place them in the `ee/` specific sub-directory, and
+wrap class or module in `module EE` to avoid naming conflicts.
+
+For example to override the CE implementation of
+`ApplicationController#after_sign_out_path_for`:
+
+```ruby
+def after_sign_out_path_for(resource)
+ current_application_settings.after_sign_out_path.presence || new_user_session_path
+end
+```
+
+Instead of modifying the method in place, you should add `prepend` to
+the existing file:
+
+```ruby
+class ApplicationController < ActionController::Base
+ prepend EE::ApplicationController
+ # ...
+
+ def after_sign_out_path_for(resource)
+ current_application_settings.after_sign_out_path.presence || new_user_session_path
+ end
+
+ # ...
+end
+```
+
+And create a new file in the `ee/` sub-directory with the altered
+implementation:
+
+```ruby
+module EE
+ class ApplicationController
+ def after_sign_out_path_for(resource)
+ raise NotImplementedError unless defined?(super)
+
+ if Gitlab::Geo.secondary?
+ Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state)
+ else
+ super
+ end
+ end
+ end
+end
+```
+
+#### Use self-descriptive wrapper methods
+
+When it's not possible/logical to modify the implementation of a
+method. Wrap it in a self-descriptive method and use that method.
+
+For example, in CE only an `admin` is allowed to access all private
+projects/groups, but in EE also an `auditor` has full private
+access. It would be incorrect to override the implementation of
+`User#admin?`, so instead add a method `full_private_access?` to
+`app/models/users.rb`. The implementation in CE will be:
+
+```ruby
+def full_private_access?
+ admin?
+end
+```
+
+In EE, the implementation `ee/app/models/ee/users.rb` would be:
+
+```ruby
+def full_private_access?
+ raise NotImplementedError unless defined?(super)
+ super || auditor?
+end
+```
+
+In `lib/gitlab/visibility_level.rb` this method is used to return the
+allowed visibilty levels:
+
+```ruby
+def levels_for_user(user = nil)
+ if user.full_private_access?
+ [PRIVATE, INTERNAL, PUBLIC]
+ elsif # ...
+end
+```
+
+See [CE MR][ce-mr-full-private] and [EE MR][ee-mr-full-private] for
+full implementation details.
+
+[ce-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12373
+[ee-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2199
+
+### Code in `app/controllers/`
+
+In controllers, the most common type of conflict is with `before_action` that
+has a list of actions in CE but EE adds some actions to that list.
+
+The same problem often occurs for `params.require` / `params.permit` calls.
+
+**Mitigations**
+
+Separate CE and EE actions/keywords. For instance for `params.require` in
+`ProjectsController`:
+
+```ruby
+def project_params
+ params.require(:project).permit(project_params_attributes)
+end
+
+# Always returns an array of symbols, created however best fits the use case.
+# It _should_ be sorted alphabetically.
+def project_params_attributes
+ %i[
+ description
+ name
+ path
+ ]
+end
+
+```
+
+In the `EE::ProjectsController` module:
+
+```ruby
+def project_params_attributes
+ super + project_params_attributes_ee
+end
+
+def project_params_attributes_ee
+ %i[
+ approvals_before_merge
+ approver_group_ids
+ approver_ids
+ ...
+ ]
+end
+```
+
+### Code in `app/models/`
+
+EE-specific models should `extend EE::Model`.
+
+For example, if EE has a specific `Tanuki` model, you would
+place it in `ee/app/models/ee/tanuki.rb`.
+
+### Code in `app/views/`
+
+It's a very frequent problem that EE is adding some specific view code in a CE
+view. For instance the approval code in the project's settings page.
+
+**Mitigations**
+
+Blocks of code that are EE-specific should be moved to partials. This
+avoids conflicts with big chunks of HAML code that that are not fun to
+resolve when you add the indentation to the equation.
+
+EE-specific views should be placed in `ee/app/views/ee/`, using extra
+sub-directories if appropriate.
+
+### Code in `lib/`
+
+Place EE-specific logic in the top-level `EE` module namespace. Namespace the
+class beneath the `EE` module just as you would normally.
+
+For example, if CE has LDAP classes in `lib/gitlab/ldap/` then you would place
+EE-specific LDAP classes in `ee/lib/ee/gitlab/ldap`.
+
+### Code in `spec/`
+
+When you're testing EE-only features, avoid adding examples to the
+existing CE specs. Also do no change existing CE examples, since they
+should remain working as-is when EE is running without a license.
+
+Instead place EE specs in the `spec/ee/spec` folder.
+
+## JavaScript code in `assets/javascripts/`
+
+To separate EE-specific JS-files we can also move the files into an `ee` folder.
+
+For example there can be an
+`app/assets/javascripts/protected_branches/protected_branches_bundle.js` and an
+EE counterpart
+`ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js`.
+
+That way we can create a separate webpack bundle in `webpack.config.js`:
+
+```javascript
+ protected_branches: '~/protected_branches',
+ ee_protected_branches: 'ee/protected_branches/protected_branches_bundle.js',
+```
+
+With the separate bundle in place, we can decide which bundle to load inside the
+view, using the `page_specific_javascript_bundle_tag` helper.
+
+```haml
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('protected_branches')
+```
+
+## SCSS code in `assets/stylesheets`
+
+To separate EE-specific styles in SCSS files, if a component you're adding styles for
+is limited to only EE, it is better to have a separate SCSS file in appropriate directory
+within `app/assets/stylesheets`.
+
+In some cases, this is not entirely possible or creating dedicated SCSS file is an overkill,
+e.g. a text style of some component is different for EE. In such cases,
+styles are usually kept in stylesheet that is common for both CE and EE, and it is wise
+to isolate such ruleset from rest of CE rules (along with adding comment describing the same)
+to avoid conflicts during CE to EE merge.
+
+#### Bad
+```scss
+.section-body {
+ .section-title {
+ background: $gl-header-color;
+ }
+
+ &.ee-section-body {
+ .section-title {
+ background: $gl-header-color-cyan;
+ }
+ }
+}
+```
+
+#### Good
+```scss
+.section-body {
+ .section-title {
+ background: $gl-header-color;
+ }
+}
+
+/* EE-specific styles */
+.section-body.ee-section-body {
+ .section-title {
+ background: $gl-header-color-cyan;
+ }
+}
+```
diff --git a/doc/development/fe_guide/axios.md b/doc/development/fe_guide/axios.md
new file mode 100644
index 00000000000..962fe3dcec9
--- /dev/null
+++ b/doc/development/fe_guide/axios.md
@@ -0,0 +1,68 @@
+# Axios
+We use [axios][axios] to communicate with the server in Vue applications and most new code.
+
+In order to guarantee all defaults are set you *should not use `axios` directly*, you should import `axios` from `axios_utils`.
+
+## CSRF token
+All our request require a CSRF token.
+To guarantee this token is set, we are importing [axios][axios], setting the token, and exporting `axios` .
+
+This exported module should be used instead of directly using `axios` to ensure the token is set.
+
+## Usage
+```javascript
+ import axios from '~/lib/utils/axios_utils';
+
+ axios.get(url)
+ .then((response) => {
+ // `data` is the response that was provided by the server
+ const data = response.data;
+
+ // `headers` the headers that the server responded with
+ // All header names are lower cased
+ const paginationData = response.headers;
+ })
+ .catch(() => {
+ //handle the error
+ });
+```
+
+## Mock axios response on tests
+
+To help us mock the responses we need we use [axios-mock-adapter][axios-mock-adapter]
+
+
+```javascript
+ import axios from '~/lib/utils/axios_utils';
+ import MockAdapter from 'axios-mock-adapter';
+
+ let mock;
+ beforeEach(() => {
+ // This sets the mock adapter on the default instance
+ mock = new MockAdapter(axios);
+ // Mock any GET request to /users
+ // arguments for reply are (status, data, headers)
+ mock.onGet('/users').reply(200, {
+ users: [
+ { id: 1, name: 'John Smith' }
+ ]
+ });
+ });
+
+ afterEach(() => {
+ mock.reset();
+ });
+```
+
+### Mock poll requests on tests with axios
+
+Because polling function requires an header object, we need to always include an object as the third argument:
+
+```javascript
+ mock.onGet('/users').reply(200, { foo: 'bar' }, {});
+```
+
+[axios]: https://github.com/axios/axios
+[axios-instance]: #creating-an-instance
+[axios-interceptors]: https://github.com/axios/axios#interceptors
+[axios-mock-adapter]: https://github.com/ctimmerm/axios-mock-adapter
diff --git a/doc/development/fe_guide/dropdowns.md b/doc/development/fe_guide/dropdowns.md
new file mode 100644
index 00000000000..e1660ac5caa
--- /dev/null
+++ b/doc/development/fe_guide/dropdowns.md
@@ -0,0 +1,38 @@
+# Dropdowns
+
+
+## How to style a bootstrap dropdown
+1. Use the HTML structure provided by the [docs][bootstrap-dropdowns]
+1. Add a specific class to the top level `.dropdown` element
+
+
+ ```Haml
+ .dropdown.my-dropdown
+ %button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
+ %span.dropdown-toggle-text
+ Toggle Dropdown
+ = icon('chevron-down')
+
+ %ul.dropdown-menu
+ %li
+ %a
+ item!
+ ```
+
+ Or use the helpers
+ ```Haml
+ .dropdown.my-dropdown
+ = dropdown_toggle('Toogle!', { toggle: 'dropdown' })
+ = dropdown_content
+ %li
+ %a
+ item!
+ ```
+
+1. Include the mixin in CSS
+
+ ```SCSS
+ @include new-style-dropdown('.my-dropdown ');
+ ```
+
+[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns
diff --git a/doc/development/fe_guide/emojis.md b/doc/development/fe_guide/emojis.md
new file mode 100644
index 00000000000..38794c47965
--- /dev/null
+++ b/doc/development/fe_guide/emojis.md
@@ -0,0 +1,27 @@
+# Emojis
+
+GitLab supports native unicode emojis and fallsback to image-based emojis selectively
+when your platform does not support it.
+
+# How to update Emojis
+
+ 1. Update the `gemojione` gem
+ 1. Update `fixtures/emojis/index.json` from [Gemojione](https://github.com/jonathanwiesel/gemojione/blob/master/config/index.json).
+ In the future, we could grab the file directly from the gem.
+ We should probably make a PR on the Gemojione project to get access to
+ all emojis after being parsed or just a raw path to the `json` file itself.
+ 1. Ensure [`emoji-unicode-version`](https://www.npmjs.com/package/emoji-unicode-version)
+ is up to date with the latest version.
+ 1. Run `bundle exec rake gemojione:aliases`
+ 1. Run `bundle exec rake gemojione:digests`
+ 1. Run `bundle exec rake gemojione:sprite`
+ 1. Ensure new sprite sheets generated for 1x and 2x
+ - `app/assets/images/emoji.png`
+ - `app/assets/images/emoji@2x.png`
+ 1. Ensure you see new individual images copied into `app/assets/images/emoji/`
+ 1. Ensure you can see the new emojis and their aliases in the GFM Autocomplete
+ 1. Ensure you can see the new emojis and their aliases in the award emoji menu
+ 1. You might need to add new emoji unicode support checks and rules for platforms
+ that do not support a certain emoji and we need to fallback to an image.
+ See `app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js`
+ and `app/assets/javascripts/emoji/support/unicode_support_map.js`
diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md
index a76e978bd26..b288ee95722 100644
--- a/doc/development/fe_guide/icons.md
+++ b/doc/development/fe_guide/icons.md
@@ -4,15 +4,17 @@ We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are on
### Usage in HAML/Rails
-To use a sprite Icon in HAML or Rails we use a specific helper function :
+To use a sprite Icon in HAML or Rails we use a specific helper function :
`sprite_icon(icon_name, size: nil, css_class: '')`
-**icon_name** Use the icon_name that you can find in the SVG Sprite (Overview is available under `/assets/sprite.symbol.html`).
+**icon_name** Use the icon_name that you can find in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`).
+
**size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class)
+
**css_class (optional)** If you want to add additional css classes
-**Example**
+**Example**
`= sprite_icon('issues', size: 72, css_class: 'icon-danger')`
@@ -20,16 +22,34 @@ To use a sprite Icon in HAML or Rails we use a specific helper function :
`<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>`
+### Usage in Vue
+
+We have a special Vue component for our sprite icons in `\vue_shared\components\icon.vue`.
+
+Sample usage :
+
+`<icon
+ name="retry"
+ :size="32"
+ css-classes="top"
+ />`
+
+**name** Name of the Icon in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`).
+
+**size (optional)** Number value for the size which is then mapped to a specific CSS class (Available Sizes: 8,12,16,18,24,32,48,72 are mapped to `sXX` css classes)
+
+**css-classes (optional)** Additional CSS Classes to add to the svg tag.
+
### Usage in HTML/JS
-Please use the following function inside JS to render an icon :
+Please use the following function inside JS to render an icon :
`gl.utils.spriteIcon(iconName)`
## Adding a new icon to the sprite
All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency.
-To upgrade to a new SVG Sprite version run `yarn upgrade https://gitlab.com/gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders.
+To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. The updated files should be tracked in Git as those are referenced.
# SVG Illustrations
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 031b12a8e91..72cb557d054 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -54,7 +54,8 @@ or make changes to our frontend development guidelines.
---
-## [Testing](testing.md)
+## [Testing](../testing_guide/frontend_testing.md)
+
How we write frontend tests, run the GitLab test suite, and debug test related
issues.
@@ -70,9 +71,14 @@ Vue specific design patterns and practices.
---
+## [Axios](axios.md)
+Axios specific practices and gotchas.
+
## [Icons](icons.md)
How we use SVG for our Icons.
+## [Dropdowns](dropdowns.md)
+How we use dropdowns.
---
## Style Guides
@@ -102,6 +108,10 @@ Frontend security practices.
## [Accessibility](accessibility.md)
Our accessibility standards and resources.
+## [Internationalization (i18n) and Translations](../i18n/externalization.md)
+Frontend internationalization support is described in [this document](../i18n/).
+The [externalization part of the guide](../i18n/externalization.md) explains the helpers/methods available.
+
[rails]: http://rubyonrails.org/
[haml]: http://haml.info/
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 77ae6d2a0ea..10f4c5a0902 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -88,16 +88,31 @@ followed by any global declarations, then a blank newline prior to any imports o
1. Use ES module syntax to import modules
```javascript
// bad
- require('foo');
+ const SomeClass = require('some_class');
// good
- import Foo from 'foo';
+ import SomeClass from 'some_class';
// bad
- module.exports = Foo;
+ module.exports = SomeClass;
// good
- export default Foo;
+ export default SomeClass;
+ ```
+
+ Import statements are following usual naming guidelines, for example object literals use camel case:
+
+ ```javascript
+ // some_object file
+ export default {
+ key: 'value',
+ };
+
+ // bad
+ import ObjectLiteral from 'some_object';
+
+ // good
+ import objectLiteral from 'some_object';
```
1. Relative paths: when importing a module in the same directory, a child
@@ -285,6 +300,13 @@ A forEach will cause side effects, it will be mutating the array being iterated.
1. **Extensions**: Use `.vue` extension for Vue components.
1. **Reference Naming**: Use camelCase for their instances:
```javascript
+ // bad
+ import CardBoard from 'cardBoard'
+
+ components: {
+ CardBoard:
+ };
+
// good
import cardBoard from 'cardBoard'
diff --git a/doc/development/fe_guide/testing.md b/doc/development/fe_guide/testing.md
index 867c83f1e72..98e499b8c0f 100644
--- a/doc/development/fe_guide/testing.md
+++ b/doc/development/fe_guide/testing.md
@@ -1,254 +1 @@
-# Frontend Testing
-
-There are two types of test suites you'll encounter while developing frontend code
-at GitLab. We use Karma and Jasmine for JavaScript unit and integration testing, and RSpec
-feature tests with Capybara for e2e (end-to-end) integration testing.
-
-Unit and feature tests need to be written for all new features.
-Most of the time, you should use rspec for your feature tests.
-There are cases where the behaviour you are testing is not worth the time spent running the full application,
-for example, if you are testing styling, animation, edge cases or small actions that don't involve the backend,
-you should write an integration test using Jasmine.
-
-![Testing priority triangle](img/testing_triangle.png)
-
-_This diagram demonstrates the relative priority of each test type we use_
-
-Regression tests should be written for bug fixes to prevent them from recurring in the future.
-
-See [the Testing Standards and Style Guidelines](../testing.md)
-for more information on general testing practices at GitLab.
-
-## Karma test suite
-
-GitLab uses the [Karma][karma] test runner with [Jasmine][jasmine] as its test
-framework for our JavaScript unit and integration tests. For integration tests,
-we generate HTML files using RSpec (see `spec/javascripts/fixtures/*.rb` for examples).
-Some fixtures are still HAML templates that are translated to HTML files using the same mechanism (see `static_fixtures.rb`).
-Adding these static fixtures should be avoided as they are harder to keep up to date with real views.
-The existing static fixtures will be migrated over time.
-Please see [gitlab-org/gitlab-ce#24753](https://gitlab.com/gitlab-org/gitlab-ce/issues/24753) to track our progress.
-Fixtures are served during testing by the [jasmine-jquery][jasmine-jquery] plugin.
-
-JavaScript tests live in `spec/javascripts/`, matching the folder structure
-of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js`
-has a corresponding `spec/javascripts/behaviors/autosize_spec.js` file.
-
-Keep in mind that in a CI environment, these tests are run in a headless
-browser and you will not have access to certain APIs, such as
-[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
-which will have to be stubbed.
-
-### Best practice
-
-#### Naming unit tests
-
-When writing describe test blocks to test specific functions/methods,
-please use the method name as the describe block name.
-
-```javascript
-// Good
-describe('methodName', () => {
- it('passes', () => {
- expect(true).toEqual(true);
- });
-});
-
-// Bad
-describe('#methodName', () => {
- it('passes', () => {
- expect(true).toEqual(true);
- });
-});
-
-// Bad
-describe('.methodName', () => {
- it('passes', () => {
- expect(true).toEqual(true);
- });
-});
-```
-#### Testing Promises
-
-When testing Promises you should always make sure that the test is asynchronous and rejections are handled.
-Your Promise chain should therefore end with a call of the `done` callback and `done.fail` in case an error occurred.
-
-```javascript
-// Good
-it('tests a promise', (done) => {
- promise
- .then((data) => {
- expect(data).toBe(asExpected);
- })
- .then(done)
- .catch(done.fail);
-});
-
-// Good
-it('tests a promise rejection', (done) => {
- promise
- .then(done.fail)
- .catch((error) => {
- expect(error).toBe(expectedError);
- })
- .then(done)
- .catch(done.fail);
-});
-
-// Bad (missing done callback)
-it('tests a promise', () => {
- promise
- .then((data) => {
- expect(data).toBe(asExpected);
- })
-});
-
-// Bad (missing catch)
-it('tests a promise', (done) => {
- promise
- .then((data) => {
- expect(data).toBe(asExpected);
- })
- .then(done)
-});
-
-// Bad (use done.fail in asynchronous tests)
-it('tests a promise', (done) => {
- promise
- .then((data) => {
- expect(data).toBe(asExpected);
- })
- .then(done)
- .catch(fail)
-});
-
-// Bad (missing catch)
-it('tests a promise rejection', (done) => {
- promise
- .catch((error) => {
- expect(error).toBe(expectedError);
- })
- .then(done)
-});
-```
-
-#### Stubbing
-
-For unit tests, you should stub methods that are unrelated to the current unit you are testing.
-If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely.
-
-For integration tests, you should stub methods that will effect the stability of the test if they
-execute their original behaviour. i.e. Network requests.
-
-### Vue.js unit tests
-See this [section][vue-test].
-
-### Running frontend tests
-
-`rake karma` runs the frontend-only (JavaScript) tests.
-It consists of two subtasks:
-
-- `rake karma:fixtures` (re-)generates fixtures
-- `rake karma:tests` actually executes the tests
-
-As long as the fixtures don't change, `rake karma:tests` (or `yarn karma`)
-is sufficient (and saves you some time).
-
-### Live testing and focused testing
-
-While developing locally, it may be helpful to keep karma running so that you
-can get instant feedback on as you write tests and modify code. To do this
-you can start karma with `npm run karma-start`. It will compile the javascript
-assets and run a server at `http://localhost:9876/` where it will automatically
-run the tests on any browser which connects to it. You can enter that url on
-multiple browsers at once to have it run the tests on each in parallel.
-
-While karma is running, any changes you make will instantly trigger a recompile
-and retest of the entire test suite, so you can see instantly if you've broken
-a test with your changes. You can use [jasmine focused][jasmine-focus] or
-excluded tests (with `fdescribe` or `xdescribe`) to get karma to run only the
-tests you want while you're working on a specific feature, but make sure to
-remove these directives when you commit your code.
-
-## RSpec Feature Integration Tests
-
-Information on setting up and running RSpec integration tests with
-[Capybara][capybara] can be found in the
-[general testing guide](../testing.md).
-
-## Gotchas
-
-### Errors due to use of unsupported JavaScript features
-
-Similar errors will be thrown if you're using JavaScript features not yet
-supported by the PhantomJS test runner which is used for both Karma and RSpec
-tests. We polyfill some JavaScript objects for older browsers, but some
-features are still unavailable:
-
-- Array.from
-- Array.first
-- Async functions
-- Generators
-- Array destructuring
-- For..Of
-- Symbol/Symbol.iterator
-- Spread
-
-Until these are polyfilled appropriately, they should not be used. Please
-update this list with additional unsupported features.
-
-### RSpec errors due to JavaScript
-
-By default RSpec unit tests will not run JavaScript in the headless browser
-and will simply rely on inspecting the HTML generated by rails.
-
-If an integration test depends on JavaScript to run correctly, you need to make
-sure the spec is configured to enable JavaScript when the tests are run. If you
-don't do this you'll see vague error messages from the spec runner.
-
-To enable a JavaScript driver in an `rspec` test, add `:js` to the
-individual spec or the context block containing multiple specs that need
-JavaScript enabled:
-
-```ruby
-# For one spec
-it 'presents information about abuse report', :js do
- # assertions...
-end
-
-describe "Admin::AbuseReports", :js do
- it 'presents information about abuse report' do
- # assertions...
- end
- it 'shows buttons for adding to abuse report' do
- # assertions...
- end
-end
-```
-
-### Spinach errors due to missing JavaScript
-
-> **Note:** Since we are discouraging the use of Spinach when writing new
-> feature tests, you shouldn't ever need to use this. This information is kept
-> available for legacy purposes only.
-
-In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
-file for the failing spec, add the `@javascript` flag above the Scenario:
-
-```
-@javascript
-Scenario: Developer can approve merge request
- Given I am a "Shop" developer
- And I visit project "Shop" merge requests page
- And merge request 'Bug NS-04' must be approved
- And I click link "Bug NS-04"
- When I click link "Approve"
- Then I should see approved merge request "Bug NS-04"
-```
-
-[capybara]: http://teamcapybara.github.io/capybara/
-[jasmine]: https://jasmine.github.io/
-[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
-[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
-[karma]: http://karma-runner.github.io/
-[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
+This document was moved to [../testing_guide/frontend_testing.md](../testing_guide/frontend_testing.md).
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 277e0cd5f00..6e9f18dd1c3 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -178,66 +178,15 @@ itself, please read this guide: [State Management][state-management]
The Service is a class used only to communicate with the server.
It does not store or manipulate any data. It is not aware of the store or the components.
-We use [vue-resource][vue-resource-repo] to communicate with the server.
+We use [axios][axios] to communicate with the server.
+Refer to [axios](axios.md) for more details.
-Vue Resource should only be imported in the service file.
+Axios instance should only be imported in the service file.
```javascript
- import Vue from 'vue';
- import VueResource from 'vue-resource';
-
- Vue.use(VueResource);
- ```
-
-#### Vue-resource gotchas
-#### Headers
-Headers are being parsed into a plain object in an interceptor.
-In Vue-resource 1.x `headers` object was changed into an `Headers` object. In order to not change all old code, an interceptor was added.
-
-If you need to write a unit test that takes the headers in consideration, you need to include an interceptor to parse the headers after your test interceptor.
-You can see an example in `spec/javascripts/environments/environment_spec.js`:
- ```javascript
- import { headersInterceptor } from './helpers/vue_resource_helper';
-
- beforeEach(() => {
- Vue.http.interceptors.push(myInterceptor);
- Vue.http.interceptors.push(headersInterceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, myInterceptor);
- Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
- });
- ```
-
-#### `.json()`
-When making a request to the server, you will most likely need to access the body of the response.
-Use `.json()` to convert. Because `.json()` returns a Promise the follwoing structure should be used:
-
- ```javascript
- service.get('url')
- .then(resp => resp.json())
- .then((data) => {
- this.store.storeData(data);
- })
- .catch(() => new Flash('Something went wrong'));
+ import axios from 'javascripts/lib/utils/axios_utils';
```
-When using `Poll` (`app/assets/javascripts/lib/utils/poll.js`), the `successCallback` needs to handle `.json()` as a Promise:
- ```javascript
- successCallback: (response) => {
- return response.json().then((data) => {
- // handle the response
- });
- }
- ```
-
-#### CSRF token
-We use a Vue Resource interceptor to manage the CSRF token.
-`app/assets/javascripts/vue_shared/vue_resource_interceptor.js` holds all our common interceptors.
-Note: You don't need to load `app/assets/javascripts/vue_shared/vue_resource_interceptor.js`
-since it's already being loaded by `common_vue.js`.
-
### End Result
The following example shows an application:
@@ -278,15 +227,14 @@ export default class Store {
}
// service.js
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-import 'vue_shared/vue_resource_interceptor';
-
-Vue.use(VueResource);
+import axios from 'javascripts/lib/utils/axios_utils'
export default class Service {
constructor(options) {
- this.todos = Vue.resource(endpoint.todosEndpoint);
+ this.todos = axios.create({
+ baseURL: endpoint.todosEndpoint
+ });
+
}
getTodos() {
@@ -525,50 +473,8 @@ The main return value of a Vue component is the rendered output. In order to tes
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:
+Refer to [mock axios](axios.md#mock-axios-response-on-tests)
- ```javascript
- // Mock the service to return data
- const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([{
- title: 'This is a todo',
- body: 'This is the text'
- }]), {
- status: 200,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
- });
-
- it('should do something', (done) => {
- setTimeout(() => {
- // Test received data
- done();
- }, 0);
- });
- ```
-
-1. Headers interceptor
-Refer to [this section](vue.md#headers)
-
-1. Use `$.mount()` to mount the component
-
-```javascript
-// bad
-new Component({
- el: document.createElement('div')
-});
-
-// good
-new Component().$mount();
-```
## Vuex
To manage the state of an application you may use [Vuex][vuex-docs].
@@ -769,8 +675,6 @@ describe('component', () => {
[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
[one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow
-[vue-resource-repo]: https://github.com/pagekit/vue-resource
-[vue-resource-interceptor]: https://github.com/pagekit/vue-resource/blob/develop/docs/http.md#interceptors
[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
@@ -778,3 +682,6 @@ describe('component', () => {
[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
+[axios]: https://github.com/axios/axios
+[axios-interceptors]: https://github.com/axios/axios#interceptors
+
diff --git a/doc/development/file_storage.md b/doc/development/file_storage.md
new file mode 100644
index 00000000000..cf00e24e11a
--- /dev/null
+++ b/doc/development/file_storage.md
@@ -0,0 +1,49 @@
+# File Storage in GitLab
+
+We use the [CarrierWave] gem to handle file upload, store and retrieval.
+
+There are many places where file uploading is used, according to contexts:
+
+* System
+ - Instance Logo (logo visible in sign in/sign up pages)
+ - Header Logo (one displayed in the navigation bar)
+* Group
+ - Group avatars
+* User
+ - User avatars
+ - User snippet attachments
+* Project
+ - Project avatars
+ - Issues/MR Markdown attachments
+ - Issues/MR Legacy Markdown attachments
+ - CI Build Artifacts
+ - LFS Objects
+
+
+## Disk storage
+
+GitLab started saving everything on local disk. While directory location changed from previous versions,
+they are still not 100% standardized. You can see them below:
+
+| Description | In DB? | Relative path | Uploader class | model_type |
+| ------------------------------------- | ------ | ----------------------------------------------------------- | ---------------------- | ---------- |
+| Instance logo | yes | uploads/-/system/appearance/logo/:id/:filename | `AttachmentUploader` | Appearance |
+| Header logo | yes | uploads/-/system/appearance/header_logo/:id/:filename | `AttachmentUploader` | Appearance |
+| Group avatars | yes | uploads/-/system/group/avatar/:id/:filename | `AvatarUploader` | Group |
+| User avatars | yes | uploads/-/system/user/avatar/:id/:filename | `AvatarUploader` | User |
+| User snippet attachments | yes | uploads/-/system/personal_snippet/:id/:random_hex/:filename | `PersonalFileUploader` | Snippet |
+| Project avatars | yes | uploads/-/system/project/avatar/:id/:filename | `AvatarUploader` | Project |
+| Issues/MR Markdown attachments | yes | uploads/:project_path_with_namespace/:random_hex/:filename | `FileUploader` | Project |
+| Issues/MR Legacy Markdown attachments | no | uploads/-/system/note/attachment/:id/:filename | `AttachmentUploader` | Note |
+| CI Artifacts (CE) | yes | shared/artifacts/:year_:month/:project_id/:id | `ArtifactUploader` | Ci::Build |
+| LFS Objects (CE) | yes | shared/lfs-objects/:hex/:hex/:object_hash | `LfsObjectUploader` | LfsObject |
+
+CI Artifacts and LFS Objects behave differently in CE and EE. In CE they inherit the `GitlabUploader`
+while in EE they inherit the `ObjectStoreUploader` and store files in and S3 API compatible object store.
+
+In the case of Issues/MR Markdown attachments, there is a different approach using the [Hashed Storage] layout,
+instead of basing the path into a mutable variable `:project_path_with_namespace`, it's possible to use the
+hash of the project ID instead, if project migrates to the new approach (introduced in 10.2).
+
+[CarrierWave]: https://github.com/carrierwaveuploader/carrierwave
+[Hashed Storage]: ../administration/repository_storage_types.md
diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md
index e41d258bec6..ca2048c7019 100644
--- a/doc/development/gitaly.md
+++ b/doc/development/gitaly.md
@@ -52,8 +52,8 @@ rm -rf tmp/tests/gitaly
## `TooManyInvocationsError` errors
-During development and testing, you may experience `Gitlab::GitalyClient::TooManyInvocationsError` failures.
-The `GitalyClient` will attempt to block against potential n+1 issues by raising this error
+During development and testing, you may experience `Gitlab::GitalyClient::TooManyInvocationsError` failures.
+The `GitalyClient` will attempt to block against potential n+1 issues by raising this error
when Gitaly is called more than 30 times in a single Rails request or Sidekiq execution.
As a temporary measure, export `GITALY_DISABLE_REQUEST_LIMITS=1` to suppress the error. This will disable the n+1 detection
@@ -64,7 +64,7 @@ Please raise an issue in the GitLab CE or EE repositories to report the issue. I
`TooManyInvocationsError`. Also include any known failing tests if possible.
Isolate the source of the n+1 problem. This will normally be a loop that results in Gitaly being called for each
-element in an array. If you are unable to isolate the problem, please contact a member
+element in an array. If you are unable to isolate the problem, please contact a member
of the [Gitaly Team](https://gitlab.com/groups/gl-gitaly/group_members) for assistance.
Once the source has been found, wrap it in an `allow_n_plus_1_calls` block, as follows:
@@ -79,6 +79,24 @@ end
Once the code is wrapped in this block, this code-path will be excluded from n+1 detection.
+## Request counts
+
+Commits and other git data, is now fetched through Gitaly. These fetches can,
+much like with a database, be batched. This improves performance for the client
+and for Gitaly itself and therefore for the users too. To keep performance stable
+and guard performance regressions, Gitaly calls can be counted and the call count
+can be tested against. This requires the `:request_store` flag to be set.
+
+```ruby
+describe 'Gitaly Request count tests' do
+ context 'when the request store is activated', :request_store do
+ it 'correctly counts the gitaly requests made' do
+ expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(10)
+ end
+ end
+end
+```
+
---
[Return to Development documentation](README.md)
diff --git a/doc/development/github_importer.md b/doc/development/github_importer.md
new file mode 100644
index 00000000000..0d558583bb8
--- /dev/null
+++ b/doc/development/github_importer.md
@@ -0,0 +1,209 @@
+# Working with the GitHub importer
+
+In GitLab 10.2 a new version of the GitHub importer was introduced. This new
+importer performs its work in parallel using Sidekiq, greatly reducing the time
+necessary to import GitHub projects into a GitLab instance.
+
+The GitHub importer offers two different types of importers: a sequential
+importer and a parallel importer. The Rake task `import:github` uses the
+sequential importer, while everything else uses the parallel importer. The
+difference between these two importers is quite simple: the sequential importer
+does all work in a single thread, making it more useful for debugging purposes
+or Rake tasks. The parallel importer on the other hand uses Sidekiq.
+
+## Requirements
+
+* GitLab CE 10.2.0 or newer.
+* Sidekiq workers that process the `github_importer` and
+ `github_importer_advance_stage` queues (this is enabled by default).
+* Octokit (used for interacting with the GitHub API)
+
+## Code structure
+
+The importer's codebase is broken up into the following directories:
+
+* `lib/gitlab/github_import`: this directory contains most of the code such as
+ the classes used for importing resources.
+* `app/workers/gitlab/github_import`: this directory contains the Sidekiq
+ workers.
+* `app/workers/concerns/gitlab/github_import`: this directory contains a few
+ modules reused by the various Sidekiq workers.
+
+## Architecture overview
+
+When a GitHub project is imported we schedule and execute a job for the
+`RepositoryImportworker` worker as all other importers. However, unlike other
+importers we don't immediately perform the work necessary. Instead work is
+divided into separate stages, with each stage consisting out of a set of Sidekiq
+jobs that are executed. Between every stage a job is scheduled that periodically
+checks if all work of the current stage is completed, advancing the import
+process to the next stage when this is the case. The worker handling this is
+called `Gitlab::GithubImport::AdvanceStageWorker`.
+
+## Stages
+
+### 1. RepositoryImportWorker
+
+This worker will kick off the import process by simply scheduling a job for the
+next worker.
+
+### 2. Stage::ImportRepositoryWorker
+
+This worker will import the repository and wiki, scheduling the next stage when
+done.
+
+### 3. Stage::ImportBaseDataWorker
+
+This worker will import base data such as labels, milestones, and releases. This
+work is done in a single thread since it can be performed fast enough that we
+don't need to perform this work in parallel.
+
+### 4. Stage::ImportPullRequestsWorker
+
+This worker will import all pull requests. For every pull request a job for the
+`Gitlab::GithubImport::ImportPullRequestWorker` worker is scheduled.
+
+### 5. Stage::ImportIssuesAndDiffNotesWorker
+
+This worker will import all issues and pull request comments. For every issue we
+schedule a job for the `Gitlab::GithubImport::ImportIssueWorker` worker. For
+pull request comments we instead schedule jobs for the
+`Gitlab::GithubImport::DiffNoteImporter` worker.
+
+This worker processes both issues and diff notes in parallel so we don't need to
+schedule a separate stage and wait for the previous one to complete.
+
+Issues are imported separately from pull requests because only the "issues" API
+includes labels for both issue and pull requests. Importing issues and setting
+label links in the same worker removes the need for performing a separate crawl
+through the API data, reducing the number of API calls necessary to import a
+project.
+
+### 6. Stage::ImportNotesWorker
+
+This worker imports regular comments for both issues and pull requests. For
+every comment we schedule a job for the
+`Gitlab::GithubImport::ImportNoteWorker` worker.
+
+Regular comments have to be imported at the end since the GitHub API used
+returns comments for both issues and pull requests. This means we have to wait
+for all issues and pull requests to be imported before we can import regular
+comments.
+
+### 7. Stage::FinishImportWorker
+
+This worker will wrap up the import process by performing some housekeeping
+(such as flushing any caches) and by marking the import as completed.
+
+## Advancing stages
+
+Advancing stages is done in one of two ways:
+
+1. Scheduling the worker for the next stage directly.
+2. Scheduling a job for `Gitlab::GithubImport::AdvanceStageWorker` which will
+ advance the stage when all work of the current stage has been completed.
+
+The first approach should only be used by workers that perform all their work in
+a single thread, while `AdvanceStageWorker` should be used for everything else.
+
+The way `AdvanceStageWorker` works is fairly simple. When scheduling a job it
+will be given a project ID, a list of Redis keys, and the name of the next
+stage. The Redis keys (produced by `Gitlab::JobWaiter`) are used to check if the
+currently running stage has been completed or not. If the stage has not yet been
+completed `AdvanceStageWorker` will reschedule itself. Once a stage finishes
+`AdvanceStageworker` will refresh the import JID (more on this below) and
+schedule the worker of the next stage.
+
+To reduce the number of `AdvanceStageWorker` jobs scheduled this worker will
+briefly wait for jobs to complete before deciding what the next action should
+be. For small projects this may slow down the import process a bit, but it will
+also reduce pressure on the system as a whole.
+
+## Refreshing import JIDs
+
+GitLab includes a worker called `StuckImportJobsWorker` that will periodically
+run and mark project imports as failed if they have been running for more than
+15 hours. For GitHub projects this poses a bit of a problem: importing large
+projects could take several hours depending on how often we hit the GitHub rate
+limit (more on this below), but we don't want `StuckImportJobsWorker` to mark
+our import as failed because of this.
+
+To prevent this from happening we periodically refresh the expiration time of
+the import process. This works by storing the JID of the import job in the
+database, then refreshing this JID's TTL at various stages throughout the import
+process. This is done by calling `Project#refresh_import_jid_expiration`. By
+refreshing this TTL we can ensure our import does not get marked as failed so
+long we're still performing work.
+
+## GitHub rate limit
+
+GitHub has a rate limit of 5 000 API calls per hour. The number of requests
+necessary to import a project is largely dominated by the number of unique users
+involved in a project (e.g. issue authors). Other data such as issue pages
+and comments typically only requires a few dozen requests to import. This is
+because we need the Email address of users in order to map them to GitLab users.
+
+We handle this by doing the following:
+
+1. Once we hit the rate limit all jobs will automatically reschedule themselves
+ in such a way that they are not executed until the rate limit has been reset.
+2. We cache the mapping of GitHub users to GitLab users in Redis.
+
+More information on user caching can be found below.
+
+## Caching user lookups
+
+When mapping GitHub users to GitLab users we need to (in the worst case)
+perform:
+
+1. One API call to get the user's Email address.
+2. Two database queries to see if a corresponding GitLab user exists. One query
+ will try to find the user based on the GitHub user ID, while the second query
+ is used to find the user using their GitHub Email address.
+
+Because this process is quite expensive we cache the result of these lookups in
+Redis. For every user looked up we store three keys:
+
+1. A Redis key mapping GitHub usernames to their Email addresses.
+2. A Redis key mapping a GitHub Email addresses to a GitLab user ID.
+3. A Redis key mapping a GitHub user ID to GitLab user ID.
+
+There are two types of lookups we cache:
+
+1. A positive lookup, meaning we found a GitLab user ID.
+2. A negative lookup, meaning we didn't find a GitLab user ID. Caching this
+ prevents us from performing the same work for users that we know don't exist
+ in our GitLab database.
+
+The expiration time of these keys is 24 hours. When retrieving the cache of a
+positive lookups we refresh the TTL automatically. The TTL of false lookups is
+never refreshed.
+
+Because of this caching layer it's possible newly registered GitLab accounts
+won't be linked to their corresponding GitHub accounts. This however will sort
+itself out once the cached keys expire.
+
+The user cache lookup is shared across projects. This means that the more
+projects get imported the fewer GitHub API calls will be needed.
+
+The code for this resides in:
+
+* `lib/gitlab/github_import/user_finder.rb`
+* `lib/gitlab/github_import/caching.rb`
+
+## Mapping labels and milestones
+
+To reduce pressure on the database we do not query it when setting labels and
+milestones on issues and merge requests. Instead we cache this data when we
+import labels and milestones, then we reuse this cache when assigning them to
+issues/merge requests. Similar to the user lookups these cache keys are expired
+automatically after 24 hours of not being used.
+
+Unlike the user lookup caches these label and milestone caches are scoped to the
+project that is being imported.
+
+The code for this resides in:
+
+* `lib/gitlab/github_import/label_finder.rb`
+* `lib/gitlab/github_import/milestone_finder.rb`
+* `lib/gitlab/github_import/caching.rb`
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 167260b6e0e..4b65a0f4a35 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -110,7 +110,7 @@ You can mark that content for translation with:
In JavaScript we added the `__()` (double underscore parenthesis) function
for translations.
-### Updating the PO files with the new content
+## Updating the PO files with the new content
Now that the new content is marked for translation, we need to update the PO
files with the following command:
@@ -119,23 +119,20 @@ files with the following command:
bundle exec rake gettext:find
```
-This command will update the `locale/**/gitlab.edit.po` file with the
-new content that the parser has found.
+This command will update the `locale/gitlab.pot` file with the newly externalized
+strings and remove any strings that aren't used anymore. You should check this
+file in. Once the changes are on master, they will be picked up by
+[Crowdin](http://translate.gitlab.com) and be presented for translation.
-New translations will be added with their default content and will be marked
-fuzzy. To use the translation, look for the `#, fuzzy` mention in `gitlab.edit.po`
-and remove it.
+The command also updates the translation files for each language: `locale/*/gitlab.po`
+These changes can be discarded, the languange files will be updated by Crowdin
+automatically.
-We need to make sure we remove the `fuzzy` translations before generating the
-`locale/**/gitlab.po` file. When they aren't removed, the resulting `.po` will
-be treated as a binary file which could overwrite translations that were merged
-before the new translations.
+Discard all of them at once like this:
-When we are just preparing a page to be translated, but not actually adding any
-translations. There's no need to generate `.po` files.
-
-Translations that aren't used in the source code anymore will be marked with
-`~#`; these can be removed to keep our translation files clutter-free.
+```sh
+git checkout locale/*/gitlab.po
+```
### Validating PO files
@@ -180,15 +177,43 @@ aren't in the message with id `1 pipeline`.
## Working with special content
+
+### Just marking content for parsing
+
+- In Ruby/HAML:
+
+ ```ruby
+ _('Subscribe')
+ ```
+
+- In JavaScript:
+
+ ```js
+ import { __ } from '../../../locale';
+ const label = __('Subscribe');
+ ```
+
+
+Sometimes there are some dynamic translations that can't be found by the
+parser when running `bundle exec rake gettext:find`. For these scenarios you can
+use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
+
+There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a).
+
### Interpolation
- In Ruby/HAML:
```ruby
- _("Hello %{name}") % { name: 'Joe' }
+ _("Hello %{name}") % { name: 'Joe' } => 'Hello Joe'
```
-- In JavaScript: Not supported at this moment.
+- In JavaScript:
+
+ ```js
+ import { __, sprintf } from '../../../locale';
+ sprintf(__('Hello %{username}'), { username: 'Joe' }) => 'Hello Joe'
+ ```
### Plurals
@@ -234,14 +259,6 @@ Sometimes you need to add some context to the text that you want to translate
s__('OpenedNDaysAgo|Opened')
```
-### Just marking content for parsing
-
-Sometimes there are some dynamic translations that can't be found by the
-parser when running `bundle exec rake gettext:find`. For these scenarios you can
-use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
-
-There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a).
-
## Adding a new language
Let's suppose you want to add translations for a new language, let's say French.
diff --git a/doc/development/i18n/translation.md b/doc/development/i18n/translation.md
index 79707aaf671..b34ec754742 100644
--- a/doc/development/i18n/translation.md
+++ b/doc/development/i18n/translation.md
@@ -58,6 +58,18 @@ For example, in French we translate `you` as the informal `tu`.
You can refer to other translated strings and notes in the glossary to assist determining a
suitable level of formality.
+### Inclusive language
+
+[Diversity] is one of GitLab's values.
+We ask you to avoid translations which exclude people based on their gender or ethnicity.
+In languages which distinguish between a male and female form,
+use both or choose a neutral formulation.
+
+For example in German, the word "user" can be translated into "Benutzer" (male) or "Benutzerin" (female).
+Therefore "create a new user" would translate into "Benutzer(in) anlegen".
+
+[Diversity]: https://about.gitlab.com/handbook/values/#diversity
+
### Updating the glossary
To propose additions to the glossary please
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index a75cdf22f40..274923c2d43 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -4,11 +4,11 @@ GitLab CE is licensed under the terms of the MIT License. GitLab EE is licensed
## Automated Testing
-In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition.
+In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems and node modules in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition.
-There are some limitations with the automated testing, however. CSS and JavaScript libraries, as well as any Ruby libraries not included by way of Bundler, must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them.
+There are some limitations with the automated testing, however. CSS, JavaScript, or Ruby libraries which are not included by way of Bundler, NPM, or Yarn (for instance those manually copied into our source tree in the `vendor` directory), must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them.
-Some gems may not include their license information in their `gemspec` file. These won't be detected by License Finder, and will have to be verified manually.
+Some gems may not include their license information in their `gemspec` file, and some node modules may not include their license information in their `package.json` file. These won't be detected by License Finder, and will have to be verified manually.
### License Finder commands
@@ -56,6 +56,7 @@ Libraries with the following licenses are acceptable for use:
- [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.
+- [OWFa 1.0][OWFa1]: An open-source license and patent grant designed for specifications.
## Unacceptable Licenses
@@ -105,6 +106,7 @@ Gems which are included only in the "development" or "test" groups by Bundler ar
[OSL-GNU]: https://www.gnu.org/licenses/license-list.en.html#OSL
[Org-Repo]: https://gitlab.com/gitlab-com/organization
[UNLICENSE]: https://unlicense.org
+[OWFa1]: http://www.openwebfoundation.org/legal/the-owf-1-0-agreements/owfa-1-0
[Facebook]: https://code.facebook.com/pages/850928938376556
[x-list]: https://www.apache.org/legal/resolved.html#category-x
[Acceptable-Licenses]: #acceptable-licenses
diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md
index 899be9eae4b..ba82babb38a 100644
--- a/doc/development/limit_ee_conflicts.md
+++ b/doc/development/limit_ee_conflicts.md
@@ -336,6 +336,12 @@ Blocks of code that are EE-specific should be moved to partials as much as
possible to avoid conflicts with big chunks of HAML code that that are not fun
to resolve when you add the indentation in the equation.
+### Assets
+
+#### gitlab-svgs
+
+Conflicts in `app/assets/images/icons.json` or `app/assets/images/icons.svg` can be resolved simply by regenerating those assets with [`yarn run svg`](https://gitlab.com/gitlab-org/gitlab-svgs).
+
---
[Return to Development documentation](README.md)
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 9b8ab5da74e..a235dd74909 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -198,7 +198,43 @@ end
Keep in mind that this operation can easily take 10-15 minutes to complete on
larger installations (e.g. GitLab.com). As a result you should only add default
-values if absolutely necessary.
+values if absolutely necessary. There is a RuboCop cop that will fail if this
+method is used on some tables that are very large on GitLab.com, which would
+cause other issues.
+
+## Updating an existing column
+
+To update an existing column to a particular value, you can use
+`update_column_in_batches` (`add_column_with_default` uses this internally to
+fill in the default value). This will split the updates into batches, so we
+don't update too many rows at in a single statement.
+
+This updates the column `foo` in the `projects` table to 10, where `some_column`
+is `'hello'`:
+
+```ruby
+update_column_in_batches(:projects, :foo, 10) do |table, query|
+ query.where(table[:some_column].eq('hello'))
+end
+```
+
+To perform a computed update, the value can be wrapped in `Arel.sql`, so Arel
+treats it as an SQL literal. The below example is the same as the one above, but
+the value is set to the product of the `bar` and `baz` columns:
+
+```ruby
+update_value = Arel.sql('bar * baz')
+
+update_column_in_batches(:projects, :foo, update_value) do |table, query|
+ query.where(table[:some_column].eq('hello'))
+end
+```
+
+Like `add_column_with_default`, there is a RuboCop cop to detect usage of this
+on large tables. In the case of `update_column_in_batches`, it may be acceptable
+to run on a large table, as long as it is only updating a small subset of the
+rows in the table, but do not ignore that without validating on the GitLab.com
+staging environment - or asking someone else to do so for you - beforehand.
## Integer column type
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
index 933033a09e0..af79353b721 100644
--- a/doc/development/profiling.md
+++ b/doc/development/profiling.md
@@ -27,3 +27,13 @@ Bullet will log query problems to both the Rails log as well as the Chrome
console.
As a follow up to finding `N+1` queries with Bullet, consider writing a [QueryRecoder test](query_recorder.md) to prevent a regression.
+
+## GitLab Profiler
+
+
+[Gitlab-Profiler](https://gitlab.com/gitlab-com/gitlab-profiler) was built to
+help developers understand why specific URLs of their application may be slow
+and to provide hard data that can help reduce load times.
+
+For GitLab.com, you can find the latest results here:
+<http://redash.gitlab.com/dashboard/gitlab-profiler-statistics>
diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md
index e0127aaed4c..12e90101139 100644
--- a/doc/development/query_recorder.md
+++ b/doc/development/query_recorder.md
@@ -22,6 +22,52 @@ As an example you might create 5 issues in between counts, which would cause the
> **Note:** In some cases the query count might change slightly between runs for unrelated reasons. In this case you might need to test `exceed_query_limit(control_count + acceptable_change)`, but this should be avoided if possible.
+## Finding the source of the query
+
+It may be useful to identify the source of the queries by looking at the call backtrace.
+To enable this, run the specs with the `QUERY_RECORDER_DEBUG` environment variable set. For example:
+
+```
+QUERY_RECORDER_DEBUG=1 bundle exec rspec spec/requests/api/projects_spec.rb
+```
+
+This will log calls to QueryRecorder into the `test.log`. For example:
+
+```
+QueryRecorder SQL: SELECT COUNT(*) FROM "issues" WHERE "issues"."deleted_at" IS NULL AND "issues"."project_id" = $1 AND ("issues"."state" IN ('opened')) AND "issues"."confidential" = $2
+ --> /home/user/gitlab/gdk/gitlab/spec/support/query_recorder.rb:19:in `callback'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:127:in `finish'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `block in finish'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `each'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `finish'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/instrumenter.rb:36:in `finish'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/instrumenter.rb:25:in `instrument'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract_adapter.rb:478:in `log'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql_adapter.rb:601:in `exec_cache'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql_adapter.rb:585:in `execute_and_clear'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql/database_statements.rb:160:in `exec_query'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/database_statements.rb:356:in `select'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/database_statements.rb:32:in `select_all'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:68:in `block in select_all'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:83:in `cache_sql'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:68:in `select_all'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:270:in `execute_simple_calculation'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:227:in `perform_calculation'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:133:in `calculate'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:48:in `count'
+ --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:20:in `uncached_count'
+ --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:12:in `block in count'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:299:in `block in fetch'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:585:in `block in save_block_result_to_cache'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:547:in `block in instrument'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications.rb:166:in `instrument'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:547:in `instrument'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:584:in `save_block_result_to_cache'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:299:in `fetch'
+ --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:12:in `count'
+ --> /home/user/gitlab/gdk/gitlab/app/models/project.rb:1296:in `open_issues_count'
+```
+
## See also
- [Bullet](profiling.md#Bullet) For finding `N+1` query problems
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index bfd80aab6a4..ceff57276d2 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -122,6 +122,15 @@ they can be easily inspected.
bundle exec rake services:doc
```
+## Updating Emoji Aliases
+
+To update the Emoji aliases file (used for Emoji autocomplete) you must run the
+following:
+
+```
+bundle exec rake gemojione:aliases
+```
+
## Updating Emoji Digests
To update the Emoji digests file (used for Emoji autocomplete) you must run the
@@ -131,6 +140,7 @@ following:
bundle exec rake gemojione:digests
```
+
This will update the file `fixtures/emojis/digests.json` based on the currently
available Emoji.
@@ -153,7 +163,7 @@ Starting a project from a template needs this project to be exported. On a
up to date master branch with run:
```
-gdk run db
+gdk run
# In a new terminal window
bundle exec rake gitlab:update_project_templates
git checkout -b update-project-templates
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 4d5b90de6fc..45b1519ece8 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -1,566 +1 @@
-# Testing Standards and Style Guidelines
-
-This guide outlines standards and best practices for automated testing of GitLab
-CE and EE.
-
-It is meant to be an _extension_ of the [thoughtbot testing
-styleguide](https://github.com/thoughtbot/guides/tree/master/style/testing). If
-this guide defines a rule that contradicts the thoughtbot guide, this guide
-takes precedence. Some guidelines may be repeated verbatim to stress their
-importance.
-
-## Definitions
-
-### Unit tests
-
-Formal definition: https://en.wikipedia.org/wiki/Unit_testing
-
-These kind of tests ensure that a single unit of code (a method) works as
-expected (given an input, it has a predictable output). These tests should be
-isolated as much as possible. For example, model methods that don't do anything
-with the database shouldn't need a DB record. Classes that don't need database
-records should use stubs/doubles as much as possible.
-
-| Code path | Tests path | Testing engine | Notes |
-| --------- | ---------- | -------------- | ----- |
-| `app/finders/` | `spec/finders/` | RSpec | |
-| `app/helpers/` | `spec/helpers/` | RSpec | |
-| `app/db/{post_,}migrate/` | `spec/migrations/` | RSpec | More details at [`spec/migrations/README.md`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md). |
-| `app/policies/` | `spec/policies/` | RSpec | |
-| `app/presenters/` | `spec/presenters/` | RSpec | |
-| `app/routing/` | `spec/routing/` | RSpec | |
-| `app/serializers/` | `spec/serializers/` | RSpec | |
-| `app/services/` | `spec/services/` | RSpec | |
-| `app/tasks/` | `spec/tasks/` | RSpec | |
-| `app/uploaders/` | `spec/uploaders/` | RSpec | |
-| `app/views/` | `spec/views/` | RSpec | |
-| `app/workers/` | `spec/workers/` | RSpec | |
-| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. |
-
-### Integration tests
-
-Formal definition: https://en.wikipedia.org/wiki/Integration_testing
-
-These kind of tests ensure that individual parts of the application work well together, without the overhead of the actual app environment (i.e. the browser). These tests should assert at the request/response level: status code, headers, body. They're useful to test permissions, redirections, what view is rendered etc.
-
-| Code path | Tests path | Testing engine | Notes |
-| --------- | ---------- | -------------- | ----- |
-| `app/controllers/` | `spec/controllers/` | RSpec | |
-| `app/mailers/` | `spec/mailers/` | RSpec | |
-| `lib/api/` | `spec/requests/api/` | RSpec | |
-| `lib/ci/api/` | `spec/requests/ci/api/` | RSpec | |
-| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. |
-
-#### About controller tests
-
-In an ideal world, controllers should be thin. However, when this is not the
-case, it's acceptable to write a system/feature test without JavaScript instead
-of a controller test. The reason is that testing a fat controller usually
-involves a lot of stubbing, things like:
-
-```ruby
-controller.instance_variable_set(:@user, user)
-```
-
-and use methods which are deprecated in Rails 5 ([#23768]).
-
-[#23768]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23768
-
-#### About Karma
-
-As you may have noticed, Karma is both in the Unit tests and the Integration
-tests category. That's because Karma is a tool that provides an environment to
-run JavaScript tests, so you can either run unit tests (e.g. test a single
-JavaScript method), or integration tests (e.g. test a component that is composed
-of multiple components).
-
-### System tests or Feature tests
-
-Formal definition: https://en.wikipedia.org/wiki/System_testing.
-
-These kind of tests ensure the application works as expected from a user point
-of view (aka black-box testing). These tests should test a happy path for a
-given page or set of pages, and a test case should be added for any regression
-that couldn't have been caught at lower levels with better tests (i.e. if a
-regression is found, regression tests should be added at the lowest-level
-possible).
-
-| Tests path | Testing engine | Notes |
-| ---------- | -------------- | ----- |
-| `spec/features/` | [Capybara] + [RSpec] | If your spec has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. |
-| `features/` | Spinach | Spinach tests are deprecated, [you shouldn't add new Spinach tests](#spinach-feature-tests). |
-
-[Capybara]: https://github.com/teamcapybara/capybara
-[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
-[Poltergeist]: https://github.com/teamcapybara/capybara#poltergeist
-[RackTest]: https://github.com/teamcapybara/capybara#racktest
-
-#### Best practices
-
-- Create only the necessary records in the database
-- Test a happy path and a less happy path but that's it
-- Every other possible path should be tested with Unit or Integration tests
-- Test what's displayed on the page, not the internals of ActiveRecord models.
- For instance, if you want to verify that a record was created, add
- expectations that its attributes are displayed on the page, not that
- `Model.count` increased by one.
-- It's ok to look for DOM elements but don't abuse it since it makes the tests
- more brittle
-
-If we're confident that the low-level components work well (and we should be if
-we have enough Unit & Integration tests), we shouldn't need to duplicate their
-thorough testing at the System test level.
-
-It's very easy to add tests, but a lot harder to remove or improve tests, so one
-should take care of not introducing too many (slow and duplicated) specs.
-
-The reasons why we should follow these best practices are as follows:
-
-- System tests are slow to run since they spin up the entire application stack
- in a headless browser, and even slower when they integrate a JS driver
-- When system tests run with a JavaScript driver, the tests are run in a
- different thread than the application. This means it does not share a
- database connection and your test will have to commit the transactions in
- order for the running application to see the data (and vice-versa). In that
- case we need to truncate the database after each spec instead of simply
- rolling back a transaction (the faster strategy that's in use for other kind
- of tests). This is slower than transactions, however, so we want to use
- truncation only when necessary.
-
-### Black-box tests or End-to-end tests
-
-GitLab consists of [multiple pieces] such as [GitLab Shell], [GitLab Workhorse],
-[Gitaly], [GitLab Pages], [GitLab Runner], and GitLab Rails. All theses pieces
-are configured and packaged by [GitLab Omnibus].
-
-[GitLab QA] is a tool that allows to test that all these pieces integrate well
-together by building a Docker image for a given version of GitLab Rails and
-running feature tests (i.e. using Capybara) against it.
-
-The actual test scenarios and steps are [part of GitLab Rails] so that they're
-always in-sync with the codebase.
-
-[multiple pieces]: ./architecture.md#components
-[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell
-[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
-[Gitaly]: https://gitlab.com/gitlab-org/gitaly
-[GitLab Pages]: https://gitlab.com/gitlab-org/gitlab-pages
-[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner
-[GitLab Omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab
-[GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa
-[part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa
-
-## Test for what should not be there
-
-This is particularly important for permission calls and might be called a
-negative assertion: make sure only the bare minimum is returned and nothing else.
-
-See an issue about [leaking tokens] as an example of a vulnerability that is
-captured by such a test.
-
-[leaking tokens]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37948
-
-## How to test at the correct level?
-
-As many things in life, deciding what to test at each level of testing is a
-trade-off:
-
-- Unit tests are usually cheap, and you should consider them like the basement
- of your house: you need them to be confident that your code is behaving
- correctly. However if you run only unit tests without integration / system
- tests, you might [miss] the [big] [picture]!
-- Integration tests are a bit more expensive, but don't abuse them. A system test
- is often better than an integration test that is stubbing a lot of internals.
-- System tests are expensive (compared to unit tests), even more if they require
- a JavaScript driver. Make sure to follow the guidelines in the [Speed](#test-speed)
- section.
-
-Another way to see it is to think about the "cost of tests", this is well
-explained [in this article][tests-cost] and the basic idea is that the cost of a
-test includes:
-
-- The time it takes to write the test
-- The time it takes to run the test every time the suite runs
-- The time it takes to understand the test
-- The time it takes to fix the test if it breaks and the underlying code is OK
-- Maybe, the time it takes to change the code to make the code testable.
-
-[miss]: https://twitter.com/ThePracticalDev/status/850748070698651649
-[big]: https://twitter.com/timbray/status/822470746773409794
-[picture]: https://twitter.com/withzombies/status/829716565834752000
-[tests-cost]: https://medium.com/table-xi/high-cost-tests-and-high-value-tests-a86e27a54df#.2ulyh3a4e
-
-## Frontend testing
-
-Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md).
-
-## RSpec
-
-### General Guidelines
-
-- Use a single, top-level `describe ClassName` block.
-- Use `.method` to describe class methods and `#method` to describe instance
- methods.
-- Use `context` to test branching logic.
-- Don't assert against the absolute value of a sequence-generated attribute (see [Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
-- Try to match the ordering of tests to the ordering within the class.
-- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
- to separate phases.
-- Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
-- Don't assert against the absolute value of a sequence-generated attribute (see
- [Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
-- Don't supply the `:each` argument to hooks since it's the default.
-- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`
-
-[four-phase-test]: https://robots.thoughtbot.com/four-phase-test
-
-### Automatic retries and flaky tests detection
-
-On our CI, we use [rspec-retry] to automatically retry a failing example a few
-times (see [`spec/spec_helper.rb`] for the precise retries count).
-
-We also use a home-made `RspecFlaky::Listener` listener which records flaky
-examples in a JSON report file on `master` (`retrieve-tests-metadata` and `update-tests-metadata` jobs), and warns when a new flaky example
-is detected in any other branch (`flaky-examples-check` job). In the future, the
-`flaky-examples-check` job will not be allowed to fail.
-
-[rspec-retry]: https://github.com/NoRedInk/rspec-retry
-[`spec/spec_helper.rb`]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/spec_helper.rb
-
-### `let` variables
-
-GitLab's RSpec suite has made extensive use of `let` variables to reduce
-duplication. However, this sometimes [comes at the cost of clarity][lets-not],
-so we need to set some guidelines for their use going forward:
-
-- `let` variables are preferable to instance variables. Local variables are
- preferable to `let` variables.
-- Use `let` to reduce duplication throughout an entire spec file.
-- Don't use `let` to define variables used by a single test; define them as
- local variables inside the test's `it` block.
-- Don't define a `let` variable inside the top-level `describe` block that's
- only used in a more deeply-nested `context` or `describe` block. Keep the
- definition as close as possible to where it's used.
-- Try to avoid overriding the definition of one `let` variable with another.
-- Don't define a `let` variable that's only used by the definition of another.
- Use a helper method instead.
-
-[lets-not]: https://robots.thoughtbot.com/lets-not
-
-#### `set` variables
-
-In some cases there is no need to recreate the same object for tests again for
-each example. For example, a project is needed to test issues on the same
-project, one project will do for the entire file. This can be achieved by using
-`set` in the same way you would use `let`.
-
-`rspec-set` only works on ActiveRecord objects, and before new examples it
-reloads or recreates the model, _only_ if needed. That is, when you changed
-properties or destroyed the object.
-
-There is one gotcha; you can't reference a model defined in a `let` block in a
-`set` block.
-
-### Time-sensitive tests
-
-[Timecop](https://github.com/travisjeffery/timecop) is available in our
-Ruby-based tests for verifying things that are time-sensitive. Any test that
-exercises or verifies something time-sensitive should make use of Timecop to
-prevent transient test failures.
-
-Example:
-
-```ruby
-it 'is overdue' do
- issue = build(:issue, due_date: Date.tomorrow)
-
- Timecop.freeze(3.days.from_now) do
- expect(issue).to be_overdue
- end
-end
-```
-
-### System / Feature tests
-
-- Feature specs should be named `ROLE_ACTION_spec.rb`, such as
- `user_changes_password_spec.rb`.
-- Use only one `feature` block per feature spec file.
-- Use scenario titles that describe the success and failure paths.
-- Avoid scenario titles that add no information, such as "successfully".
-- Avoid scenario titles that repeat the feature title.
-
-### Table-based / Parameterized tests
-
-This style of testing is used to exercise one piece of code with a comprehensive
-range of inputs. By specifying the test case once, alongside a table of inputs
-and the expected output for each, your tests can be made easier to read and more
-compact.
-
-We use the [rspec-parameterized](https://github.com/tomykaira/rspec-parameterized)
-gem. A short example, using the table syntax and checking Ruby equality for a
-range of inputs, might look like this:
-
-```ruby
-describe "#==" do
- using RSpec::Parameterized::TableSyntax
-
- let(:project1) { create(:project) }
- let(:project2) { create(:project) }
- where(:a, :b, :result) do
- 1 | 1 | true
- 1 | 2 | false
- true | true | true
- true | false | false
- project1 | project1 | true
- project2 | project2 | true
- project 1 | project2 | false
- end
-
- with_them do
- it { expect(a == b).to eq(result) }
-
- it 'is isomorphic' do
- expect(b == a).to eq(result)
- end
- end
-end
-```
-
-### Matchers
-
-Custom matchers should be created to clarify the intent and/or hide the
-complexity of RSpec expectations.They should be placed under
-`spec/support/matchers/`. Matchers can be placed in subfolder if they apply to
-a certain type of specs only (e.g. features, requests etc.) but shouldn't be if
-they apply to multiple type of specs.
-
-#### have_gitlab_http_status
-
-Prefer `have_gitlab_http_status` over `have_http_status` because the former
-could also show the response body whenever the status mismatched. This would
-be very useful whenever some tests start breaking and we would love to know
-why without editing the source and rerun the tests.
-
-This is especially useful whenever it's showing 500 internal server error.
-
-### Shared contexts
-
-All shared contexts should be be placed under `spec/support/shared_contexts/`.
-Shared contexts can be placed in subfolder if they apply to a certain type of
-specs only (e.g. features, requests etc.) but shouldn't be if they apply to
-multiple type of specs.
-
-Each file should include only one context and have a descriptive name, e.g.
-`spec/support/shared_contexts/controllers/githubish_import_controller_shared_context.rb`.
-
-### Shared examples
-
-All shared examples should be be placed under `spec/support/shared_examples/`.
-Shared examples can be placed in subfolder if they apply to a certain type of
-specs only (e.g. features, requests etc.) but shouldn't be if they apply to
-multiple type of specs.
-
-Each file should include only one context and have a descriptive name, e.g.
-`spec/support/shared_examples/controllers/githubish_import_controller_shared_example.rb`.
-
-### Helpers
-
-Helpers are usually modules that provide some methods to hide the complexity of
-specific RSpec examples. You can define helpers in RSpec files if they're not
-intended to be shared with other specs. Otherwise, they should be be placed
-under `spec/support/helpers/`. Helpers can be placed in subfolder if they apply
-to a certain type of specs only (e.g. features, requests etc.) but shouldn't be
-if they apply to multiple type of specs.
-
-Helpers should follow the Rails naming / namespacing convention. For instance
-`spec/support/helpers/cycle_analytics_helpers.rb` should define:
-
-```ruby
-module Spec
- module Support
- module Helpers
- module CycleAnalyticsHelpers
- def create_commit_referencing_issue(issue, branch_name: random_git_name)
- project.repository.add_branch(user, branch_name, 'master')
- create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
- end
- end
- end
- end
-end
-```
-
-Helpers should not change the RSpec config. For instance, the helpers module
-described above should not include:
-
-```ruby
-RSpec.configure do |config|
- config.include Spec::Support::Helpers::CycleAnalyticsHelpers
-end
-```
-
-### Factories
-
-GitLab uses [factory_girl] as a test fixture replacement.
-
-- Factory definitions live in `spec/factories/`, named using the pluralization
- of their corresponding model (`User` factories are defined in `users.rb`).
-- There should be only one top-level factory definition per file.
-- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and
- should) call `create(...)` instead of `FactoryGirl.create(...)`.
-- Make use of [traits] to clean up definitions and usages.
-- When defining a factory, don't define attributes that are not required for the
- resulting record to pass validation.
-- When instantiating from a factory, don't supply attributes that aren't
- required by the test.
-- Factories don't have to be limited to `ActiveRecord` objects.
- [See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d).
-
-[factory_girl]: https://github.com/thoughtbot/factory_girl
-[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits
-
-### Fixtures
-
-All fixtures should be be placed under `spec/fixtures/`.
-
-### Config
-
-RSpec config files are files that change the RSpec config (i.e.
-`RSpec.configure do |config|` blocks). They should be placed under
-`spec/support/config/`.
-
-Each file should be related to a specific domain, e.g.
-`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc.
-
-Helpers can be included in the `spec/support/config/rspec.rb` file. If a
-helpers module applies only to a certain kind of specs, it should add modifiers
-to the `config.include` call. For instance if
-`spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and
-`type: :model` specs only, you would write the following:
-
-```ruby
-RSpec.configure do |config|
- config.include Spec::Support::Helpers::CycleAnalyticsHelpers, :lib
- config.include Spec::Support::Helpers::CycleAnalyticsHelpers, type: :model
-end
-```
-
-## Testing Rake Tasks
-
-To make testing Rake tasks a little easier, there is a helper that can be included
-in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use
-`require 'rake_helper'`. The helper includes `spec_helper` for you, and configures
-a few other things to make testing Rake tasks easier.
-
-At a minimum, requiring the Rake helper will redirect `stdout`, include the
-runtime task helpers, and include the `RakeHelpers` Spec support module.
-
-The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make
-executing tasks simple. See `spec/support/rake_helpers.rb` for all available
-methods.
-
-Example:
-
-```ruby
-require 'rake_helper'
-
-describe 'gitlab:shell rake tasks' do
- before do
- Rake.application.rake_require 'tasks/gitlab/shell'
-
- stub_warn_user_is_not_gitlab
- end
-
- describe 'install task' do
- it 'invokes create_hooks task' do
- expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
-
- run_rake_task('gitlab:shell:install')
- end
- end
-end
-```
-
-## Test speed
-
-GitLab has a massive test suite that, without [parallelization], can take hours
-to run. It's important that we make an effort to write tests that are accurate
-and effective _as well as_ fast.
-
-Here are some things to keep in mind regarding test performance:
-
-- `double` and `spy` are faster than `FactoryGirl.build(...)`
-- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`.
-- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
- `spy`, or `double` will do. Database persistence is slow!
-- Don't mark a feature as requiring JavaScript (through `@javascript` in
- Spinach or `:js` in RSpec) unless it's _actually_ required for the test
- to be valid. Headless browser testing is slow!
-
-[parallelization]: #test-suite-parallelization-on-the-ci
-
-### Test suite parallelization on the CI
-
-Our current CI parallelization setup is as follows:
-
-1. The `retrieve-tests-metadata` job in the `prepare` stage ensures that we have
- a `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file:
- - The `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file is fetched
- from S3, if it's not here we initialize the file with `{}`.
-1. Each `rspec-pg x y`/`rspec-mysql x y` job is run with `knapsack rspec` and
- should have an evenly distributed share of tests:
- - It works because the jobs have access to the
- `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` since the "artifacts
- from all previous stages are passed by default". [^1]
- - The jobs set their own report path to
- `KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`.
- - If knapsack is doing its job, test files that are run should be listed under
- `Report specs`, not under `Leftover specs`.
-1. The `update-tests-metadata` job takes all the
- `knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`
- files from the `rspec-pg x y`/`rspec-mysql x y`jobs and merge them all together
- into a single `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file that
- is then uploaded to S3.
-
-After that, the next pipeline will use the up-to-date
-`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy
-is used for Spinach tests as well.
-
-### Monitoring
-
-The GitLab test suite is [monitored] for the `master` branch, and any branch
-that includes `rspec-profile` in their name.
-
-A [public dashboard] is available for everyone to see. Feel free to look at the
-slowest test files and try to improve them.
-
-[monitored]: ./performance.md#rspec-profiling
-[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default
-
-## CI setup
-
-- On CE and EE, the test suite runs both PostgreSQL and MySQL.
-- Rails logging to `log/test.log` is disabled by default in CI [for
- performance reasons][logging]. To override this setting, provide the
- `RAILS_ENABLE_TEST_LOG` environment variable.
-
-[logging]: https://jtway.co/speed-up-your-rails-test-suite-by-6-in-1-line-13fedb869ec4
-
-## Spinach (feature) tests
-
-GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
-for its feature/integration tests in September 2012.
-
-As of March 2016, we are [trying to avoid adding new Spinach
-tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward,
-opting for [RSpec feature](#features-integration) specs.
-
-Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
-no more than one new `step` definition. If more than that is required, the
-test should be re-implemented using RSpec instead.
-
----
-
-[Return to Development documentation](README.md)
-
-[^1]: /ci/yaml/README.html#dependencies
+This document was moved to [testing_guide/index.md](testing_guide/index.md).
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
new file mode 100644
index 00000000000..8b7b015427f
--- /dev/null
+++ b/doc/development/testing_guide/best_practices.md
@@ -0,0 +1,303 @@
+# Testing best practices
+
+## Test speed
+
+GitLab has a massive test suite that, without [parallelization], can take hours
+to run. It's important that we make an effort to write tests that are accurate
+and effective _as well as_ fast.
+
+Here are some things to keep in mind regarding test performance:
+
+- `double` and `spy` are faster than `FactoryGirl.build(...)`
+- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`.
+- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
+ `spy`, or `double` will do. Database persistence is slow!
+- Don't mark a feature as requiring JavaScript (through `@javascript` in
+ Spinach or `:js` in RSpec) unless it's _actually_ required for the test
+ to be valid. Headless browser testing is slow!
+
+[parallelization]: ci.md#test-suite-parallelization-on-the-ci
+
+## RSpec
+
+### General guidelines
+
+- Use a single, top-level `describe ClassName` block.
+- Use `.method` to describe class methods and `#method` to describe instance
+ methods.
+- Use `context` to test branching logic.
+- Don't assert against the absolute value of a sequence-generated attribute (see [Gotchas](../gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
+- Try to match the ordering of tests to the ordering within the class.
+- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
+ to separate phases.
+- Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
+- Don't assert against the absolute value of a sequence-generated attribute (see
+ [Gotchas](../gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
+- Don't supply the `:each` argument to hooks since it's the default.
+- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`
+- When using `evaluate_script("$('.js-foo').testSomething()")` (or `execute_script`) which acts on a given element,
+ use a Capyabara matcher beforehand (e.g. `find('.js-foo')`) to ensure the element actually exists.
+
+[four-phase-test]: https://robots.thoughtbot.com/four-phase-test
+
+### System / Feature tests
+
+NOTE: **Note:** Before writing a new system test, [please consider **not**
+writing one](testing_levels.md#consider-not-writing-a-system-test)!
+
+- Feature specs should be named `ROLE_ACTION_spec.rb`, such as
+ `user_changes_password_spec.rb`.
+- Use scenario titles that describe the success and failure paths.
+- Avoid scenario titles that add no information, such as "successfully".
+- Avoid scenario titles that repeat the feature title.
+- Create only the necessary records in the database
+- Test a happy path and a less happy path but that's it
+- Every other possible path should be tested with Unit or Integration tests
+- Test what's displayed on the page, not the internals of ActiveRecord models.
+ For instance, if you want to verify that a record was created, add
+ expectations that its attributes are displayed on the page, not that
+ `Model.count` increased by one.
+- It's ok to look for DOM elements but don't abuse it since it makes the tests
+ more brittle
+
+#### Debugging Capybara
+
+Sometimes you may need to debug Capybara tests by observing browser behavior.
+
+You can pause Capybara and view the website on the browser by using the
+`live_debug` method in your spec. The current page will be automatically opened
+in your default browser.
+You may need to sign in first (the current user's credentials are displayed in
+the terminal).
+
+To resume the test run, press any key.
+
+For example:
+
+```
+$ bin/rspec spec/features/auto_deploy_spec.rb:34
+Running via Spring preloader in process 8999
+Run options: include {:locations=>{"./spec/features/auto_deploy_spec.rb"=>[34]}}
+
+Current example is paused for live debugging
+The current user credentials are: user2 / 12345678
+Press any key to resume the execution of the example!
+Back to the example!
+.
+
+Finished in 34.51 seconds (files took 0.76702 seconds to load)
+1 example, 0 failures
+```
+
+### `let` variables
+
+GitLab's RSpec suite has made extensive use of `let` variables to reduce
+duplication. However, this sometimes [comes at the cost of clarity][lets-not],
+so we need to set some guidelines for their use going forward:
+
+- `let` variables are preferable to instance variables. Local variables are
+ preferable to `let` variables.
+- Use `let` to reduce duplication throughout an entire spec file.
+- Don't use `let` to define variables used by a single test; define them as
+ local variables inside the test's `it` block.
+- Don't define a `let` variable inside the top-level `describe` block that's
+ only used in a more deeply-nested `context` or `describe` block. Keep the
+ definition as close as possible to where it's used.
+- Try to avoid overriding the definition of one `let` variable with another.
+- Don't define a `let` variable that's only used by the definition of another.
+ Use a helper method instead.
+
+[lets-not]: https://robots.thoughtbot.com/lets-not
+
+### `set` variables
+
+In some cases there is no need to recreate the same object for tests again for
+each example. For example, a project is needed to test issues on the same
+project, one project will do for the entire file. This can be achieved by using
+`set` in the same way you would use `let`.
+
+`rspec-set` only works on ActiveRecord objects, and before new examples it
+reloads or recreates the model, _only_ if needed. That is, when you changed
+properties or destroyed the object.
+
+There is one gotcha; you can't reference a model defined in a `let` block in a
+`set` block.
+
+### Time-sensitive tests
+
+[Timecop](https://github.com/travisjeffery/timecop) is available in our
+Ruby-based tests for verifying things that are time-sensitive. Any test that
+exercises or verifies something time-sensitive should make use of Timecop to
+prevent transient test failures.
+
+Example:
+
+```ruby
+it 'is overdue' do
+ issue = build(:issue, due_date: Date.tomorrow)
+
+ Timecop.freeze(3.days.from_now) do
+ expect(issue).to be_overdue
+ end
+end
+```
+
+### Table-based / Parameterized tests
+
+This style of testing is used to exercise one piece of code with a comprehensive
+range of inputs. By specifying the test case once, alongside a table of inputs
+and the expected output for each, your tests can be made easier to read and more
+compact.
+
+We use the [rspec-parameterized](https://github.com/tomykaira/rspec-parameterized)
+gem. A short example, using the table syntax and checking Ruby equality for a
+range of inputs, might look like this:
+
+```ruby
+describe "#==" do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:project1) { create(:project) }
+ let(:project2) { create(:project) }
+ where(:a, :b, :result) do
+ 1 | 1 | true
+ 1 | 2 | false
+ true | true | true
+ true | false | false
+ project1 | project1 | true
+ project2 | project2 | true
+ project 1 | project2 | false
+ end
+
+ with_them do
+ it { expect(a == b).to eq(result) }
+
+ it 'is isomorphic' do
+ expect(b == a).to eq(result)
+ end
+ end
+end
+```
+
+### Matchers
+
+Custom matchers should be created to clarify the intent and/or hide the
+complexity of RSpec expectations.They should be placed under
+`spec/support/matchers/`. Matchers can be placed in subfolder if they apply to
+a certain type of specs only (e.g. features, requests etc.) but shouldn't be if
+they apply to multiple type of specs.
+
+#### `have_gitlab_http_status`
+
+Prefer `have_gitlab_http_status` over `have_http_status` because the former
+could also show the response body whenever the status mismatched. This would
+be very useful whenever some tests start breaking and we would love to know
+why without editing the source and rerun the tests.
+
+This is especially useful whenever it's showing 500 internal server error.
+
+### Shared contexts
+
+All shared contexts should be be placed under `spec/support/shared_contexts/`.
+Shared contexts can be placed in subfolder if they apply to a certain type of
+specs only (e.g. features, requests etc.) but shouldn't be if they apply to
+multiple type of specs.
+
+Each file should include only one context and have a descriptive name, e.g.
+`spec/support/shared_contexts/controllers/githubish_import_controller_shared_context.rb`.
+
+### Shared examples
+
+All shared examples should be be placed under `spec/support/shared_examples/`.
+Shared examples can be placed in subfolder if they apply to a certain type of
+specs only (e.g. features, requests etc.) but shouldn't be if they apply to
+multiple type of specs.
+
+Each file should include only one context and have a descriptive name, e.g.
+`spec/support/shared_examples/controllers/githubish_import_controller_shared_example.rb`.
+
+### Helpers
+
+Helpers are usually modules that provide some methods to hide the complexity of
+specific RSpec examples. You can define helpers in RSpec files if they're not
+intended to be shared with other specs. Otherwise, they should be be placed
+under `spec/support/helpers/`. Helpers can be placed in subfolder if they apply
+to a certain type of specs only (e.g. features, requests etc.) but shouldn't be
+if they apply to multiple type of specs.
+
+Helpers should follow the Rails naming / namespacing convention. For instance
+`spec/support/helpers/cycle_analytics_helpers.rb` should define:
+
+```ruby
+module Spec
+ module Support
+ module Helpers
+ module CycleAnalyticsHelpers
+ def create_commit_referencing_issue(issue, branch_name: random_git_name)
+ project.repository.add_branch(user, branch_name, 'master')
+ create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
+ end
+ end
+ end
+ end
+end
+```
+
+Helpers should not change the RSpec config. For instance, the helpers module
+described above should not include:
+
+```ruby
+RSpec.configure do |config|
+ config.include Spec::Support::Helpers::CycleAnalyticsHelpers
+end
+```
+
+### Factories
+
+GitLab uses [factory_girl] as a test fixture replacement.
+
+- Factory definitions live in `spec/factories/`, named using the pluralization
+ of their corresponding model (`User` factories are defined in `users.rb`).
+- There should be only one top-level factory definition per file.
+- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and
+ should) call `create(...)` instead of `FactoryGirl.create(...)`.
+- Make use of [traits] to clean up definitions and usages.
+- When defining a factory, don't define attributes that are not required for the
+ resulting record to pass validation.
+- When instantiating from a factory, don't supply attributes that aren't
+ required by the test.
+- Factories don't have to be limited to `ActiveRecord` objects.
+ [See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d).
+
+[factory_girl]: https://github.com/thoughtbot/factory_girl
+[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits
+
+### Fixtures
+
+All fixtures should be be placed under `spec/fixtures/`.
+
+### Config
+
+RSpec config files are files that change the RSpec config (i.e.
+`RSpec.configure do |config|` blocks). They should be placed under
+`spec/support/config/`.
+
+Each file should be related to a specific domain, e.g.
+`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc.
+
+Helpers can be included in the `spec/support/config/rspec.rb` file. If a
+helpers module applies only to a certain kind of specs, it should add modifiers
+to the `config.include` call. For instance if
+`spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and
+`type: :model` specs only, you would write the following:
+
+```ruby
+RSpec.configure do |config|
+ config.include Spec::Support::Helpers::CycleAnalyticsHelpers, :lib
+ config.include Spec::Support::Helpers::CycleAnalyticsHelpers, type: :model
+end
+```
+
+---
+
+[Return to Testing documentation](index.md)
diff --git a/doc/development/testing_guide/ci.md b/doc/development/testing_guide/ci.md
new file mode 100644
index 00000000000..e90de55068d
--- /dev/null
+++ b/doc/development/testing_guide/ci.md
@@ -0,0 +1,52 @@
+# GitLab tests in the Continuous Integration (CI) context
+
+### Test suite parallelization on the CI
+
+Our current CI parallelization setup is as follows:
+
+1. The `knapsack` job in the prepare stage that is supposed to ensure we have a
+ `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file:
+ - The `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file is fetched
+ from S3, if it's not here we initialize the file with `{}`.
+1. Each `rspec x y` job are run with `knapsack rspec` and should have an evenly
+ distributed share of tests:
+ - It works because the jobs have access to the
+ `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` since the "artifacts
+ from all previous stages are passed by default". [^1]
+ - the jobs set their own report path to
+ `KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`.
+ - if knapsack is doing its job, test files that are run should be listed under
+ `Report specs`, not under `Leftover specs`.
+1. The `update-knapsack` job takes all the
+ `knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`
+ files from the `rspec x y` jobs and merge them all together into a single
+ `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file that is then
+ uploaded to S3.
+
+After that, the next pipeline will use the up-to-date
+`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy
+is used for Spinach tests as well.
+
+### Monitoring
+
+The GitLab test suite is [monitored] for the `master` branch, and any branch
+that includes `rspec-profile` in their name.
+
+A [public dashboard] is available for everyone to see. Feel free to look at the
+slowest test files and try to improve them.
+
+[monitored]: ../performance.md#rspec-profiling
+[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default
+
+## CI setup
+
+- On CE and EE, the test suite runs both PostgreSQL and MySQL.
+- Rails logging to `log/test.log` is disabled by default in CI [for
+ performance reasons][logging]. To override this setting, provide the
+ `RAILS_ENABLE_TEST_LOG` environment variable.
+
+[logging]: https://jtway.co/speed-up-your-rails-test-suite-by-6-in-1-line-13fedb869ec4
+
+---
+
+[Return to Testing documentation](index.md)
diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md
new file mode 100644
index 00000000000..bbb2313ea7b
--- /dev/null
+++ b/doc/development/testing_guide/flaky_tests.md
@@ -0,0 +1,74 @@
+# Flaky tests
+
+## What's a flaky test?
+
+It's a test that sometimes fails, but if you retry it enough times, it passes,
+eventually.
+
+## Automatic retries and flaky tests detection
+
+On our CI, we use [rspec-retry] to automatically retry a failing example a few
+times (see [`spec/spec_helper.rb`] for the precise retries count).
+
+We also use a home-made `RspecFlaky::Listener` listener which records flaky
+examples in a JSON report file on `master` (`retrieve-tests-metadata` and `update-tests-metadata` jobs), and warns when a new flaky example
+is detected in any other branch (`flaky-examples-check` job). In the future, the
+`flaky-examples-check` job will not be allowed to fail.
+
+This was originally implemented in: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13021.
+
+[rspec-retry]: https://github.com/NoRedInk/rspec-retry
+[`spec/spec_helper.rb`]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/spec_helper.rb
+
+## Problems we had in the past at GitLab
+
+- [`rspec-retry` is bitting us when some API specs fail](https://gitlab.com/gitlab-org/gitlab-ce/issues/29242): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9825
+- [Sporadic RSpec failures due to `PG::UniqueViolation`](https://gitlab.com/gitlab-org/gitlab-ce/issues/28307#note_24958837): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9846
+ - Follow-up: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10688
+ - [Capybara.reset_session! should be called before requests are blocked](https://gitlab.com/gitlab-org/gitlab-ce/issues/33779): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12224
+- FFaker generates funky data that tests are not ready to handle (and tests should be predictable so that's bad!):
+ - [Make `spec/mailers/notify_spec.rb` more robust](https://gitlab.com/gitlab-org/gitlab-ce/issues/20121): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10015
+ - [Transient failure in spec/requests/api/commits_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/27988#note_25342521): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9944
+ - [Replace FFaker factory data with sequences](https://gitlab.com/gitlab-org/gitlab-ce/issues/29643): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10184
+ - [Transient failure in spec/finders/issues_finder_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/30211#note_26707685): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10404
+
+### Time-sensitive flaky tests
+
+- https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10046
+- https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10306
+
+### Array order expectation
+
+- https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10148
+
+### Feature tests
+
+- [Be sure to create all the data the test need before starting exercize](https://gitlab.com/gitlab-org/gitlab-ce/issues/32622#note_31128195): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12059
+- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34609#note_34048715): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12604
+- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34698#note_34276286): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12664
+- [Assert against the underlying database state instead of against a page's content](https://gitlab.com/gitlab-org/gitlab-ce/issues/31437): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10934
+
+#### Capybara viewport size related issues
+
+- [Transient failure of spec/features/issues/filtered_search/filter_issues_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/29241#note_26743936): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10411
+
+#### Capybara JS driver related issues
+
+- [Don't wait for AJAX when no AJAX request is fired](https://gitlab.com/gitlab-org/gitlab-ce/issues/30461): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10454
+- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34647): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12626
+
+#### PhantomJS / WebKit related issues
+
+- Memory is through the roof! (TL;DR: Load images but block images requests!): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12003
+
+## Resources
+
+- [Flaky Tests: Are You Sure You Want to Rerun Them?](http://semaphoreci.com/blog/2017/04/20/flaky-tests.html)
+- [How to Deal With and Eliminate Flaky Tests](https://semaphoreci.com/community/tutorials/how-to-deal-with-and-eliminate-flaky-tests)
+- [Tips on Treating Flakiness in your Rails Test Suite](http://semaphoreci.com/blog/2017/08/03/tips-on-treating-flakiness-in-your-test-suite.html)
+- ['Flaky' tests: a short story](https://www.ombulabs.com/blog/rspec/continuous-integration/how-to-track-down-a-flaky-test.html)
+- [Using Insights to Discover Flaky, Slow, and Failed Tests](https://circleci.com/blog/using-insights-to-discover-flaky-slow-and-failed-tests/)
+
+---
+
+[Return to Testing documentation](index.md)
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
new file mode 100644
index 00000000000..0c63f51cb45
--- /dev/null
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -0,0 +1,254 @@
+# Frontend testing standards and style guidelines
+
+There are two types of test suites you'll encounter while developing frontend code
+at GitLab. We use Karma and Jasmine for JavaScript unit and integration testing,
+and RSpec feature tests with Capybara for e2e (end-to-end) integration testing.
+
+Unit and feature tests need to be written for all new features.
+Most of the time, you should use [RSpec] for your feature tests.
+
+Regression tests should be written for bug fixes to prevent them from recurring
+in the future.
+
+See the [Testing Standards and Style Guidelines](index.md) page for more
+information on general testing practices at GitLab.
+
+## Karma test suite
+
+GitLab uses the [Karma][karma] test runner with [Jasmine] as its test
+framework for our JavaScript unit and integration tests. For integration tests,
+we generate HTML files using RSpec (see `spec/javascripts/fixtures/*.rb` for examples).
+Some fixtures are still HAML templates that are translated to HTML files using the same mechanism (see `static_fixtures.rb`).
+Adding these static fixtures should be avoided as they are harder to keep up to date with real views.
+The existing static fixtures will be migrated over time.
+Please see [gitlab-org/gitlab-ce#24753](https://gitlab.com/gitlab-org/gitlab-ce/issues/24753) to track our progress.
+Fixtures are served during testing by the [jasmine-jquery][jasmine-jquery] plugin.
+
+JavaScript tests live in `spec/javascripts/`, matching the folder structure
+of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js`
+has a corresponding `spec/javascripts/behaviors/autosize_spec.js` file.
+
+Keep in mind that in a CI environment, these tests are run in a headless
+browser and you will not have access to certain APIs, such as
+[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
+which will have to be stubbed.
+
+### Best practices
+
+#### Naming unit tests
+
+When writing describe test blocks to test specific functions/methods,
+please use the method name as the describe block name.
+
+```javascript
+// Good
+describe('methodName', () => {
+ it('passes', () => {
+ expect(true).toEqual(true);
+ });
+});
+
+// Bad
+describe('#methodName', () => {
+ it('passes', () => {
+ expect(true).toEqual(true);
+ });
+});
+
+// Bad
+describe('.methodName', () => {
+ it('passes', () => {
+ expect(true).toEqual(true);
+ });
+});
+```
+#### Testing promises
+
+When testing Promises you should always make sure that the test is asynchronous and rejections are handled.
+Your Promise chain should therefore end with a call of the `done` callback and `done.fail` in case an error occurred.
+
+```javascript
+// Good
+it('tests a promise', (done) => {
+ promise
+ .then((data) => {
+ expect(data).toBe(asExpected);
+ })
+ .then(done)
+ .catch(done.fail);
+});
+
+// Good
+it('tests a promise rejection', (done) => {
+ promise
+ .then(done.fail)
+ .catch((error) => {
+ expect(error).toBe(expectedError);
+ })
+ .then(done)
+ .catch(done.fail);
+});
+
+// Bad (missing done callback)
+it('tests a promise', () => {
+ promise
+ .then((data) => {
+ expect(data).toBe(asExpected);
+ })
+});
+
+// Bad (missing catch)
+it('tests a promise', (done) => {
+ promise
+ .then((data) => {
+ expect(data).toBe(asExpected);
+ })
+ .then(done)
+});
+
+// Bad (use done.fail in asynchronous tests)
+it('tests a promise', (done) => {
+ promise
+ .then((data) => {
+ expect(data).toBe(asExpected);
+ })
+ .then(done)
+ .catch(fail)
+});
+
+// Bad (missing catch)
+it('tests a promise rejection', (done) => {
+ promise
+ .catch((error) => {
+ expect(error).toBe(expectedError);
+ })
+ .then(done)
+});
+```
+
+#### Stubbing
+
+For unit tests, you should stub methods that are unrelated to the current unit you are testing.
+If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely.
+
+For integration tests, you should stub methods that will effect the stability of the test if they
+execute their original behaviour. i.e. Network requests.
+
+### Vue.js unit tests
+
+See this [section][vue-test].
+
+### Running frontend tests
+
+`rake karma` runs the frontend-only (JavaScript) tests.
+It consists of two subtasks:
+
+- `rake karma:fixtures` (re-)generates fixtures
+- `rake karma:tests` actually executes the tests
+
+As long as the fixtures don't change, `rake karma:tests` (or `yarn karma`)
+is sufficient (and saves you some time).
+
+### Live testing and focused testing
+
+While developing locally, it may be helpful to keep karma running so that you
+can get instant feedback on as you write tests and modify code. To do this
+you can start karma with `npm run karma-start`. It will compile the javascript
+assets and run a server at `http://localhost:9876/` where it will automatically
+run the tests on any browser which connects to it. You can enter that url on
+multiple browsers at once to have it run the tests on each in parallel.
+
+While karma is running, any changes you make will instantly trigger a recompile
+and retest of the entire test suite, so you can see instantly if you've broken
+a test with your changes. You can use [jasmine focused][jasmine-focus] or
+excluded tests (with `fdescribe` or `xdescribe`) to get karma to run only the
+tests you want while you're working on a specific feature, but make sure to
+remove these directives when you commit your code.
+
+## RSpec feature integration tests
+
+Information on setting up and running RSpec integration tests with
+[Capybara] can be found in the [Testing Best Practices](best_practices.md).
+
+## Gotchas
+
+### Errors due to use of unsupported JavaScript features
+
+Similar errors will be thrown if you're using JavaScript features not yet
+supported by the PhantomJS test runner which is used for both Karma and RSpec
+tests. We polyfill some JavaScript objects for older browsers, but some
+features are still unavailable:
+
+- Array.from
+- Array.first
+- Async functions
+- Generators
+- Array destructuring
+- For..Of
+- Symbol/Symbol.iterator
+- Spread
+
+Until these are polyfilled appropriately, they should not be used. Please
+update this list with additional unsupported features.
+
+### RSpec errors due to JavaScript
+
+By default RSpec unit tests will not run JavaScript in the headless browser
+and will simply rely on inspecting the HTML generated by rails.
+
+If an integration test depends on JavaScript to run correctly, you need to make
+sure the spec is configured to enable JavaScript when the tests are run. If you
+don't do this you'll see vague error messages from the spec runner.
+
+To enable a JavaScript driver in an `rspec` test, add `:js` to the
+individual spec or the context block containing multiple specs that need
+JavaScript enabled:
+
+```ruby
+# For one spec
+it 'presents information about abuse report', :js do
+ # assertions...
+end
+
+describe "Admin::AbuseReports", :js do
+ it 'presents information about abuse report' do
+ # assertions...
+ end
+ it 'shows buttons for adding to abuse report' do
+ # assertions...
+ end
+end
+```
+
+### Spinach errors due to missing JavaScript
+
+NOTE: **Note:** Since we are discouraging the use of Spinach when writing new
+feature tests, you shouldn't ever need to use this. This information is kept
+available for legacy purposes only.
+
+In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
+file for the failing spec, add the `@javascript` flag above the Scenario:
+
+```
+@javascript
+Scenario: Developer can approve merge request
+ Given I am a "Shop" developer
+ And I visit project "Shop" merge requests page
+ And merge request 'Bug NS-04' must be approved
+ And I click link "Bug NS-04"
+ When I click link "Approve"
+ Then I should see approved merge request "Bug NS-04"
+```
+
+[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
+[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
+[karma]: http://karma-runner.github.io/
+[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
+[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
+[Capybara]: https://github.com/teamcapybara/capybara
+[Karma]: http://karma-runner.github.io/
+[Jasmine]: https://jasmine.github.io/
+
+---
+
+[Return to Testing documentation](index.md)
diff --git a/doc/development/fe_guide/img/testing_triangle.png b/doc/development/testing_guide/img/testing_triangle.png
index 7a9a848c2ee..7a9a848c2ee 100644
--- a/doc/development/fe_guide/img/testing_triangle.png
+++ b/doc/development/testing_guide/img/testing_triangle.png
Binary files differ
diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md
new file mode 100644
index 00000000000..8045bbad7ba
--- /dev/null
+++ b/doc/development/testing_guide/index.md
@@ -0,0 +1,91 @@
+# Testing standards and style guidelines
+
+This document describes various guidelines and best practices for automated
+testing of the GitLab project.
+
+It is meant to be an _extension_ of the [thoughtbot testing
+styleguide](https://github.com/thoughtbot/guides/tree/master/style/testing). If
+this guide defines a rule that contradicts the thoughtbot guide, this guide
+takes precedence. Some guidelines may be repeated verbatim to stress their
+importance.
+
+## Overview
+
+GitLab is built on top of [Ruby on Rails][rails], and we're using [RSpec] for all
+the backend tests, with [Capybara] for end-to-end integration testing.
+On the frontend side, we're using [Karma] and [Jasmine] for JavaScript unit and
+integration testing.
+
+Following are two great articles that everyone should read to understand what
+automated testing means, and what are its principles:
+
+- [Five Factor Testing](https://www.devmynd.com/blog/five-factor-testing): Why do we need tests?
+- [Principles of Automated Testing](http://www.lihaoyi.com/post/PrinciplesofAutomatedTesting.html): Levels of testing. Prioritize tests. Cost of tests.
+
+---
+
+## [Testing levels](testing_levels.md)
+
+Learn about the different testing levels, and how to decide at what level your
+changes should be tested.
+
+---
+
+## [Testing best practices](best_practices.md)
+
+Everything you should know about how to write good tests: RSpec, FactoryGirl,
+system tests, parameterized tests etc.
+
+---
+
+## [Frontend testing standards and style guidelines](frontend_testing.md)
+
+Everything you should know about how to write good Frontend tests: Karma,
+testing promises, stubbing etc.
+
+---
+
+## [Flaky tests](flaky_tests.md)
+
+What are flaky tests, the different kind of flaky tests we encountered, and what
+we do about them.
+
+---
+
+## [GitLab tests in the Continuous Integration (CI) context](ci.md)
+
+How GitLab test suite is run in the CI context: setup, caches, artifacts,
+parallelization, monitoring.
+
+---
+
+## [Testing Rake tasks](testing_rake_tasks.md)
+
+Everything you should know about how to test Rake tasks.
+
+---
+
+## Spinach (feature) tests
+
+GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
+for its feature/integration tests in September 2012.
+
+As of March 2016, we are [trying to avoid adding new Spinach
+tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward,
+opting for [RSpec feature](#features-integration) specs.
+
+Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
+no more than one new `step` definition. If more than that is required, the
+test should be re-implemented using RSpec instead.
+
+---
+
+[Return to Development documentation](../README.md)
+
+[^1]: /ci/yaml/README.html#dependencies
+
+[rails]: http://rubyonrails.org/
+[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
+[Capybara]: https://github.com/teamcapybara/capybara
+[Karma]: http://karma-runner.github.io/
+[Jasmine]: https://jasmine.github.io/
diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md
new file mode 100644
index 00000000000..1cbd4350284
--- /dev/null
+++ b/doc/development/testing_guide/testing_levels.md
@@ -0,0 +1,173 @@
+# Testing levels
+
+![Testing priority triangle](img/testing_triangle.png)
+
+_This diagram demonstrates the relative priority of each test type we use. `e2e` stands for end-to-end._
+
+## Unit tests
+
+Formal definition: https://en.wikipedia.org/wiki/Unit_testing
+
+These kind of tests ensure that a single unit of code (a method) works as
+expected (given an input, it has a predictable output). These tests should be
+isolated as much as possible. For example, model methods that don't do anything
+with the database shouldn't need a DB record. Classes that don't need database
+records should use stubs/doubles as much as possible.
+
+| Code path | Tests path | Testing engine | Notes |
+| --------- | ---------- | -------------- | ----- |
+| `app/finders/` | `spec/finders/` | RSpec | |
+| `app/helpers/` | `spec/helpers/` | RSpec | |
+| `app/db/{post_,}migrate/` | `spec/migrations/` | RSpec | More details at [`spec/migrations/README.md`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md). |
+| `app/policies/` | `spec/policies/` | RSpec | |
+| `app/presenters/` | `spec/presenters/` | RSpec | |
+| `app/routing/` | `spec/routing/` | RSpec | |
+| `app/serializers/` | `spec/serializers/` | RSpec | |
+| `app/services/` | `spec/services/` | RSpec | |
+| `app/tasks/` | `spec/tasks/` | RSpec | |
+| `app/uploaders/` | `spec/uploaders/` | RSpec | |
+| `app/views/` | `spec/views/` | RSpec | |
+| `app/workers/` | `spec/workers/` | RSpec | |
+| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [Frontent Testing guide](frontend_testing.md) section. |
+
+## Integration tests
+
+Formal definition: https://en.wikipedia.org/wiki/Integration_testing
+
+These kind of tests ensure that individual parts of the application work well together, without the overhead of the actual app environment (i.e. the browser). These tests should assert at the request/response level: status code, headers, body. They're useful to test permissions, redirections, what view is rendered etc.
+
+| Code path | Tests path | Testing engine | Notes |
+| --------- | ---------- | -------------- | ----- |
+| `app/controllers/` | `spec/controllers/` | RSpec | |
+| `app/mailers/` | `spec/mailers/` | RSpec | |
+| `lib/api/` | `spec/requests/api/` | RSpec | |
+| `lib/ci/api/` | `spec/requests/ci/api/` | RSpec | |
+| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. |
+
+### About controller tests
+
+In an ideal world, controllers should be thin. However, when this is not the
+case, it's acceptable to write a system/feature test without JavaScript instead
+of a controller test. The reason is that testing a fat controller usually
+involves a lot of stubbing, things like:
+
+```ruby
+controller.instance_variable_set(:@user, user)
+```
+
+and use methods which are deprecated in Rails 5 ([#23768]).
+
+[#23768]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23768
+
+### About Karma
+
+As you may have noticed, Karma is both in the Unit tests and the Integration
+tests category. That's because Karma is a tool that provides an environment to
+run JavaScript tests, so you can either run unit tests (e.g. test a single
+JavaScript method), or integration tests (e.g. test a component that is composed
+of multiple components).
+
+## System tests or feature tests
+
+Formal definition: https://en.wikipedia.org/wiki/System_testing.
+
+These kind of tests ensure the application works as expected from a user point
+of view (aka black-box testing). These tests should test a happy path for a
+given page or set of pages, and a test case should be added for any regression
+that couldn't have been caught at lower levels with better tests (i.e. if a
+regression is found, regression tests should be added at the lowest-level
+possible).
+
+| Tests path | Testing engine | Notes |
+| ---------- | -------------- | ----- |
+| `spec/features/` | [Capybara] + [RSpec] | If your spec has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. |
+| `features/` | Spinach | Spinach tests are deprecated, [you shouldn't add new Spinach tests](#spinach-feature-tests). |
+
+### Consider **not** writing a system test!
+
+If we're confident that the low-level components work well (and we should be if
+we have enough Unit & Integration tests), we shouldn't need to duplicate their
+thorough testing at the System test level.
+
+It's very easy to add tests, but a lot harder to remove or improve tests, so one
+should take care of not introducing too many (slow and duplicated) specs.
+
+The reasons why we should follow these best practices are as follows:
+
+- System tests are slow to run since they spin up the entire application stack
+ in a headless browser, and even slower when they integrate a JS driver
+- When system tests run with a JavaScript driver, the tests are run in a
+ different thread than the application. This means it does not share a
+ database connection and your test will have to commit the transactions in
+ order for the running application to see the data (and vice-versa). In that
+ case we need to truncate the database after each spec instead of simply
+ rolling back a transaction (the faster strategy that's in use for other kind
+ of tests). This is slower than transactions, however, so we want to use
+ truncation only when necessary.
+
+[Poltergeist]: https://github.com/teamcapybara/capybara#poltergeist
+[RackTest]: https://github.com/teamcapybara/capybara#racktest
+
+## Black-box tests or end-to-end tests
+
+GitLab consists of [multiple pieces] such as [GitLab Shell], [GitLab Workhorse],
+[Gitaly], [GitLab Pages], [GitLab Runner], and GitLab Rails. All theses pieces
+are configured and packaged by [GitLab Omnibus].
+
+[GitLab QA] is a tool that allows to test that all these pieces integrate well
+together by building a Docker image for a given version of GitLab Rails and
+running feature tests (i.e. using Capybara) against it.
+
+The actual test scenarios and steps are [part of GitLab Rails] so that they're
+always in-sync with the codebase.
+
+[multiple pieces]: ../architecture.md#components
+[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell
+[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
+[Gitaly]: https://gitlab.com/gitlab-org/gitaly
+[GitLab Pages]: https://gitlab.com/gitlab-org/gitlab-pages
+[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-runner
+[GitLab Omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab
+[GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa
+[part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa
+
+## How to test at the correct level?
+
+As many things in life, deciding what to test at each level of testing is a
+trade-off:
+
+- Unit tests are usually cheap, and you should consider them like the basement
+ of your house: you need them to be confident that your code is behaving
+ correctly. However if you run only unit tests without integration / system
+ tests, you might [miss] the [big] [picture]!
+- Integration tests are a bit more expensive, but don't abuse them. A system test
+ is often better than an integration test that is stubbing a lot of internals.
+- System tests are expensive (compared to unit tests), even more if they require
+ a JavaScript driver. Make sure to follow the guidelines in the [Speed](#test-speed)
+ section.
+
+Another way to see it is to think about the "cost of tests", this is well
+explained [in this article][tests-cost] and the basic idea is that the cost of a
+test includes:
+
+- The time it takes to write the test
+- The time it takes to run the test every time the suite runs
+- The time it takes to understand the test
+- The time it takes to fix the test if it breaks and the underlying code is OK
+- Maybe, the time it takes to change the code to make the code testable.
+
+### Frontend-related tests
+
+There are cases where the behaviour you are testing is not worth the time spent
+running the full application, for example, if you are testing styling, animation,
+edge cases or small actions that don't involve the backend,
+you should write an integration test using Jasmine.
+
+[miss]: https://twitter.com/ThePracticalDev/status/850748070698651649
+[big]: https://twitter.com/timbray/status/822470746773409794
+[picture]: https://twitter.com/withzombies/status/829716565834752000
+[tests-cost]: https://medium.com/table-xi/high-cost-tests-and-high-value-tests-a86e27a54df#.2ulyh3a4e
+
+---
+
+[Return to Testing documentation](index.md)
diff --git a/doc/development/testing_guide/testing_rake_tasks.md b/doc/development/testing_guide/testing_rake_tasks.md
new file mode 100644
index 00000000000..60163f1a230
--- /dev/null
+++ b/doc/development/testing_guide/testing_rake_tasks.md
@@ -0,0 +1,39 @@
+# Testing Rake tasks
+
+To make testing Rake tasks a little easier, there is a helper that can be included
+in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use
+`require 'rake_helper'`. The helper includes `spec_helper` for you, and configures
+a few other things to make testing Rake tasks easier.
+
+At a minimum, requiring the Rake helper will redirect `stdout`, include the
+runtime task helpers, and include the `RakeHelpers` Spec support module.
+
+The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make
+executing tasks simple. See `spec/support/rake_helpers.rb` for all available
+methods.
+
+Example:
+
+```ruby
+require 'rake_helper'
+
+describe 'gitlab:shell rake tasks' do
+ before do
+ Rake.application.rake_require 'tasks/gitlab/shell'
+
+ stub_warn_user_is_not_gitlab
+ end
+
+ describe 'install task' do
+ it 'invokes create_hooks task' do
+ expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
+
+ run_rake_task('gitlab:shell:install')
+ end
+ end
+end
+```
+
+---
+
+[Return to Testing documentation](index.md)
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
index fa31c496b30..16a811dbc74 100644
--- a/doc/development/ux_guide/components.md
+++ b/doc/development/ux_guide/components.md
@@ -10,6 +10,7 @@
* [Tables](#tables)
* [Blocks](#blocks)
* [Panels](#panels)
+* [Dialog modals](#dialog-modals)
* [Alerts](#alerts)
* [Forms](#forms)
* [Search box](#search-box)
@@ -254,6 +255,38 @@ Skeleton loading can replace any existing UI elements for the period in which th
---
+## Dialog modals
+
+Dialog modals are only used for having a conversation and confirmation with the user. The user is not able to access the features on the main page until closing the modal.
+
+### Usage
+
+* When the action is irreversible, dialog modals provide the details and confirm with the user before they take an advanced action.
+* When the action will affect privacy or authorization, dialog modals provide advanced information and confirm with the user.
+
+### Style
+
+* Dialog modals contain the header, body, and actions.
+ * **Header(1):** The header title is a question instead of a descriptive phrase.
+ * **Body(2):** The content in body should never be ambiguous and unclear. It provides specific information.
+ * **Actions(3):** Contains a affirmative action, a dismissive action, and an extra action. The order of actions from left to right: Dismissive action → Extra action → Affirmative action
+* Confirmations regarding labels should keep labeling styling.
+* References to commits, branches, and tags should be **monospaced**.
+
+![layout-modal](img/modals-layout-for-modals.png)
+
+### Placement
+
+* Dialog modals should always be the center of the screen horizontally and be positioned **72px** from the top.
+
+| Dialog with 2 actions | Dialog with 3 actions | Special confirmation |
+| --------------------- | --------------------- | -------------------- |
+| ![two-actions](img/modals-general-confimation-dialog.png) | ![three-actions](img/modals-three-buttons.png) | ![spcial-confirmation](img/modals-special-confimation-dialog.png) |
+
+> TODO: Special case for dialog modal.
+
+---
+
## Panels
> TODO: Catalog how we are currently using panels and rationalize how they relate to alerts
diff --git a/doc/development/ux_guide/img/modals-general-confimation-dialog.png b/doc/development/ux_guide/img/modals-general-confimation-dialog.png
new file mode 100644
index 00000000000..00a17374a0b
--- /dev/null
+++ b/doc/development/ux_guide/img/modals-general-confimation-dialog.png
Binary files differ
diff --git a/doc/development/ux_guide/img/modals-layout-for-modals.png b/doc/development/ux_guide/img/modals-layout-for-modals.png
new file mode 100644
index 00000000000..6c7bc09e750
--- /dev/null
+++ b/doc/development/ux_guide/img/modals-layout-for-modals.png
Binary files differ
diff --git a/doc/development/ux_guide/img/modals-special-confimation-dialog.png b/doc/development/ux_guide/img/modals-special-confimation-dialog.png
new file mode 100644
index 00000000000..bf1e56326c5
--- /dev/null
+++ b/doc/development/ux_guide/img/modals-special-confimation-dialog.png
Binary files differ
diff --git a/doc/development/ux_guide/img/modals-three-buttons.png b/doc/development/ux_guide/img/modals-three-buttons.png
new file mode 100644
index 00000000000..519439e64e4
--- /dev/null
+++ b/doc/development/ux_guide/img/modals-three-buttons.png
Binary files differ
diff --git a/doc/development/ux_guide/resources.md b/doc/development/ux_guide/resources.md
index 2f760c94414..db57258237f 100644
--- a/doc/development/ux_guide/resources.md
+++ b/doc/development/ux_guide/resources.md
@@ -10,4 +10,8 @@ you can use during GitLab development.
## Design repository
All design files are stored in the [gitlab-design](https://gitlab.com/gitlab-org/gitlab-design)
-repository and maintained by GitLab UX designers. \ No newline at end of file
+repository and maintained by GitLab UX designers.
+
+## [brand.ai](https://brand.ai/git-lab/primary-brand)
+
+We are in the process of capturing our UI paradigms on brand.ai, see https://brand.ai/git-lab/primary-brand
diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md
index cbd7c17de41..fce882a45f1 100644
--- a/doc/development/ux_guide/users.md
+++ b/doc/development/ux_guide/users.md
@@ -1,4 +1,5 @@
-## UX Personas
+# UX Personas
+
* [Nazim Ramesh](#nazim-ramesh)
- Small to medium size organisations using GitLab CE
* [James Mackey](#james-mackey)
@@ -7,16 +8,16 @@
* [Karolina Plaskaty](#karolina-plaskaty)
- Using GitLab.com for personal/hobby projects
- Would like to use GitLab at work
- - Working for a medium to large size organisation
+ - Working for a medium to large size organisation
-<hr>
+---
-### Nazim Ramesh
+## Nazim Ramesh
- Small to medium size organisations using GitLab CE
<img src="img/nazim-ramesh.png" width="300px">
-#### Demographics
+### Demographics
- **Age**<br>32 years old
- **Location**<br>Germany
@@ -26,7 +27,7 @@
- **Frequently used programming languages**<br>JavaScript, SQL, PHP
- **Hobbies / interests**<br>Functional programming, open source, gaming, web development and web security.
-#### Motivations
+### Motivations
Nazim works for a software development company which currently hires around 80 people. When Nazim first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Nazim felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Nazim began comparing source control tools. A search for “self-hosted Git server repository management†returned GitLab. In his own words, Nazim explains why he wanted the engineering team to start using GitLab:
>
@@ -39,48 +40,48 @@ In his role as a full-stack web developer, Nazim could recommend products that h
“The biggest challenge...why should we change anything at all from the status quo? We needed to switch from SVN to Git. They knew they needed to learn Git and a Git workflow...using Git was scary to my colleagues...they thought it was more complex than SVN to use.â€
>
-Undeterred, Nazim decided to migrate a couple of projects across to GitLab.
+Undeterred, Nazim decided to migrate a couple of projects across to GitLab.
>
“Old SVN users couldn’t see the benefits of Git at first. It took a month or two to convince them.â€
>
-Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab.
+Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab.
-The engineering team have been using GitLab CE for around 2 years now. Nazim credits himself as being entirely responsible for his company’s decision to move to GitLab.
+The engineering team have been using GitLab CE for around 2 years now. Nazim credits himself as being entirely responsible for his company’s decision to move to GitLab.
-#### Frustrations
-##### Adoption to GitLab has been slow
+### Frustrations
+#### Adoption to GitLab has been slow
Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Nazim sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Nazim hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits.
-##### Missing Features
+#### Missing Features
Nazim’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Nazim’s company wants to know if GitLab has a specific feature or does a particular thing, Nazim is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Nazim gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do.
-##### Regressions and bugs
+#### Regressions and bugs
Nazim often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks somethingâ€. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.†Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month.
-##### Uses too much RAM and CPU
+#### Uses too much RAM and CPU
>
“Memory usages mean that if we host it from a cloud based host like AWS, we spend almost as much on the instance as what we would pay GitHubâ€
>
-##### UI/UX
+#### UI/UX
GitLab’s interface initially attracted Nazim when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.â€
-#### Goals
+### Goals
* To convince his colleagues to fully adopt GitLab CE, thus improving workflow and collaboration.
* To use a feature rich version control platform that covers all stages of the development lifecycle, in order to reduce dependencies on other tools.
* To use an intuitive and stable product, so he can spend more time on his core job responsibilities and less time bug-fixing, guiding colleagues, etc.
-<hr>
+---
-### James Mackey
+## James Mackey
- Medium to large size organisations using CE or EE
- Small organisations using EE
<img src="img/james-mackey.png" width="300px">
-#### Demographics
+### Demographics
- **Age**<br>36 years old
- **Location**<br>US
@@ -90,7 +91,7 @@ GitLab’s interface initially attracted Nazim when he was comparing version con
- **Frequently used programming languages**<br>JavaScript, SQL, Node.js, Java, PHP, Python
- **Hobbies / interests**<br>DevOps, open source, web development, science, automation and electronics.
-#### Motivations
+### Motivations
James works for a research company which currently hires around 800 staff. He began using GitLab.com back in 2013 for his own open source, hobby projects and loved “the simplicity of installation, administration and useâ€. After using GitLab for over a year, he began to wonder about using it at work. James explains:
>
@@ -99,7 +100,7 @@ James works for a research company which currently hires around 800 staff. He be
James and his colleagues also reviewed competitor products including GitHub Enterprise, but they found it “less innovative and with considerable costs...GitLab had the features we wanted at a much lower cost per head than GitHubâ€.
-The company James works for provides employees with a discretionary budget to spend how they want on software, so James and his team decided to upgrade to EE.
+The company James works for provides employees with a discretionary budget to spend how they want on software, so James and his team decided to upgrade to EE.
James feels partially responsible for his organisation’s decision to start using GitLab.
@@ -107,33 +108,33 @@ James feels partially responsible for his organisation’s decision to start usi
“It's still up to the teams themselves [to decide] which tools to use. We just had a great experience moving our daily development to GitLab, so other teams have followed the path or are thinking about switching.â€
>
-#### Frustrations
-##### Third Party Integration
+### Frustrations
+#### Third Party Integration
Some of GitLab EE’s features are too basic, in particular, issues boards which do not have the level of reporting that James and his team need. Subsequently, they still need to use GitLab EE in conjunction with other tools, such as JIRA. Whilst James feels it isn’t essential for GitLab to meet all his needs (his company are happy for him to use, and pay for, multiple tools), he sometimes isn’t sure what is/isn’t possible with plugins and what level of custom development he and his team will need to do.
-##### UX/UI
+#### UX/UI
James and his team use CI quite heavily for several projects. Whilst they’ve welcomed improvements to the builds and pipelines interface, they still have some difficulty following build process on the different tabs under Pipelines. Some confusion has arisen from not knowing where to find different pieces of information or how to get to the next stages logs from the current stage’s log output screen. They feel more intuitive linking and flow may alleviate the problem. Generally, they feel GitLab’s navigation needs to reviewed and optimised.
-##### Permissions
+#### Permissions
>
“There is no granular control over user or group permissions. The permissions for a project are too tightly coupled to the permissions for Gitlab CI/build pipelines.â€
>
-#### Goals
+### Goals
* To be able to integrate third party tools easily with GitLab EE and to create custom integrations and patches where needed.
* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. James and his team want to be able to understand and use these particular features easily.
* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to James.
-<hr>
+---
-### Karolina Plaskaty
+## Karolina Plaskaty
- Using GitLab.com for personal/hobby projects
- Would like to use GitLab at work
-- Working for a medium to large size organisation
+- Working for a medium to large size organisation
<img src="img/karolina-plaskaty.png" width="300px">
-#### Demographics
+### Demographics
- **Age**<br>26 years old
- **Location**<br>UK
@@ -143,22 +144,22 @@ James and his team use CI quite heavily for several projects. Whilst they’ve w
- **Frequently used programming languages**<br>JavaScript and SQL
- **Hobbies / interests**<br>Web development, mobile development, UX, open source, gaming and travel.
-#### Motivations
+### Motivations
Karolina has been using GitLab.com for around a year. She roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Karolina contributes to open source projects to gain programming experience and to give back to the community. She likes GitLab.com for its free private repositories and range of features which provide her with everything she needs for her personal projects. Karolina is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a companyâ€. She explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.†She’s also an avid reader of GitLab’s blog.
Karolina works for a software development company which currently hires around 500 people. Karolina would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. She describes management at her company as “old fashioned†and explains that it’s “less of a technical issue and more of a cultural issue†to convince upper management to move to GitLab. Karolina is also relatively new to the company so she’s apprehensive about pushing too hard to change version control platforms.
-#### Frustrations
-##### Unable to use GitLab at work
+### Frustrations
+#### Unable to use GitLab at work
Karolina wants to use GitLab at work but isn’t sure how to approach the subject with management. In her current role, she doesn’t feel that she has the authority to request GitLab.
-##### Performance
+#### Performance
GitLab.com is frequently slow and unavailable. Karolina has also heard that GitLab is a “memory hog†which has deterred her from running GitLab on her own machine for just hobby / personal projects.
-##### UX/UI
+#### UX/UI
Karolina has an interest in UX and therefore has strong opinions about how GitLab should look and feel. She feels the interface is cluttered, “it has too many links/buttons†and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.†As Karolina also enjoys contributing to open-source projects, it’s important to her that GitLab is well designed for public repositories, she doesn’t feel that GitLab currently achieves this.
-#### Goals
+### Goals
* To develop her programming experience and to learn from other developers.
* To contribute to both her own and other open source projects.
-* To use a fast and intuitive version control platform. \ No newline at end of file
+* To use a fast and intuitive version control platform.
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index c4830322fa8..05e0a64af18 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -37,7 +37,7 @@ when using the migration helper method
`Gitlab::Database::MigrationHelpers#add_column_with_default`. This method works
similar to `add_column` except it updates existing rows in batches without
blocking access to the table being modified. See ["Adding Columns With Default
-Values"](migration_style_guide.html#adding-columns-with-default-values) for more
+Values"](migration_style_guide.md#adding-columns-with-default-values) for more
information on how to use this method.
## Dropping Columns
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index 3d893ba53dd..4e15f7cfd49 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab basics
Step-by-step guides on the basics of working with Git and GitLab.
diff --git a/doc/gitlab-basics/add-merge-request.md b/doc/gitlab-basics/add-merge-request.md
index bf01fe51dc3..5cc014419ad 100644
--- a/doc/gitlab-basics/add-merge-request.md
+++ b/doc/gitlab-basics/add-merge-request.md
@@ -3,31 +3,28 @@
Merge requests are useful to integrate separate changes that you've made to a
project, on different branches. This is a brief guide on how to create a merge
request. For more information, check the
-[merge requests documentation](../user/project/merge_requests.md).
+[merge requests documentation](../user/project/merge_requests/index.md).
---
1. Before you start, you should have already [created a branch](create-branch.md)
and [pushed your changes](basic-git-commands.md) to GitLab.
-
-1. You can then go to the project where you'd like to merge your changes and
- click on the **Merge requests** tab.
-
- ![Merge requests](img/project_navbar.png)
-
+1. Go to the project where you'd like to merge your changes and click on the
+ **Merge requests** tab.
1. Click on **New merge request** on the right side of the screen.
-
- ![New Merge Request](img/merge_request_new.png)
-
-1. Select a source branch and click on the **Compare branches and continue** button.
+1. From there on, you have the option to select the source branch and the target
+ branch you'd like to compare to. The default target project is the upstream
+ repository, but you can choose to compare across any of its forks.
![Select a branch](img/merge_request_select_branch.png)
+1. When ready, click on the **Compare branches and continue** button.
1. At a minimum, add a title and a description to your merge request. Optionally,
select a user to review your merge request and to accept or close it. You may
also select a milestone and labels.
![New merge request page](img/merge_request_page.png)
-1. When ready, click on the **Submit merge request** button. Your merge request
- will be ready to be approved and published.
+1. When ready, click on the **Submit merge request** button.
+
+Your merge request will be ready to be approved and merged.
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index 67ef189fee9..e18711f3392 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -17,7 +17,7 @@
[Project Templates](https://gitlab.com/gitlab-org/project-templates):
this will kickstart your repository code and CI automatically.
Otherwise, if you have a project in a different repository, you can [import it] by
- clicking an **Import project from** button provided this is enabled in
+ clicking on the **Import project** tab, provided this is enabled in
your GitLab instance. Ask your administrator if not.
1. Provide the following information:
diff --git a/doc/gitlab-basics/img/create_new_project_info.png b/doc/gitlab-basics/img/create_new_project_info.png
index ef8753e224b..ce4f7d1204b 100644
--- a/doc/gitlab-basics/img/create_new_project_info.png
+++ b/doc/gitlab-basics/img/create_new_project_info.png
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_new.png b/doc/gitlab-basics/img/merge_request_new.png
deleted file mode 100644
index 6fcd7bebada..00000000000
--- a/doc/gitlab-basics/img/merge_request_new.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_select_branch.png b/doc/gitlab-basics/img/merge_request_select_branch.png
index 9f6b93943a9..57ea0e65f34 100644
--- a/doc/gitlab-basics/img/merge_request_select_branch.png
+++ b/doc/gitlab-basics/img/merge_request_select_branch.png
Binary files differ
diff --git a/doc/gitlab-basics/img/project_navbar.png b/doc/gitlab-basics/img/project_navbar.png
deleted file mode 100644
index be6f38ede32..00000000000
--- a/doc/gitlab-basics/img/project_navbar.png
+++ /dev/null
Binary files differ
diff --git a/doc/install/README.md b/doc/install/README.md
index 656f8720361..540cb0d3f38 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Installation
GitLab can be installed via various ways. Check the [installation methods][methods]
diff --git a/doc/install/installation.md b/doc/install/installation.md
index af6c797dc00..570b0d5b22f 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -80,7 +80,7 @@ Make sure you have the right version of Git installed
# Install Git
sudo apt-get install -y git-core
- # Make sure Git is version 2.13.0 or higher
+ # Make sure Git is version 2.13.6 or higher
git --version
Is the system packaged Git too old? Remove it and compile from source.
@@ -121,7 +121,7 @@ The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
in production, frequently leads to hard to diagnose problems. For example,
GitLab Shell is called from OpenSSH, and having a version manager can prevent
pushing and pulling over SSH. Version managers are not supported and we strongly
-advise everyone to follow the instructions below to use a system Ruby.
+advise everyone to follow the instructions below to use a system Ruby.
Linux distributions generally have older versions of Ruby available, so these
instructions are designed to install Ruby from the official source code.
@@ -299,9 +299,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-0-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-2-stable gitlab
-**Note:** You can change `10-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `10-2-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -367,6 +367,9 @@ sudo usermod -aG redis git
# Enable packfile bitmaps
sudo -u git -H git config --global repack.writeBitmaps true
+
+ # Enable push options
+ sudo -u git -H git config --global receive.advertisePushOptions true
# Configure Redis connection settings
sudo -u git -H cp config/resque.yml.example config/resque.yml
@@ -513,8 +516,7 @@ Check if GitLab and its environment are configured correctly:
### Compile GetText PO files
- sudo -u git -H bundle exec rake gettext:pack RAILS_ENV=production
- sudo -u git -H bundle exec rake gettext:po_to_json RAILS_ENV=production
+ sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
### Compile Assets
diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index ddfd47df099..fa564d83785 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -1,6 +1,6 @@
# GitLab Helm Chart
> **Note**:
-* This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). For more information on available charts, please see our [overview](index.md#chart-overview).
+* This chart is deprecated, and is being replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). For more information on available charts, please see our [overview](index.md#chart-overview).
* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
@@ -8,7 +8,9 @@ For more information on available GitLab Helm Charts, please see our [overview](
## Introduction
-The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. This chart requires advanced knowledge of Kubernetes to successfully use. For most deployments we **strongly recommended** the [gitlab-omnibus](gitlab_omnibus.md) chart, which will replace this chart once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). Due to the difficulty in supporting upgrades to the `omnibus-gitlab` chart, migrating will require exporting data out of this instance and importing it into the new deployment.
+The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. This chart requires advanced knowledge of Kubernetes to successfully use. We **strongly recommend** the [gitlab-omnibus](gitlab_omnibus.md) chart.
+
+This chart is deprecated, and will be replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). Due to the difficulty in supporting upgrades, migrating will require exporting data out of this instance and importing it into the new deployment.
This chart includes the following:
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
index 8c110a37380..6659c3cf7b2 100644
--- a/doc/install/kubernetes/gitlab_omnibus.md
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -1,7 +1,6 @@
# GitLab-Omnibus Helm Chart
> **Note:**
-* This Helm chart is in beta, while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being worked on.
-* GitLab is working on a [cloud native set of Charts](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) which will eventually replace these.
+* This Helm chart is in beta, and will be deprecated by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md).
* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work.
@@ -12,6 +11,8 @@ For more information on available GitLab Helm Charts, please see our [overview](
This chart provides an easy way to get started with GitLab, provisioning an installation with nearly all functionality enabled. SSL is automatically provisioned via [Let's Encrypt](https://letsencrypt.org/).
+This Helm chart is in beta, and will be deprecated by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) once available. Due to the difficulty in supporting upgrades, migrating will require exporting data out of this instance and importing it into the new deployment.
+
The deployment includes:
- A [GitLab Omnibus](https://docs.gitlab.com/omnibus/) Pod, including Mattermost, Container Registry, and Prometheus
@@ -23,9 +24,8 @@ The deployment includes:
### Limitations
-* This chart is suited for small to medium size deployments, because [High Availability](https://docs.gitlab.com/ee/administration/high_availability/) and [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html) will not be supported.
-* It is in beta. Additional features to support production deployments, like backups, are [in development](https://gitlab.com/charts/charts.gitlab.io/issues/68). Once completed, this chart will be generally available.
-* A new generation of [cloud native charts](index.md#upcoming-cloud-native-helm-charts) is in development, and will eventually deprecate these. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. We do not expect these to be production ready before the second half of 2018.
+* This chart is in beta, and suited for small to medium size deployments. [High Availability](https://docs.gitlab.com/ee/administration/high_availability/) and [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html) are not supported.
+* A new generation [cloud native GitLab chart](index.md#cloud-native-gitlab-chart) is in development, and will deprecate this chart. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. We plan to release the new chart in beta by the end of 2017.
For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index aed00ae9e2c..dd350820c18 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -9,11 +9,11 @@ should be deployed, upgraded, and configured.
## Chart Overview
-* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today. It is suited for small to medium deployments, and is in beta while support for backups and other features are added.
+* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today, suited for small to medium deployments. The chart is in beta and will be deprecated by the [cloud native GitLab chart](#cloud-native-gitlab-chart).
* **[Cloud Native GitLab Chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md)**: The next generation GitLab chart, currently in development. Will support large deployments with horizontal scaling of individual GitLab components.
* Other Charts
* [GitLab Runner Chart](gitlab_runner_chart.md): For deploying just the GitLab Runner.
- * [Advanced GitLab Installation](gitlab_chart.md): Provides additional deployment options, but provides less functionality out-of-the-box. It's beta, no longer actively developed, and will be deprecated by [gitlab-omnibus](#gitlab-omnibus-chart-recommended) once it supports these options.
+ * [Advanced GitLab Installation](gitlab_chart.md): Deprecated, being replaced by the [cloud native GitLab chart](#cloud-native-gitlab-chart). Provides additional deployment options, but provides less functionality out-of-the-box.
* [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab chart.
## GitLab-Omnibus Chart (Recommended)
@@ -21,13 +21,13 @@ should be deployed, upgraded, and configured.
This chart is the best available way to operate GitLab on Kubernetes. It deploys and configures nearly all features of GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](../../user/project/container_registry.html#gitlab-container-registry), [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and a [load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). It is based on our [GitLab Omnibus Docker Images](https://docs.gitlab.com/omnibus/docker/README.html).
-Once the [cloud native charts](#upcoming-cloud-native-helm-charts) are ready for production use, this chart will be deprecated. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment.
+Once the [cloud native GitLab chart](#cloud-native-gitlab-chart) is ready for production use, this chart will be deprecated. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment.
-Learn more about the [gitlab-omnibus chart.](gitlab_omnibus.md)
+Learn more about the [gitlab-omnibus chart](gitlab_omnibus.md).
## Cloud Native GitLab Chart
-GitLab is working towards building a [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current charts](#official-gitlab-helm-charts-recommended).
+GitLab is working towards building a [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current chart](#gitlab-omnibus-chart-recommended).
By offering individual containers and charts, we will be able to provide a number of benefits:
* Easier horizontal scaling of each service,
@@ -35,9 +35,9 @@ By offering individual containers and charts, we will be able to provide a numbe
* Potential for rolling updates and canaries within a service,
* and plenty more.
-This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We do not expect these to be production ready before the second half of 2018.
+This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We are planning to launch this chart in beta by the end of 2017.
-Learn more about the [cloud native GitLab chart.](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md)
+Learn more about the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md).
## Other Charts
diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md
index 713d11b75e4..2f5d4142d04 100644
--- a/doc/install/relative_url.md
+++ b/doc/install/relative_url.md
@@ -1,11 +1,11 @@
-## Install GitLab under a relative URL
+# Install GitLab under a relative URL
-_**Note:**
+NOTE: **Note:**
This document describes how to run GitLab under a relative URL for installations
from source. If you are using an Omnibus package,
[the steps are different][omnibus-rel]. Use this guide along with the
[installation guide](installation.md) if you are installing GitLab for the
-first time._
+first time.
---
@@ -33,7 +33,7 @@ serve GitLab under a relative URL is:
After all the changes you need to recompile the assets and [restart GitLab].
-### Relative URL requirements
+## Relative URL requirements
If you configure GitLab with a relative URL, the assets (JavaScript, CSS, fonts,
images, etc.) will need to be recompiled, which is a task which consumes a lot
@@ -43,11 +43,11 @@ least 2GB of RAM available on your system, while we recommend 4GB RAM, and 4 or
See the [requirements](requirements.md) document for more information.
-### Enable relative URL in GitLab
+## Enable relative URL in GitLab
-_**Note:**
+NOTE: **Note:**
Do not make any changes to your web server configuration file regarding
-relative URL. The relative URL support is implemented by GitLab Workhorse._
+relative URL. The relative URL support is implemented by GitLab Workhorse.
---
@@ -115,7 +115,7 @@ Make sure to follow all steps below:
1. [Restart GitLab][] for the changes to take effect.
-### Disable relative URL in GitLab
+## Disable relative URL in GitLab
To disable the relative URL:
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 17fe80fa93d..baecf9455b0 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -80,13 +80,13 @@ errors during usage.
- 256GB RAM supports up to 32,000 users
- More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/)
-We recommend having at least 2GB of swap on your server, even if you currently have
+We recommend having at least [2GB of swap on your server](https://askubuntu.com/a/505344/310789), even if you currently have
enough available RAM. Having swap will help reduce the chance of errors occurring
if your available memory changes. We also recommend [configuring the kernel's swappiness setting](https://askubuntu.com/a/103916)
to a low value like `10` to make the most of your RAM while still having the swap
available when needed.
-Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those.
+Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those.
## Database
@@ -121,8 +121,8 @@ Existing users using GitLab with MySQL/MariaDB are advised to
### PostgreSQL Requirements
-As of GitLab 9.3, PostgreSQL 9.2 or newer is required, and earlier versions are
-not supported. We highly recommend users to use at least PostgreSQL 9.6 as this
+As of GitLab 10.0, PostgreSQL 9.6 or newer (but less than 10) is required, and earlier versions are
+not supported. We highly recommend users to use PostgreSQL 9.6 as this
is the PostgreSQL version used for development and testing.
Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every
@@ -146,7 +146,7 @@ So for a machine with 2 cores, 3 unicorn workers is ideal.
For all machines that have 2GB and up we recommend a minimum of three unicorn workers.
If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping.
-To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings).
+To change the Unicorn workers when you have the Omnibus package (which defaults to the recommendation above) please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings).
## Redis and Sidekiq
@@ -184,7 +184,7 @@ Runner.
We recommend using a separate machine for each GitLab Runner, if you plan to
use the CI features.
-[security reasons]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md
+[security reasons]: https://gitlab.com/gitlab-org/gitlab-runner/blob/master/docs/security/index.md
## Supported web browsers
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 09d96bdd338..54e78bdef54 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Integration
GitLab integrates with multiple third-party services to allow external issue
diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md
index 372e1909330..075feaeead9 100644
--- a/doc/integration/external-issue-tracker.md
+++ b/doc/integration/external-issue-tracker.md
@@ -22,6 +22,7 @@ Visit the links below for details:
- [Redmine](../user/project/integrations/redmine.md)
- [Jira](../user/project/integrations/jira.md)
- [Bugzilla](../user/project/integrations/bugzilla.md)
+- [Custom Issue Tracker](../user/project/integrations/custom_issue_tracker.md)
### Service Template
diff --git a/doc/integration/google.md b/doc/integration/google.md
index d5b523e6dc0..727ca13ebcf 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -1,83 +1,92 @@
# Google OAuth2 OmniAuth Provider
-To enable the Google OAuth2 OmniAuth provider you must register your application with Google. Google will generate a client ID and secret key for you to use.
-
-1. Sign in to the [Google Developers Console](https://console.developers.google.com/) with the Google account you want to use to register GitLab.
-
-1. Select "Create Project".
-
-1. Provide the project information
- - Project name: 'GitLab' works just fine here.
- - Project ID: Must be unique to all Google Developer registered applications. Google provides a randomly generated Project ID by default. You can use the randomly generated ID or choose a new one.
-1. Refresh the page. You should now see your new project in the list. Click on the project.
-
-1. Select the "Google APIs" tab in the Overview.
-
-1. Select and enable the following Google APIs - listed under "Popular APIs"
- - Enable `Contacts API`
- - Enable `Google+ API`
+To enable the Google OAuth2 OmniAuth provider you must register your application
+with Google. Google will generate a client ID and secret key for you to use.
+
+## Enabling Google OAuth
+
+In Google's side:
+
+1. Navigate to the [cloud resource manager](https://console.cloud.google.com/cloud-resource-manager) page
+1. Select **Create Project**
+1. Provide the project information:
+ - **Project name** - "GitLab" works just fine here.
+ - **Project ID** - Must be unique to all Google Developer registered applications.
+ Google provides a randomly generated Project ID by default. You can use
+ the randomly generated ID or choose a new one.
+1. Refresh the page and you should see your new project in the list
+1. Go to the [Google API Console](https://console.developers.google.com/apis/dashboard)
+1. Select the previously created project form the upper left corner
+1. Select **Credentials** from the sidebar
+1. Select **OAuth consent screen** and fill the form with the required information
+1. In the **Credentials** tab, select **Create credentials > OAuth client ID**
+1. Fill in the required information
+ - **Application type** - Choose "Web Application"
+ - **Name** - Use the default one or provide your own
+ - **Authorized JavaScript origins** -This isn't really used by GitLab but go
+ ahead and put `https://gitlab.example.com`
+ - **Authorized redirect URIs** - Enter your domain name followed by the
+ callback URIs one at a time:
-1. Select "Credentials" in the submenu.
+ ```
+ https://gitlab.example.com/users/auth/google_oauth2/callback
+ https://gitlab.exampl.com/-/google_api/auth/callback
+ ```
-1. Select "Create New Client ID".
+1. You should now be able to see a Client ID and Client secret. Note them down
+ or keep this page open as you will need them later.
+1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Google Cloud APIs > Container Engine API > Enable**
-1. Fill in the required information
- - Application type: "Web Application"
- - Authorized JavaScript origins: This isn't really used by GitLab but go ahead and put 'https://gitlab.example.com' here.
- - Authorized redirect URI: 'https://gitlab.example.com/users/auth/google_oauth2/callback'
-1. Under the heading "Client ID for web application" you should see a Client ID and Client secret (see screenshot). Keep this page open as you continue configuration. ![Google app](img/google_app.png)
+On your GitLab server:
-1. On your GitLab server, open the configuration file.
+1. Open the configuration file.
- For omnibus package:
+ For Omnibus GitLab:
```sh
- sudo editor /etc/gitlab/gitlab.rb
+ sudo editor /etc/gitlab/gitlab.rb
```
For installations from source:
```sh
- cd /home/git/gitlab
-
- sudo -u git -H editor config/gitlab.yml
+ cd /home/git/gitlab
+ sudo -u git -H editor config/gitlab.yml
```
-1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+1. Add the provider configuration:
-1. Add the provider configuration:
-
- For omnibus package:
+ For Omnibus GitLab:
```ruby
- gitlab_rails['omniauth_providers'] = [
- {
- "name" => "google_oauth2",
- "app_id" => "YOUR_APP_ID",
- "app_secret" => "YOUR_APP_SECRET",
- "args" => { "access_type" => "offline", "approval_prompt" => '' }
- }
- ]
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "google_oauth2",
+ "app_id" => "YOUR_APP_ID",
+ "app_secret" => "YOUR_APP_SECRET",
+ "args" => { "access_type" => "offline", "approval_prompt" => '' }
+ }
+ ]
```
For installations from source:
```
- - { name: 'google_oauth2', app_id: 'YOUR_APP_ID',
- app_secret: 'YOUR_APP_SECRET',
- args: { access_type: 'offline', approval_prompt: '' } }
+ - { name: 'google_oauth2', app_id: 'YOUR_APP_ID',
+ app_secret: 'YOUR_APP_SECRET',
+ args: { access_type: 'offline', approval_prompt: '' } }
```
-1. Change 'YOUR_APP_ID' to the client ID from the Google Developer page from step 10.
-
-1. Change 'YOUR_APP_SECRET' to the client secret from the Google Developer page from step 10.
-
-1. Make sure that you configure GitLab to use an FQDN as Google will not accept raw IP addresses.
+1. Change `YOUR_APP_ID` to the client ID from the Google Developer page
+1. Similarly, change `YOUR_APP_SECRET` to the client secret
+1. Make sure that you configure GitLab to use an FQDN as Google will not accept
+ raw IP addresses.
For Omnibus packages:
```ruby
- external_url 'https://gitlab.example.com'
+ external_url 'https://gitlab.example.com'
```
For installations from source:
@@ -88,21 +97,13 @@ To enable the Google OAuth2 OmniAuth provider you must register your application
```
1. Save the configuration file.
-
1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
-On the sign in page there should now be a Google icon below the regular sign in form. Click the icon to begin the authentication process. Google will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
-
-## Further Configuration
-
-This further configuration is not required for Google authentication to function but it is strongly recommended. Taking these steps will increase usability for users by providing a little more recognition and branding.
-
-At this point, when users first try to authenticate to your GitLab installation with Google they will see a generic application name on the prompt screen. The prompt informs the user that "Project Default Service Account" would like to access their account. "Project Default Service Account" isn't very recognizable and may confuse or cause users to be concerned. This is easily changeable.
-
-1. Select 'Consent screen' in the left menu. (See steps 1, 4 and 5 above for instructions on how to get here if you closed your window).
-1. Scroll down until you find "Product Name". Change the product name to something more descriptive.
-1. Add any additional information as you wish - homepage, logo, privacy policy, etc. None of this is required, but it may help your users.
+On the sign in page there should now be a Google icon below the regular sign in
+form. Click the icon to begin the authentication process. Google will ask the
+user to sign in and authorize the GitLab application. If everything goes well
+the user will be returned to GitLab and will be signed in.
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index b5b245c626f..3ae98adc465 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -132,14 +132,17 @@ On the sign in page there should now be a SAML button below the regular sign in
Click the icon to begin the authentication process. If everything goes well the user
will be returned to GitLab and will be signed in.
-## External Groups
+## Marking Users as External based on SAML Groups
>**Note:**
This setting is only available on GitLab 8.7 and above.
-SAML login includes support for external groups. You can define in the SAML
-settings which groups, to which your users belong in your IdP, you wish to be
-marked as [external](../user/permissions.md).
+SAML login includes support for automatically identifying whether a user should
+be considered an [external](../user/permissions.md) user based on the user's group
+membership in the SAML identity provider. This feature **does not** allow you to
+automatically add users to GitLab [Groups](../user/group/index.md), it simply
+allows you to mark users as External if they are members of certain groups in the
+Identity Provider.
### Requirements
diff --git a/doc/integration/trello_power_up.md b/doc/integration/trello_power_up.md
index d264486a872..834d63d1166 100644
--- a/doc/integration/trello_power_up.md
+++ b/doc/integration/trello_power_up.md
@@ -39,4 +39,4 @@ Learn more about generating a personal access token in the
[Personal Access Token Documentation][personal-access-token-documentation].
Don't forget to check the API scope checkbox!
-[personal-access-token-documentation]: ../user/profile/personal_access_tokens.html
+[personal-access-token-documentation]: ../user/profile/personal_access_tokens.md
diff --git a/doc/intro/README.md b/doc/intro/README.md
index 7485912d1a2..d9acc5bdeac 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Get started with GitLab
## Organize
diff --git a/doc/legal/README.md b/doc/legal/README.md
index 56d72ae3859..6413f1d645f 100644
--- a/doc/legal/README.md
+++ b/doc/legal/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Legal
- [Corporate contributor license agreement](corporate_contributor_license_agreement.md)
diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md
index 7f08188bd65..ebb24ba0a7f 100644
--- a/doc/legal/corporate_contributor_license_agreement.md
+++ b/doc/legal/corporate_contributor_license_agreement.md
@@ -1,29 +1,2 @@
-# Corporate contributor license agreement
-
-You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions.
-
-1. Definitions.
-
- "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
- "Contribution" shall mean the code, documentation or other original works of authorship, including any modifications or additions to an existing work, that is submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
-
-2. Grant of Copyright License.
-
-Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
-
-3. Grant of Patent License.
-
-Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
-
-4. You represent that You are legally entitled to grant the above license. You represent further that each of Your employees is authorized to submit Contributions on Your behalf, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of Your corporation here]." Such designations of exclusion for unauthorized employees are to be submitted via email to legal@gitlab.com.
-
-5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).
-
-6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
-
-7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
-
-8. It is Your responsibility to notify GitLab.com when any change is required to the list of designated employees excluded from submitting Contributions on Your behalf per Section 4. Such notification should be sent via email to legal@gitlab.com.
-
-This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
+This document has been replaced by a Developer Certificate of Origin and License,
+as described in [Contributing.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md). \ No newline at end of file
diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md
index 59803aea080..ebb24ba0a7f 100644
--- a/doc/legal/individual_contributor_license_agreement.md
+++ b/doc/legal/individual_contributor_license_agreement.md
@@ -1,25 +1,2 @@
-# Individual contributor license agreement
-
-You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions.
-
-1. Definitions.
-
- "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
- "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
-
-2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
-
-3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
-
-4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to GitLab B.V., or that your employer has executed a separate Corporate CLA with GitLab B.V..
-
-5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.
-
-6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
-
-7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [insert_name_here]".
-
-8. You agree to notify GitLab B.V. of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
-
-This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
+This document has been replaced by a Developer Certificate of Origin and License,
+as described in [Contributing.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md). \ No newline at end of file
diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md
index 2e7782736ff..9347a834510 100644
--- a/doc/migrate_ci_to_ce/README.md
+++ b/doc/migrate_ci_to_ce/README.md
@@ -372,8 +372,10 @@ CREATE TABLE
```
To fix that you need to apply this SQL statement before doing final backup:
-```
-# Omnibus
+
+```sql
+## Omnibus GitLab
+
gitlab-ci-rails dbconsole <<EOF
-- ALTER TABLES - DROP DEFAULTS
ALTER TABLE ONLY ci_application_settings ALTER COLUMN id DROP DEFAULT;
@@ -427,7 +429,8 @@ ALTER TABLE ONLY ci_variables ALTER COLUMN id SET DEFAULT nextval('ci_variables_
ALTER TABLE ONLY ci_web_hooks ALTER COLUMN id SET DEFAULT nextval('ci_web_hooks_id_seq'::regclass);
EOF
-# Source
+## Source installations
+
cd /home/gitlab_ci/gitlab-ci
sudo -u gitlab_ci -H bundle exec rails dbconsole production <<EOF
... COPY SQL STATEMENTS FROM ABOVE ...
diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md
new file mode 100644
index 00000000000..8d0afa9e692
--- /dev/null
+++ b/doc/policy/maintenance.md
@@ -0,0 +1,86 @@
+# GitLab Maintenance Policy
+
+## Versioning
+
+GitLab follows the [Semantic Versioning](http://semver.org/) for its releases:
+`(Major).(Minor).(Patch)` in a [pragmatic way].
+
+- **Major version**: Whenever there is something significant or any backwards
+ incompatible changes are introduced to the public API.
+- **Minor version**: When new, backwards compatible functionality is introduced
+ to the public API or a minor feature is introduced, or when a set of smaller
+ features is rolled out.
+- **Patch number**: When backwards compatible bug fixes are introduced that fix
+ incorrect behavior.
+
+For example, for GitLab version 10.5.7:
+
+* `10` represents major version
+* `5` represents minor version
+* `7` represents patch number
+
+## Patch releases
+
+Patch releases usually only include bug fixes and are only done for the current
+stable release. That said, in some cases, we may backport it to previous stable
+release, depending on the severity of the bug.
+
+For instance, if we release `10.1.1` with a fix for a severe bug introduced in
+`10.0.0`, we could backport the fix to a new `10.0.x` patch release.
+
+### Security releases
+
+Security releases are a special kind of patch release that only include security
+fixes and patches (see below).
+
+Our current policy is to support one stable release at any given time, but for
+medium-level security issues, we may backport security fixes to the previous two
+monthly releases.
+
+For very serious security issues, there is
+[precedent](https://about.gitlab.com/2016/05/02/cve-2016-4340-patches/)
+to backport security fixes to even more monthly releases of GitLab.
+This decision is made on a case-by-case basis.
+
+## Upgrade recommendations
+
+We encourage everyone to run the latest stable release to ensure that you can
+easily upgrade to the most secure and feature-rich GitLab experience. In order
+to make sure you can easily run the most recent stable release, we are working
+hard to keep the update process simple and reliable.
+
+If you are unable to follow our monthly release cycle, there are a couple of
+cases you need to consider.
+
+It is considered safe to jump between patch versions and minor versions within
+one major version. For example, it is safe to:
+
+* Upgrade the patch version:
+ * `8.9.0` -> `8.9.7`
+ * `8.9.0` -> `8.9.1`
+ * `8.9.2` -> `8.9.6`
+* Upgrade the minor version:
+ * `8.9.4` -> `8.12.3`
+ * `9.2.3` -> `9.5.5`
+
+Upgrading the major version requires more attention.
+We cannot guarantee that upgrading between major versions will be seamless. As previously mentioned, major versions are reserved for backwards incompatible changes.
+
+We recommend that you first upgrade to the latest available minor version within
+your major version. By doing this, you can address any deprecation messages
+that could possibly change behaviour in the next major release.
+
+Please see the table below for some examples:
+
+| Latest stable version | Your version | Recommended upgrade path | Note |
+| -------------- | ------------ | ------------------------ | ---------------- |
+| 9.4.5 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.4.5` | `8.17.7` is the last version in version `8` |
+| 10.1.4 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.5.8` -> `10.1.4` | `8.17.7` is the last version in version `8`, `9.5.8` is the last version in version `9` |
+
+More information about the release procedures can be found in our
+[release-tools documentation][rel]. You may also want to read our
+[Responsible Disclosure Policy][disclosure].
+
+[rel]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/
+[disclosure]: https://about.gitlab.com/disclosure/
+[pragmatic way]: https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e
diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md
index 2b81ebc9c59..2f916f5dea7 100644
--- a/doc/raketasks/README.md
+++ b/doc/raketasks/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Rake tasks
- [Backup restore](backup_restore.md)
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index e4c09b2b507..54c3e20d61d 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -136,44 +136,54 @@ In the example below we use Amazon S3 for storage, but Fog also lets you use
for AWS, Google, OpenStack Swift, Rackspace and Aliyun as well. A local driver is
[also available](#uploading-to-locally-mounted-shares).
-For omnibus packages, add the following to `/etc/gitlab/gitlab.rb`:
+#### Using Amazon S3
-```ruby
-gitlab_rails['backup_upload_connection'] = {
- 'provider' => 'AWS',
- 'region' => 'eu-west-1',
- 'aws_access_key_id' => 'AKIAKIAKI',
- 'aws_secret_access_key' => 'secret123'
- # 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'
-```
+For Omnibus GitLab packages:
+
+1. Add the following to `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['backup_upload_connection'] = {
+ 'provider' => 'AWS',
+ 'region' => 'eu-west-1',
+ 'aws_access_key_id' => 'AKIAKIAKI',
+ 'aws_secret_access_key' => 'secret123'
+ # 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'
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect
-Make sure to run `sudo gitlab-ctl reconfigure` after editing `/etc/gitlab/gitlab.rb` to reflect the changes.
+---
For installations from source:
-```yaml
- backup:
- # snip
- upload:
- # Fog storage connection settings, see http://fog.io/storage/ .
- connection:
- provider: AWS
- 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'
- # The remote 'directory' to store your backups. For S3, this would be the bucket name.
- remote_directory: 'my.s3.bucket'
- # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
- # encryption: 'AES256'
- # Specifies Amazon S3 storage class to use for backups, this is optional
- # storage_class: 'STANDARD'
-```
+1. Edit `home/git/gitlab/config/gitlab.yml`:
+
+ ```yaml
+ backup:
+ # snip
+ upload:
+ # Fog storage connection settings, see http://fog.io/storage/ .
+ connection:
+ provider: AWS
+ 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'
+ # The remote 'directory' to store your backups. For S3, this would be the bucket name.
+ remote_directory: 'my.s3.bucket'
+ # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
+ # encryption: 'AES256'
+ # Specifies Amazon S3 storage class to use for backups, this is optional
+ # storage_class: 'STANDARD'
+ ```
+
+1. [Restart GitLab] for the changes to take effect
If you are uploading your backups to S3 you will probably want to create a new
IAM user with restricted access rights. To give the upload user access only for
@@ -226,6 +236,50 @@ with the name of your bucket:
}
```
+#### Using Google Cloud Storage
+
+If you want to use Google Cloud Storage to save backups, you'll have to create
+an access key from the Google console first:
+
+1. Go to the storage settings page https://console.cloud.google.com/storage/settings
+1. Select "Interoperability" and create an access key
+1. Make note of the "Access Key" and "Secret" and replace them in the
+ configurations below
+1. Make sure you already have a bucket created
+
+For Omnibus GitLab packages:
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['backup_upload_connection'] = {
+ 'provider' => 'Google',
+ 'google_storage_access_key_id' => 'Access Key',
+ 'google_storage_secret_access_key' => 'Secret'
+ }
+ gitlab_rails['backup_upload_remote_directory'] = 'my.google.bucket'
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect
+
+---
+
+For installations from source:
+
+1. Edit `home/git/gitlab/config/gitlab.yml`:
+
+ ```yaml
+ backup:
+ upload:
+ connection:
+ provider: 'Google'
+ google_storage_access_key_id: 'Access Key'
+ google_storage_secret_access_key: 'Secret'
+ remote_directory: 'my.google.bucket'
+ ```
+
+1. [Restart GitLab] for the changes to take effect
+
### Uploading to locally mounted shares
You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
@@ -554,3 +608,6 @@ The rake task runs this as the `gitlab` user which does not have the superuser a
Those objects have no influence on the database backup/restore but they give this annoying warning.
For more information see similar questions on postgresql issue tracker[here](http://www.postgresql.org/message-id/201110220712.30886.adrian.klaver@gmail.com) and [here](http://www.postgresql.org/message-id/2039.1177339749@sss.pgh.pa.us) as well as [stack overflow](http://stackoverflow.com/questions/4368789/error-must-be-owner-of-language-plpgsql).
+
+[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md
index 2b305cb5c99..97e9b36d1a6 100644
--- a/doc/raketasks/import.md
+++ b/doc/raketasks/import.md
@@ -3,49 +3,47 @@
## Notes
- The owner of the project will be the first admin
-- The groups will be created as needed
+- The groups will be created as needed, including subgroups
- The owner of the group will be the first admin
- Existing projects will be skipped
+- The existing Git repos will be moved from disk (removed from the original path)
## How to use
-### Create a new folder inside the git repositories path. This will be the name of the new group.
+### Create a new folder to import your Git repositories from.
-- For omnibus-gitlab, it is located at: `/var/opt/gitlab/git-data/repositories` by default, unless you changed
-it in the `/etc/gitlab/gitlab.rb` file.
-- For installations from source, it is usually located at: `/home/git/repositories` or you can see where
-your repositories are located by looking at `config/gitlab.yml` under the `repositories => storages` entries
-(you'll usually use the `default` storage path to start).
-
-New folder needs to have git user ownership and read/write/execute access for git user and its group:
+The new folder needs to have git user ownership and read/write/execute access for git user and its group:
```
-sudo -u git mkdir /var/opt/gitlab/git-data/repositories/new_group
+sudo -u git mkdir /var/opt/gitlab/git-data/repository-import-<date>/new_group
```
-If you are using an installation from source, replace `/var/opt/gitlab/git-data`
-with `/home/git`.
-
### Copy your bare repositories inside this newly created folder:
+- Any .git repositories found on any of the subfolders will be imported as projects
+- Groups will be created as needed, these could be nested folders. Example:
+
+If we copy the repos to `/var/opt/gitlab/git-data/repository-import-<date>`, and repo A needs to be under the groups G1 and G2, it will
+have to be created under those folders: `/var/opt/gitlab/git-data/repository-import-<date>/G1/G2/A.git`.
+
+
```
-sudo cp -r /old/git/foo.git /var/opt/gitlab/git-data/repositories/new_group/
+sudo cp -r /old/git/foo.git /var/opt/gitlab/git-data/repository-import-<date>/new_group/
# Do this once when you are done copying git repositories
-sudo chown -R git:git /var/opt/gitlab/git-data/repositories/new_group/
+sudo chown -R git:git /var/opt/gitlab/git-data/repository-import-<date>
```
`foo.git` needs to be owned by the git user and git users group.
-If you are using an installation from source, replace `/var/opt/gitlab/git-data`
-with `/home/git`.
+If you are using an installation from source, replace `/var/opt/gitlab/` with `/home/git`.
### Run the command below depending on your type of installation:
#### Omnibus Installation
```
-$ sudo gitlab-rake gitlab:import:repos
+$ sudo gitlab-rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>']
```
#### Installation from source
@@ -54,16 +52,21 @@ Before running this command you need to change the directory to where your GitLa
```
$ cd /home/git/gitlab
-$ sudo -u git -H bundle exec rake gitlab:import:repos RAILS_ENV=production
+$ sudo -u git -H bundle exec rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>'] RAILS_ENV=production
```
#### Example output
```
-Processing abcd.git
+Processing /var/opt/gitlab/git-data/repository-import-1/a/b/c/blah.git
+ * Using namespace: a/b/c
+ * Created blah (a/b/c/blah)
+ * Skipping repo /var/opt/gitlab/git-data/repository-import-1/a/b/c/blah.wiki.git
+Processing /var/opt/gitlab/git-data/repository-import-1/abcd.git
* Created abcd (abcd.git)
-Processing group/xyz.git
- * Created Group group (2)
+Processing /var/opt/gitlab/git-data/repository-import-1/group/xyz.git
+ * Using namespace: group (2)
* Created xyz (group/xyz.git)
+ * Skipping repo /var/opt/gitlab/git-data/repository-import-1/@shared/a/b/abcd.git
[...]
```
diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md
index 3ae46019daf..5554a0c8b78 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -149,18 +149,3 @@ cp config/secrets.yml.bak config/secrets.yml
sudo /etc/init.d/gitlab start
```
-
-## Clear authentication tokens for all users. Important! Data loss!
-
-Clear authentication tokens for all users in the GitLab database. This
-task is useful if your users' authentication tokens might have been exposed in
-any way. All the existing tokens will become invalid, and new tokens are
-automatically generated upon sign-in or user modification.
-
-```
-# omnibus-gitlab
-sudo gitlab-rake gitlab:users:clear_all_authentication_tokens
-
-# installation from source
-bundle exec rake gitlab:users:clear_all_authentication_tokens RAILS_ENV=production
-```
diff --git a/doc/security/README.md b/doc/security/README.md
index 0fea6be8b55..d397ff104ab 100644
--- a/doc/security/README.md
+++ b/doc/security/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Security
- [Password length limits](password_length_limits.md)
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 793de9d777c..33a2d7a88a7 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -1,4 +1,4 @@
-# SSH
+# GitLab and SSH keys
Git is a distributed version control system, which means you can work locally
but you can also share or "push" your changes to other servers.
@@ -114,7 +114,7 @@ custom name continue onto the next step.
If you manually copied your public SSH key make sure you copied the entire
key starting with `ssh-rsa` and ending with your email.
-
+
1. Optionally you can test your setup by running `ssh -T git@example.com`
(replacing `example.com` with your GitLab domain) and verifying that you
receive a `Welcome to GitLab` message.
@@ -172,7 +172,7 @@ dummy user account.
If you are a project master or owner, you can add a deploy key in the
project settings under the section 'Repository'. Specify a title for the new
deploy key and paste a public SSH key. After this, the machine that uses
-the corresponding private SSH key has read-only or read-write (if enabled)
+the corresponding private SSH key has read-only or read-write (if enabled)
access to the project.
You can't add the same deploy key twice using the form.
@@ -232,7 +232,7 @@ something is wrong with your SSH setup.
- Ensure that you generated your SSH key pair correctly and added the public SSH
key to your GitLab profile
-- Try manually registering your private SSH key using `ssh-agent` as documented
+- Try manually registering your private SSH key using `ssh-agent` as documented
earlier in this document
- Try to debug the connection by running `ssh -Tv git@example.com`
(replacing `example.com` with your GitLab domain)
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index 0399ebec86a..f2a9b1d769b 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -1,6 +1,26 @@
# System hooks
-Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `project_rename`, `project_transfer`, `project_update`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`.
+Your GitLab instance can perform HTTP POST requests on the following events:
+
+- `project_create`
+- `project_destroy`
+- `project_rename`
+- `project_transfer`
+- `project_update`
+- `user_add_to_team`
+- `user_remove_from_team`
+- `user_create`
+- `user_destroy`
+- `user_rename`
+- `key_create`
+- `key_destroy`
+- `group_create`
+- `group_destroy`
+- `group_rename`
+- `user_add_to_group`
+- `user_remove_from_group`
+
+The triggers for most of these are self-explanatory, but `project_update` and `project_rename` deserve some clarification: `project_update` is fired any time an attribute of a project is changed (name, description, tags, etc.) *unless* the `path` attribute is also changed. In that case, a `project_rename` is triggered instead (so that, for instance, if all you care about is the repo URL, you can just listen for `project_rename`).
System hooks can be used, e.g. for logging or changing information in a LDAP server.
@@ -70,6 +90,9 @@ X-Gitlab-Event: System Hook
}
```
+Note that `project_rename` is not triggered if the namespace changes.
+Please refer to `group_rename` and `user_rename` for that case.
+
**Project transferred:**
```json
@@ -173,6 +196,21 @@ X-Gitlab-Event: System Hook
}
```
+**User renamed:**
+
+```json
+{
+ "event_name": "user_rename",
+ "created_at": "2017-11-01T11:21:04Z",
+ "updated_at": "2017-11-01T14:04:47Z",
+ "name": "new-name",
+ "email": "best-email@example.tld",
+ "user_id": 58,
+ "username": "new-exciting-name",
+ "old_username": "old-boring-name"
+}
+```
+
**Key added**
```json
@@ -207,13 +245,15 @@ X-Gitlab-Event: System Hook
"updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_create",
"name": "StoreCloud",
- "owner_email": "johnsmith@gmail.com",
- "owner_name": "John Smith",
+ "owner_email": null,
+ "owner_name": null,
"path": "storecloud",
"group_id": 78
}
```
+`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675.
+
**Group removed:**
```json
@@ -222,13 +262,35 @@ X-Gitlab-Event: System Hook
"updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_destroy",
"name": "StoreCloud",
- "owner_email": "johnsmith@gmail.com",
- "owner_name": "John Smith",
+ "owner_email": null,
+ "owner_name": null,
"path": "storecloud",
"group_id": 78
}
```
+`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675.
+
+**Group renamed:**
+
+```json
+{
+ "event_name": "group_rename",
+ "created_at": "2017-10-30T15:09:00Z",
+ "updated_at": "2017-11-01T10:23:52Z",
+ "name": "Better Name",
+ "path": "better-name",
+ "full_path": "parent-group/better-name",
+ "group_id": 64,
+ "owner_name": null,
+ "owner_email": null,
+ "old_path": "old-name",
+ "old_full_path": "parent-group/old-name"
+}
+```
+
+`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675.
+
**New Group Member:**
```json
diff --git a/doc/topics/autodevops/img/auto_devops_settings.png b/doc/topics/autodevops/img/auto_devops_settings.png
new file mode 100644
index 00000000000..b572cc5b855
--- /dev/null
+++ b/doc/topics/autodevops/img/auto_devops_settings.png
Binary files differ
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 5561784ed0b..914217772b8 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -121,7 +121,7 @@ Google Cloud.
## Enabling Auto DevOps
-NOTE: **Note:**
+**Note:**
If you haven't done already, read the [prerequisites](#prerequisites) to make
full use of Auto DevOps. If this is your fist time, we recommend you follow the
[quick start guide](#quick-start).
@@ -129,10 +129,14 @@ full use of Auto DevOps. If this is your fist time, we recommend you follow the
1. Go to your project's **Settings > CI/CD > General pipelines settings** and
find the Auto DevOps section
1. Select "Enable Auto DevOps"
+1. After selecting an option to enable Auto DevOps, a checkbox will appear below
+ so you can immediately run a pipeline on the default branch
1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain)
that will be used by Kubernetes to deploy your application
1. Hit **Save changes** for the changes to take effect
+![Project AutoDevops settings section](img/auto_devops_settings.png)
+
Now that it's enabled, there are a few more steps depending on whether your project
has a `.gitlab-ci.yml` or not:
@@ -144,6 +148,12 @@ has a `.gitlab-ci.yml` or not:
All you need to do is remove your existing `.gitlab-ci.yml`, and you can even
do that in a branch to test Auto DevOps before committing to `master`.
+NOTE: **Note:**
+If you are a GitLab Administrator, you can enable Auto DevOps instance wide
+in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that,
+all the projects that haven't explicitly set an option will have Auto DevOps
+enabled by default.
+
## Stages of Auto DevOps
The following sections describe the stages of Auto DevOps. Read them carefully
@@ -315,7 +325,7 @@ Auto DevOps uses [Helm](https://helm.sh/) to deploy your application to Kubernet
You can override the Helm chart used by bundling up a chart into your project
repo or by specifying a project variable:
-- **Bundled chart** - If your project has a `./charts` directory with a `Chart.yaml`
+- **Bundled chart** - If your project has a `./chart` directory with a `Chart.yaml`
file in it, Auto DevOps will detect the chart and use it instead of the [default
one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app).
This can be a great way to control exactly how your application is deployed.
@@ -517,7 +527,7 @@ Feature.get(:auto_devops_banner_disabled).enable
Or through the HTTP API with an admin access token:
```sh
-curl --data "value=true" --header "PRIVATE-TOKEN: private_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled
+curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled
```
[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115
diff --git a/doc/university/README.md b/doc/university/README.md
index 170582bcd0c..55865ac23e8 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab University
GitLab University is the best place to learn about **Version Control with Git and GitLab**.
@@ -51,10 +55,10 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 1.5. Migrating from other Source Control
-1. [Migrating from BitBucket/Stash](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_bitbucket.html)
-1. [Migrating from GitHub](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_github.html)
-1. [Migrating from SVN](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
-1. [Migrating from Fogbugz](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_fogbugz.html)
+1. [Migrating from BitBucket/Stash](https://docs.gitlab.com/ee/user/project/import/bitbucket.html)
+1. [Migrating from GitHub](https://docs.gitlab.com/ee/user/project/import/github.html)
+1. [Migrating from SVN](https://docs.gitlab.com/ee/user/project/import/svn.html)
+1. [Migrating from Fogbugz](https://docs.gitlab.com/ee/user/project/import/fogbugz.html)
#### 1.6. GitLab Inc.
@@ -76,13 +80,13 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
- Being part of our Great Community and Contributing to GitLab
1. [Getting Started with the GitLab Development Kit (GDK)](https://about.gitlab.com/2016/06/08/getting-started-with-gitlab-development-kit/)
1. [Contributing Technical Articles to the GitLab Blog](https://about.gitlab.com/2016/01/26/call-for-writers/)
-1. [GitLab Training Workshops](https://about.gitlab.com/training)
+1. [GitLab Training Workshops](https://docs.gitlab.com/ce/university/training/end-user/)
+1. [GitLab Professional Services](https://about.gitlab.com/services/)
#### 1.8 GitLab Training Material
1. [Git and GitLab Terminology](glossary/README.md)
1. [Git and GitLab Workshop - Slides](https://docs.google.com/presentation/d/1JzTYD8ij9slejV2-TO-NzjCvlvj6mVn9BORePXNJoMI/edit?usp=drive_web)
-1. [Git and GitLab Revision](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/university/training/end-user)
---
diff --git a/doc/university/bookclub/booklist.md b/doc/university/bookclub/booklist.md
index c4229832e9f..26c3851276b 100644
--- a/doc/university/bookclub/booklist.md
+++ b/doc/university/bookclub/booklist.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Books
List of books and resources, that may be worth reading.
diff --git a/doc/university/bookclub/index.md b/doc/university/bookclub/index.md
index 022a61f4429..63238685b2b 100644
--- a/doc/university/bookclub/index.md
+++ b/doc/university/bookclub/index.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# The GitLab Book Club
The Book Club is a casual meet-up to read and discuss books we like.
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 9544de41b9a..076fbf6f710 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -1,4 +1,8 @@
-## What is the Glossary
+---
+comments: false
+---
+
+# What is the Glossary
This contains a simplified list and definitions of some of the terms that you will encounter in your day to day activities when working with GitLab.
Please add any terms that you discover that you think would be useful for others.
@@ -19,6 +23,10 @@ A Microsoft-based [directory service](https://msdn.microsoft.com/en-us/library/b
Building and [delivering software](http://agilemethodology.org/) in phases/parts rather than trying to build everything at once then delivering to the user/client. The latter is known as the WaterFall model.
+### Amazon RDS
+
+External reference: <http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html>
+
### Application Lifecycle Management (ALM)
The entire product lifecycle management process for an application, from requirements management, development, and testing until deployment. GitLab has [advantages](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit#slide=id.g72f2e4906_2_288) over both legacy and modern ALM tools.
@@ -64,7 +72,7 @@ A branch is a parallel version of a repository. This allows you to work on the r
Having your own logo on [your GitLab instance login page](https://docs.gitlab.com/ee/customization/branded_login_page.html) instead of the GitLab logo.
-### Job triggers
+### Job triggers (Build Triggers)
These protect your code base against breaks, for instance when a team is working on the same project. Learn about [setting up](https://docs.gitlab.com/ce/ci/triggers/README.html) job triggers.
### CEPH
@@ -105,15 +113,15 @@ Atlassian's product for collaboration on documents and projects.
### Continuous Delivery
-A [software engineering approach](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which continuous integration, automated testing, and automated deployment capabilities allow software to be developed and deployed rapidly, reliably and repeatedly with minimal human intervention. Still, the deployment to production is defined strategically and triggered manually.
+A [software engineering approach](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which continuous integration, automated testing, and automated deployment capabilities allow software to be developed and deployed rapidly, reliably and repeatedly with minimal human intervention. Still, the deployment to production is defined strategically and triggered manually. [Amazon moves toward continuous delivery](https://www.youtube.com/watch?v=esEFaY0FDKc)
### Continuous Deployment
-A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which every code change goes through the entire pipeline and is put into production automatically, resulting in many production deployments every day. It does everything that Continuous Delivery does, but the process is fully automated, there's no human intervention at all.
+A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which every code change goes through the entire pipeline and is put into production automatically, resulting in many production deployments every day. It does everything that Continuous Delivery does, but the process is fully automated, there's no human intervention at all. [The difference between Continuous Delivery and Continuous Integration.](https://www.youtube.com/watch?v=igwFj8PPSnw)
### Continuous Integration
-A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which you build and test software every time a developer pushes code to the application, and it happens several times a day.
+A [software development practice](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) in which you build and test software every time a developer pushes code to the application, and it happens several times a day. [Thoughtworks discusses continuous integration.](https://www.thoughtworks.com/continuous-integration)
### Contributor
@@ -123,6 +131,10 @@ Term used for a person contributing to an open source project.
A [natural evolution](https://about.gitlab.com/2016/09/14/gitlab-live-event-recap/) of software development that carries a conversation across functional groups throughout the development process, enabling developers to track the full path of development in a cohesive and intuitive way. ConvDev accelerates the development lifecycle by fostering collaboration and knowledge sharing from idea to production.
+### Cycle Analytics
+
+See <https://gitlab.com/gitlab-org/gitlab-ce/issues/22458>
+
### Cycle Time
The time it takes to move from [idea to production](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab).
@@ -131,6 +143,10 @@ The time it takes to move from [idea to production](https://about.gitlab.com/201
Atlassian product for High Availability.
+### Dependencies
+
+As in "specify [dependencies](https://gitlab.com/gitlab-org/gitlab-ce/issues/14728) between stages."
+
### Deploy Keys
A [SSH key](https://docs.gitlab.com/ce/gitlab-basics/create-your-ssh-keys.html)stored on your server that grants access to a single GitLab repository. This is used by a GitLab runner to clone a project's code so that tests can be run against the checked out code.
@@ -147,15 +163,17 @@ The intersection of software engineering, quality assurance, and technology oper
The difference between two commits, or saved changes. This will also be shown visually after the changes.
-#### Directory
+### Directory
A folder used for storing multiple files.
### Docker Container Registry
-A [feature](https://docs.gitlab.com/ce/user/project/container_registry.html) of GitLab projects. Containers wrap up a piece of software in a complete filesystem that contains everything it needs to run: code, runtime, system tools, system libraries – anything you can install on a server. This guarantees that it will always run the same, regardless of the environment it is running in.
+A [feature](https://docs.gitlab.com/ce/user/project/container_registry.html) of [GitLab projects](https://about.gitlab.com/2016/05/23/gitlab-container-registry/). Containers wrap up a piece of software in a complete filesystem that contains everything it needs to run: code, runtime, system tools, system libraries – anything you can install on a server. This guarantees that it will always run the same, regardless of the environment it is running in.
+
+### Dynamic Environment (review apps)
-### Dynamic Environment
+### EC2 Instance
### ElasticSearch
@@ -163,10 +181,26 @@ Elasticsearch is a flexible, scalable and powerful search service. When [enabled
### Emacs
+External reference: <https://www.masteringemacs.org/article/mastering-key-bindings-emacs>
+
+### First Byte
+
+External reference: <https://en.wikipedia.org/wiki/Time_To_First_Byte>
+
+First Byte (sometimes referred to as time to first byte or [TTFB](https://en.wikipedia.org/wiki/Time_To_First_Byte)) measures the time between making a request and receiving the first byte of information in return. As a result, First Byte encompasses everything that is the backend as well as network transit issues. It differs from [_Speed Index_](#speed-index) mostly by frontend related issues which are included in Speed Index such as javascript loading, page rendering, and so on.
+
### Fork
Your [own copy](https://docs.gitlab.com/ce/workflow/forking_workflow.html) of a repository that allows you to make changes to the repository without affecting the original.
+### Funnel, or: TOFU, MOFU, BOFU
+
+External reference: [Blog post](https://www.weidert.com/whole_brain_marketing_blog/bid/113688/ToFu-MoFu-BoFu-Serving-Up-The-Right-Content-for-Lead-Nurturing)
+
+TOFU: top of funnel
+MOFU: middle of funnel
+BOFU: bottom of funnel
+
### Gerrit
A code review [tool](https://www.gerritcodereview.com/) built on top of Git.
@@ -179,6 +213,8 @@ A [git attributes file](https://git-scm.com/docs/gitattributes) is a simple text
[Scripts](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) you can use to trigger actions at certain points.
+Difference between a [webhook](#webhooks) and a git hook: a git hook is local to its repo (usually) while a webhook is not (it can make API or http calls). So for example if you want your linter to fire before you commit, you can set that up with a git hook. If the linter fails, the commit does not go through. A git hook _can_ be configured to go beyond its repo, e.g. by having it make an API call.
+
### GitHost.io
A single-tenant solution that provides GitLab CE or EE as a managed service. GitLab Inc. is responsible for installing, updating, hosting, and backing up customers' own private and secure GitLab instance.
@@ -207,9 +243,20 @@ Our free SaaS for public and private repositories.
Allows you to replicate your GitLab instance to other geographical locations as a read-only fully operational version. It [can be used](https://docs.gitlab.com/ee/gitlab-geo/README.html) for cloning and fetching projects, in addition to reading any data. This will make working with large repositories over large distances much faster.
+### GitLab High Availability
+
+### GitLab Master Plan
+
+Related blog post: <https://about.gitlab.com/2016/09/13/gitlab-master-plan/>.
+
### GitLab Pages
+
These allow you to [create websites](https://gitlab.com/help/pages/README.md) for your GitLab projects, groups, or user account.
+### GitLab Runner
+
+Related project: <https://gitlab.com/gitlab-org/gitlab-runner>
+
### Gitolite
An [access layer](https://git-scm.com/book/en/v1/Git-on-the-Server-Gitolite) that sits on top of Git. Users are granted access to repos via a simple config file. As an admin, you only need the users' public SSH key and a username.
@@ -222,6 +269,10 @@ A web-based hosting service for projects using Git. It was acquired by GitLab an
An open source programming [language](https://golang.org/).
+### Gogs
+
+External reference: <https://gogs.io/>
+
### GUI/ Git GUI
A portable [graphical interface](https://git-scm.com/docs/git-gui) to Git that allows users to make changes to their repository by making new commits, amending existing ones, creating branches, performing local merges, and fetching/pushing to remote repositories.
@@ -252,7 +303,7 @@ A [tool](https://docs.gitlab.com/ee/integration/external-issue-tracker.html) use
### Jenkins
-An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular.
+An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html).
### Jira
@@ -286,6 +337,10 @@ GitLab [integrates](https://docs.gitlab.com/ce/administration/auth/ldap.html) wi
Allows you to synchronize the members of a GitLab group with one or more LDAP groups.
+### Lint
+
+Static code analysis for our various file types. For example, we use [scss-lint](https://github.com/brigade/scss-lint) to ensure that a consistent code styling is respected. Similar tools: rubocop / eslint.
+
### Load Balancer
A [device](https://en.wikipedia.org/wiki/Load_balancing_(computing)) that distributes network or application traffic across multiple servers.
@@ -326,6 +381,10 @@ Takes changes from one branch, and [applies them](https://git-scm.com/docs/git-m
[Arises](https://about.gitlab.com/2016/09/06/resolving-merge-conflicts-from-the-gitlab-ui/) when a merge can't be performed cleanly between two versions of the same file.
+#### Merge Request
+
+[Takes changes](https://docs.gitlab.com/ce/gitlab-basics/add-merge-request.html) from one branch, and applies them into another branch.
+
### Meteor
A [platform](https://www.meteor.com) for building javascript apps.
@@ -346,6 +405,14 @@ A type of software license. It lets people do anything with your code with prope
A free disaster recovery [software](https://help.ubuntu.com/community/MondoMindi).
+#### Mount
+
+External reference:
+
+As stated on the [wikipedia page](https://en.wikipedia.org/wiki/Mount_(Unix)), "Mounting makes file systems, files, directories, devices and special files available for use and available to the user."
+
+For example, we have NFS servers where the _git files_ reside. In order for a worker node to "see" or "use" the git files, the NFS server needs to be _mounted_ on the worker; that is, the worker needs to know that the NFS server exists and how to connect to it. Think of it as getting a shared drive to show up in your Finder (on Mac) or Explorer (on Windows).
+
### MySQL
A relational [database](http://www.mysql.com/) owned by Oracle. Currently only supported if you are using EE.
@@ -356,7 +423,7 @@ A set of symbols that are used to organize objects of various kinds so that thes
### Nginx
-A web [server](https://www.nginx.com/resources/wiki/) (pronounced "engine x"). It can act as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache.
+A web [server](https://www.nginx.com/resources/wiki/) (pronounced "engine x"). [It can act]((https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/nginx.md) as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache.
### OAuth
@@ -376,7 +443,11 @@ GitLab's [business model](https://about.gitlab.com/2016/07/20/gitlab-is-open-cor
### Open Source Software
-Software for which the original source code is freely [available](https://opensource.org/docs/osd) and may be redistributed and modified. GitLab prioritizes open source [stewardship](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/).
+Software for which the original source code is freely [available](https://opensource.org/docs/osd) and may be redistributed and modified. GitLab prioritizes open source [stewardship](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/). Including to providing access to the source code, open source software must comply with a number of criteria, among them free distribution and no discrimination against persons, groups, or fields of endeavor.
+
+#### Open Source Stewardship
+
+[Related blog post](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/).
### Owner
@@ -426,6 +497,8 @@ A popular DevOps [automation tool](https://puppet.com/product/how-puppet-works).
Git [command](https://git-scm.com/docs/git-push) to send commits from the local repository to the remote repository. Read about [advanced push rules](https://gitlab.com/help/pages/README.md) in GitLab.
+### Raketasks
+
### RE Read Only
Permissions to see a file and its contents, but not change it.
@@ -434,10 +507,24 @@ Permissions to see a file and its contents, but not change it.
In addition to the merge, the [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) is a main way to integrate changes from one branch into another.
+### Regression
+
+A regression is something that used to work one way in the last release and then we made a **breaking change** and it no longer works the same way.
+
+_or_
+
+A regression is defined as a change that results in a negative impact on the functionality of an existing feature due to recent changes, i.e. the latest release.
+
+### Remote mirroring
+
### (Git) Repository
A directory where Git [has been initiatlized](https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository) to start version controlling your files. The history of your work is stored here. A remote repository is not on your machine, but usually online (like on GitLab.com, for instance). The main remote repository is usually called "Origin."
+##### Remote repository
+
+A [repository](https://about.gitlab.com/2015/05/18/simple-words-for-a-gitlab-newbie/) that is not-on-your-machine, so it's anything that is not your computer. Usually, it is online, GitLab.com for instance. The main remote repository is usually called “Originâ€.
+
### Requirements management
Gives your distributed teams a single shared repository to collaborate and share requirements, understand their relationship to tests, and evaluate linked defects. It includes multiple, preconfigured requirement types.
@@ -456,7 +543,7 @@ A route table contains rules (called routes) that determine where network traffi
### Runners
-Actual build machines/containers that [run and execute tests](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner) you have specified to be run on GitLab CI.
+Actual build machines/containers that [run and execute tests](https://gitlab.com/gitlab-org/gitlab-runner) you have specified to be run on GitLab CI.
### Sidekiq
@@ -470,7 +557,11 @@ Software that is hosted centrally and accessed on-demand (i.e. whenever you want
This term is often used by people when they mean "Version Control."
-## Scrum
+#### SCLAU
+
+Abbreviation for SQO Count [Large And Up](https://about.gitlab.com/handbook/sales/#market-segmentation). This is the number of opportunities in large and strategic organizations passed from marketing to sales.
+
+### Scrum
An Agile [framework](https://www.scrum.org/Resources/What-is-Scrum) designed to typically help complete complex software projects. It's made up of several parts: product requirements backlog, sprint planning, sprint (development), sprint review, and retrospec (analyzing the sprint). The goal is to end up with potentially shippable products.
@@ -480,7 +571,9 @@ The board used to track the status and progress of each of the sprint backlog it
### Shell
-Terminal on Mac OSX, GitBash on Windows, or Linux Terminal on Linux. You [use git]() and make changes to GitLab projects in your shell. You [use git](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html) and make changes to GitLab projects in your shell.
+Terminal on Mac OSX, GitBash on Windows, or Linux Terminal on Linux. You [use git](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html) and make changes to GitLab projects in your shell. You [use git](https://docs.gitlab.com/ce/gitlab-basics/start-using-git.html) and make changes to GitLab projects in your shell.
+
+### Shell command runner
### Single-tenant
@@ -490,6 +583,8 @@ The tenant purchases their own copy of the software and the software can be cust
Real time messaging app for teams that is used internally by GitLab team members. GitLab users can enable [Slack integration](https://docs.gitlab.com/ce/project_services/slack.html) to trigger push, issue, and merge request events among others.
+### Slash commands
+
### Slave Servers
Also known as secondary servers, these help to spread the load over multiple machines. They also provide backups when the master/primary server crashes.
@@ -498,6 +593,10 @@ Also known as secondary servers, these help to spread the load over multiple mac
Program code as typed by a computer programmer (i.e. it has not yet been compiled/translated by the computer to machine language).
+### Speed Index
+
+[Speed Index](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index) is "the average time at which visible parts of the page are displayed".
+
### SSH Key
A unique identifier of a computer. It is used to identify computers without the need for a password (e.g., On GitLab I have [added the ssh key](https://docs.gitlab.com/ce/gitlab-basics/create-your-ssh-keys.html) of all my work machines so that the GitLab instance knows that it can accept code pushes and pulls from this trusted machines whose keys are I have added.)
@@ -538,6 +637,16 @@ An open source version control system. Read about [migrating from SVN](https://d
[Represents](https://docs.gitlab.com/ce/api/tags.html) a version of a particular branch at a moment in time.
+### Tenancy
+
+#### Multi-tenant
+
+A [multi-tenant](http://whatis.techtarget.com/definition/multi-tenancy) GitLab instance can have any number of customers - such as companies or groups of users using it. GitLab.com is an example of a multi-tenant GitLab instance.
+
+#### Single-tenant
+
+A [single-tenant](http://searchcloudapplications.techtarget.com/definition/single-tenancy) GitLab instance has only one customer - such as a company - using it. On premise GitLab instances are almost exclusively single-tenant.
+
### Tool Stack
The set of tools used in a process to achieve a common outcome (e.g. set of tools used in Application Lifecycle Management).
@@ -546,9 +655,17 @@ The set of tools used in a process to achieve a common outcome (e.g. set of tool
An open source project management and bug tracking web [application](https://trac.edgewall.org/).
+### True-Up licensing model
+
+### Ubuntu
+
### Untracked files
-New files that Git has not [been told](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) to track previously.
+New files that Git has not [been told](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) to track previously. Add them by using the command "git add [file path]"
+
+### Upstream repository vs. GitLab repository
+
+[External conversation](https://news.ycombinator.com/item?id=12487112)
### User
@@ -556,11 +673,11 @@ Anyone interacting with the software.
### Version Control Software (VCS)
-Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. VCS [has evolved](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.gd69537a19_0_32) from local version control systems, to centralized version control systems, to the present distributed version control systems like Git, Mercurial, Bazaar, and Darcs.
+Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. VCS [has evolved](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.gd69537a19_0_32) from local version control systems, to centralized version control systems, to the present [distributed version control systems](https://en.wikipedia.org/wiki/Distributed_version_control) like Git, Mercurial, Bazaar, and Darcs. If any server dies, and these systems were collaborating via it, any of the client repositories can be copied back up to the server to restore it.
### Virtual Private Cloud (VPC)
-An on demand configurable pool of shared computing resources allocated within a public cloud environment, providing some isolation between the different users using the resources. GitLab users need to create a new Amazon VPC in order to [setup High Availability](https://docs.gitlab.com/ce/university/high-availability/aws/).
+A [VPC](https://docs.gitlab.com/ce/university/glossary/README.html#virtual-private-cloud-vpc) is an on demand configurable pool of shared computing resources allocated within a public cloud environment, providing some isolation between the different users using the resources. GitLab users need to create a new Amazon VPC in order to [setup High Availability](https://docs.gitlab.com/ce/university/high-availability/aws/).
### Virtual private server (VPS)
@@ -568,7 +685,7 @@ A [virtual machine](https://en.wikipedia.org/wiki/Virtual_private_server) sold a
### VM Instance
-In object-oriented programming, an [instance](http://stackoverflow.com/questions/20461907/what-is-meaning-of-instance-in-programming) is a specific realization of any object. An object may be varied in a number of ways. Each realized variation of that object is an instance. Therefore, a VM instance is an instance of a virtual machine, which is an emulation of a computer system.
+In object-oriented programming, an [instance](http://stackoverflow.com/questions/20461907/what-is-meaning-of-instance-in-programming) is a specific realization of any [object](https://cloud.google.com/compute/docs/instances/). An object may be varied in a number of ways. Each realized variation of that object is an instance. Therefore, a VM instance is an instance of a virtual machine, which is an emulation of a computer system.
### Waterfall
@@ -582,6 +699,10 @@ A way for for an app to [provide](https://docs.gitlab.com/ce/user/project/integr
A [website/system](http://www.wiki.com/) that allows for collaborative editing of its content by the users. In programming, wikis usually contain documentation of how to use the software.
+### Working area
+
+Files that have been modified but are not committed. Check them by using the command "git status".
+
### Working Tree
[Consists of files](http://stackoverflow.com/questions/3689838/difference-between-head-working-tree-index-in-git) that you are currently working on.
diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md
index 6b8f3cd3d1d..54625996dff 100644
--- a/doc/university/high-availability/aws/README.md
+++ b/doc/university/high-availability/aws/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# High Availability on AWS
diff --git a/doc/university/process/README.md b/doc/university/process/README.md
index 04f2d52514f..fdf6224f7f6 100644
--- a/doc/university/process/README.md
+++ b/doc/university/process/README.md
@@ -1,8 +1,12 @@
---
+comments: false
+---
+
+---
title: University | Process
---
-## Suggesting improvements
+# Suggesting improvements
If you would like to teach a class or participate or help in any way please
submit a merge request and assign it to [Job](https://gitlab.com/u/JobV).
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index 567dadb3b47..25d5fe351ca 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -1,5 +1,9 @@
+---
+comments: false
+---
-## Support Boot Camp
+
+# Support Boot Camp
**Goal:** Prepare new Service Engineers at GitLab
diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md
index 03c62a81b10..a882bf0eb48 100644
--- a/doc/university/training/end-user/README.md
+++ b/doc/university/training/end-user/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Training
diff --git a/doc/university/training/gitlab_flow.md b/doc/university/training/gitlab_flow.md
index a7db1f2e069..02a6ad48a38 100644
--- a/doc/university/training/gitlab_flow.md
+++ b/doc/university/training/gitlab_flow.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Flow
- A simplified branching strategy
diff --git a/doc/university/training/index.md b/doc/university/training/index.md
index 03179ff5a77..14f096b130f 100644
--- a/doc/university/training/index.md
+++ b/doc/university/training/index.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Training Material
All GitLab training material is stored in markdown format. Slides are
diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md
index 3ed601625cf..d01634df744 100644
--- a/doc/university/training/topics/additional_resources.md
+++ b/doc/university/training/topics/additional_resources.md
@@ -1,4 +1,8 @@
-## Additional Resources
+---
+comments: false
+---
+
+# Additional Resources
1. GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
2. GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
diff --git a/doc/university/training/topics/agile_git.md b/doc/university/training/topics/agile_git.md
index e6e4fea9b51..251af99bed7 100644
--- a/doc/university/training/topics/agile_git.md
+++ b/doc/university/training/topics/agile_git.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Agile and Git
----------
diff --git a/doc/university/training/topics/bisect.md b/doc/university/training/topics/bisect.md
index a60c4365e0c..2d5ab107fe6 100644
--- a/doc/university/training/topics/bisect.md
+++ b/doc/university/training/topics/bisect.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Bisect
----------
diff --git a/doc/university/training/topics/cherry_picking.md b/doc/university/training/topics/cherry_picking.md
index af7a70a2818..df23024b6ee 100644
--- a/doc/university/training/topics/cherry_picking.md
+++ b/doc/university/training/topics/cherry_picking.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Cherry Pick
----------
diff --git a/doc/university/training/topics/env_setup.md b/doc/university/training/topics/env_setup.md
index 8149379b36f..b7bec83ed8a 100644
--- a/doc/university/training/topics/env_setup.md
+++ b/doc/university/training/topics/env_setup.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Configure your environment
----------
diff --git a/doc/university/training/topics/explore_gitlab.md b/doc/university/training/topics/explore_gitlab.md
index b65457728c0..84a1879cd92 100644
--- a/doc/university/training/topics/explore_gitlab.md
+++ b/doc/university/training/topics/explore_gitlab.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Explore GitLab projects
----------
diff --git a/doc/university/training/topics/feature_branching.md b/doc/university/training/topics/feature_branching.md
index 4b34406ea75..0df5f26dbea 100644
--- a/doc/university/training/topics/feature_branching.md
+++ b/doc/university/training/topics/feature_branching.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Feature branching
----------
diff --git a/doc/university/training/topics/getting_started.md b/doc/university/training/topics/getting_started.md
index ec7bb2631aa..153b45fb4da 100644
--- a/doc/university/training/topics/getting_started.md
+++ b/doc/university/training/topics/getting_started.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Getting Started
----------
diff --git a/doc/university/training/topics/git_add.md b/doc/university/training/topics/git_add.md
index 9ffb4b9c859..651366e0d49 100644
--- a/doc/university/training/topics/git_add.md
+++ b/doc/university/training/topics/git_add.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Git Add
----------
diff --git a/doc/university/training/topics/git_intro.md b/doc/university/training/topics/git_intro.md
index ca1ff29d93b..7e502d6dad4 100644
--- a/doc/university/training/topics/git_intro.md
+++ b/doc/university/training/topics/git_intro.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Git introduction
----------
diff --git a/doc/university/training/topics/git_log.md b/doc/university/training/topics/git_log.md
index 32ebceff491..f2709ae3890 100644
--- a/doc/university/training/topics/git_log.md
+++ b/doc/university/training/topics/git_log.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Git Log
----------
@@ -49,8 +53,8 @@ git log --since=1.month.ago --until=3.weeks.ago
```
cd ~/workspace
-git clone git@gitlab.com:gitlab-org/gitlab-ci-multi-runner.git
-cd gitlab-ci-multi-runner
+git clone git@gitlab.com:gitlab-org/gitlab-runner.git
+cd gitlab-runner
git log --author="Travis"
git log --since=1.month.ago --until=3.weeks.ago
git log --since=1.month.ago --until=1.day.ago --author="Travis"
diff --git a/doc/university/training/topics/gitlab_flow.md b/doc/university/training/topics/gitlab_flow.md
index 8e5d3baf959..b8049b5c80e 100644
--- a/doc/university/training/topics/gitlab_flow.md
+++ b/doc/university/training/topics/gitlab_flow.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Flow
----------
diff --git a/doc/university/training/topics/merge_conflicts.md b/doc/university/training/topics/merge_conflicts.md
index 77807b3e7ef..9a1ce550868 100644
--- a/doc/university/training/topics/merge_conflicts.md
+++ b/doc/university/training/topics/merge_conflicts.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Merge conflicts
----------
diff --git a/doc/university/training/topics/merge_requests.md b/doc/university/training/topics/merge_requests.md
index 5b446f02f63..4e8c9de85a1 100644
--- a/doc/university/training/topics/merge_requests.md
+++ b/doc/university/training/topics/merge_requests.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Merge requests
----------
diff --git a/doc/university/training/topics/rollback_commits.md b/doc/university/training/topics/rollback_commits.md
index cf647284604..0db1d93d1dc 100644
--- a/doc/university/training/topics/rollback_commits.md
+++ b/doc/university/training/topics/rollback_commits.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Rollback Commits
----------
diff --git a/doc/university/training/topics/stash.md b/doc/university/training/topics/stash.md
index c1bdda32645..5b27ac12f77 100644
--- a/doc/university/training/topics/stash.md
+++ b/doc/university/training/topics/stash.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Git Stash
----------
diff --git a/doc/university/training/topics/subtree.md b/doc/university/training/topics/subtree.md
index 5d869af64c1..b5a892dc17b 100644
--- a/doc/university/training/topics/subtree.md
+++ b/doc/university/training/topics/subtree.md
@@ -1,8 +1,8 @@
-## Subtree
+---
+comments: false
+---
-----------
-
-## Subtree
+# Subtree
* Used when there are nested repositories.
* Not recommended when the amount of dependencies is too large
diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md
index e9607b5a875..ab48d52d3c3 100644
--- a/doc/university/training/topics/tags.md
+++ b/doc/university/training/topics/tags.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Tags
----------
diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md
index 17dbb64b9e6..fc72949ade9 100644
--- a/doc/university/training/topics/unstage.md
+++ b/doc/university/training/topics/unstage.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Unstage
----------
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index 9e38df26b6a..90e1d2ba5e8 100644
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Git Workshop
---
diff --git a/doc/update/10.0-to-10.1.md b/doc/update/10.0-to-10.1.md
index 4a9384f3ad6..af815d26a74 100644
--- a/doc/update/10.0-to-10.1.md
+++ b/doc/update/10.0-to-10.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 10.0 to 10.1
Make sure you view this update guide from the tag (version) of GitLab you would
@@ -150,7 +154,7 @@ sudo -u git -H make
#### New Gitaly configuration options required
-In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell'.
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
```shell
echo '
@@ -335,11 +339,11 @@ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
If all items are green, then congratulations, the upgrade is complete!
-## Things went south? Revert to previous version (9.5)
+## Things went south? Revert to previous version (10.0)
### 1. Revert the code to the previous version
-Follow the [upgrade guide from 9.4 to 9.5](9.4-to-9.5.md), except for the
+Follow the [upgrade guide from 9.5 to 10.0](9.5-to-10.0.md), except for the
database migration (the backup is already migrated to the previous version).
### 2. Restore from the backup
diff --git a/doc/update/10.1-to-10.2.md b/doc/update/10.1-to-10.2.md
new file mode 100644
index 00000000000..9e0d8f79522
--- /dev/null
+++ b/doc/update/10.1-to-10.2.md
@@ -0,0 +1,360 @@
+---
+comments: false
+---
+
+# From 10.1 to 10.2
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.5.tar.gz
+echo '3247e217d6745c27ef23bdc77b6abdb4b57a118f ruby-2.3.5.tar.gz' | shasum -c - && tar xzf ruby-2.3.5.tar.gz
+cd ruby-2.3.5
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+
+Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
+JavaScript dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
+1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
+echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.8.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-2-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-2-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 11. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-1-stable:config/gitlab.yml.example origin/10-2-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/10-1-stable:lib/support/nginx/gitlab-ssl origin/10-2-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/10-1-stable:lib/support/nginx/gitlab origin/10-2-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/10-1-stable:lib/support/init.d/gitlab.default.example origin/10-2-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 12. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Compile GetText PO files
+
+sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 13. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 14. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (10.0)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 9.5 to 10.0](9.5-to-10.0.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/update/2.6-to-3.0.md b/doc/update/2.6-to-3.0.md
index 97cd277b424..8f18bd93cea 100644
--- a/doc/update/2.6-to-3.0.md
+++ b/doc/update/2.6-to-3.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 2.6 to 3.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.6-to-3.0.md) for the most up to date instructions.*
diff --git a/doc/update/2.9-to-3.0.md b/doc/update/2.9-to-3.0.md
index a890aa885d5..6a3c2387683 100644
--- a/doc/update/2.9-to-3.0.md
+++ b/doc/update/2.9-to-3.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 2.9 to 3.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.9-to-3.0.md) for the most up to date instructions.*
diff --git a/doc/update/3.0-to-3.1.md b/doc/update/3.0-to-3.1.md
index e32508745a2..1f25b8265c9 100644
--- a/doc/update/3.0-to-3.1.md
+++ b/doc/update/3.0-to-3.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 3.0 to 3.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.0-to-3.1.md) for the most up to date instructions.*
diff --git a/doc/update/3.1-to-4.0.md b/doc/update/3.1-to-4.0.md
index b370464390e..1a53ddeb4bd 100644
--- a/doc/update/3.1-to-4.0.md
+++ b/doc/update/3.1-to-4.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 3.1 to 4.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.1-to-4.0.md) for the most up to date instructions.*
diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md
index 7124424bb60..40a133e796e 100644
--- a/doc/update/4.0-to-4.1.md
+++ b/doc/update/4.0-to-4.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 4.0 to 4.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.0-to-4.1.md) for the most up to date instructions.*
diff --git a/doc/update/4.1-to-4.2.md b/doc/update/4.1-to-4.2.md
index 8ed5b333a2e..1fd6c58bda7 100644
--- a/doc/update/4.1-to-4.2.md
+++ b/doc/update/4.1-to-4.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 4.1 to 4.2
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.1-to-4.2.md) for the most up to date instructions.*
diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md
index 1ec39218ba8..311664b2bc1 100644
--- a/doc/update/4.2-to-5.0.md
+++ b/doc/update/4.2-to-5.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 4.2 to 5.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.2-to-5.0.md) for the most up to date instructions.*
diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md
index 9c9950fb2c6..7067ea4c40c 100644
--- a/doc/update/5.0-to-5.1.md
+++ b/doc/update/5.0-to-5.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.0 to 5.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.0-to-5.1.md) for the most up to date instructions.*
diff --git a/doc/update/5.1-to-5.2.md b/doc/update/5.1-to-5.2.md
index 2aab47d2d7c..4faf5fa549e 100644
--- a/doc/update/5.1-to-5.2.md
+++ b/doc/update/5.1-to-5.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.1 to 5.2
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.2.md) for the most up to date instructions.*
diff --git a/doc/update/5.1-to-5.4.md b/doc/update/5.1-to-5.4.md
index e80f1b89c63..212343bac3f 100644
--- a/doc/update/5.1-to-5.4.md
+++ b/doc/update/5.1-to-5.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.1 to 5.4
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.4.md) for the most up to date instructions.*
diff --git a/doc/update/5.1-to-6.0.md b/doc/update/5.1-to-6.0.md
index 1ee175383da..865d38e0ca4 100644
--- a/doc/update/5.1-to-6.0.md
+++ b/doc/update/5.1-to-6.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.1 to 6.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-6.0.md) for the most up to date instructions.*
diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md
index 2ae50510f63..ed4f3ebdd53 100644
--- a/doc/update/5.2-to-5.3.md
+++ b/doc/update/5.2-to-5.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.2 to 5.3
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.2-to-5.3.md) for the most up to date instructions.*
diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md
index 842e3bb6791..7277250eb32 100644
--- a/doc/update/5.3-to-5.4.md
+++ b/doc/update/5.3-to-5.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.3 to 5.4
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.3-to-5.4.md) for the most up to date instructions.*
diff --git a/doc/update/5.4-to-6.0.md b/doc/update/5.4-to-6.0.md
index 44715984f0c..dacdf05cc9c 100644
--- a/doc/update/5.4-to-6.0.md
+++ b/doc/update/5.4-to-6.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 5.4 to 6.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.4-to-6.0.md) for the most up to date instructions.*
diff --git a/doc/update/6.0-to-6.1.md b/doc/update/6.0-to-6.1.md
index 0c672abeb05..a3c52a1cfb4 100644
--- a/doc/update/6.0-to-6.1.md
+++ b/doc/update/6.0-to-6.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.0 to 6.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.0-to-6.1.md) for the most up to date instructions.*
diff --git a/doc/update/6.1-to-6.2.md b/doc/update/6.1-to-6.2.md
index d3760cf0619..36a395bf01e 100644
--- a/doc/update/6.1-to-6.2.md
+++ b/doc/update/6.1-to-6.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.1 to 6.2
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.1-to-6.2.md) for the most up to date instructions.*
diff --git a/doc/update/6.2-to-6.3.md b/doc/update/6.2-to-6.3.md
index 91105de2e29..02e87a08b8f 100644
--- a/doc/update/6.2-to-6.3.md
+++ b/doc/update/6.2-to-6.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.2 to 6.3
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.2-to-6.3.md) for the most up to date instructions.*
diff --git a/doc/update/6.3-to-6.4.md b/doc/update/6.3-to-6.4.md
index 20b58ed8b25..285ed06bdad 100644
--- a/doc/update/6.3-to-6.4.md
+++ b/doc/update/6.3-to-6.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.3 to 6.4
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.3-to-6.4.md) for the most up to date instructions.*
diff --git a/doc/update/6.4-to-6.5.md b/doc/update/6.4-to-6.5.md
index 5ee0f040b5d..e07c98a5ad4 100644
--- a/doc/update/6.4-to-6.5.md
+++ b/doc/update/6.4-to-6.5.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.4 to 6.5
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.4-to-6.5.md) for the most up to date instructions.*
diff --git a/doc/update/6.5-to-6.6.md b/doc/update/6.5-to-6.6.md
index fa3712f83ad..3f79b19644e 100644
--- a/doc/update/6.5-to-6.6.md
+++ b/doc/update/6.5-to-6.6.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.5 to 6.6
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.5-to-6.6.md) for the most up to date instructions.*
diff --git a/doc/update/6.6-to-6.7.md b/doc/update/6.6-to-6.7.md
index 9c85ed091c5..a0542d20d49 100644
--- a/doc/update/6.6-to-6.7.md
+++ b/doc/update/6.6-to-6.7.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.6 to 6.7
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.6-to-6.7.md) for the most up to date instructions.*
diff --git a/doc/update/6.7-to-6.8.md b/doc/update/6.7-to-6.8.md
index 687c1265d9b..acf004577f1 100644
--- a/doc/update/6.7-to-6.8.md
+++ b/doc/update/6.7-to-6.8.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.7 to 6.8
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.7-to-6.8.md) for the most up to date instructions.*
diff --git a/doc/update/6.8-to-6.9.md b/doc/update/6.8-to-6.9.md
index 0205b0c896a..3d7b1e5346b 100644
--- a/doc/update/6.8-to-6.9.md
+++ b/doc/update/6.8-to-6.9.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.8 to 6.9
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.8-to-6.9.md) for the most up to date instructions.*
diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md
index 4b6e3989893..27063948028 100644
--- a/doc/update/6.9-to-7.0.md
+++ b/doc/update/6.9-to-7.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.9 to 7.0
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.9-to-7.0.md) for the most up to date instructions.*
diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md
index 1e39fe47ef9..41d0e78b7d8 100644
--- a/doc/update/6.x-or-7.x-to-7.14.md
+++ b/doc/update/6.x-or-7.x-to-7.14.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 6.x or 7.x to 7.14
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.x-or-7.x-to-7.14.md) for the most up to date instructions.*
diff --git a/doc/update/7.0-to-7.1.md b/doc/update/7.0-to-7.1.md
index 2e9457aa142..308e8aeb985 100644
--- a/doc/update/7.0-to-7.1.md
+++ b/doc/update/7.0-to-7.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.0 to 7.1
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.0-to-7.1.md) for the most up to date instructions.*
diff --git a/doc/update/7.1-to-7.2.md b/doc/update/7.1-to-7.2.md
index e5045b5570f..07f92ac3af6 100644
--- a/doc/update/7.1-to-7.2.md
+++ b/doc/update/7.1-to-7.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.1 to 7.2
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.1-to-7.2.md) for the most up to date instructions.*
diff --git a/doc/update/7.10-to-7.11.md b/doc/update/7.10-to-7.11.md
index 89213ba7178..39eeefc0e32 100644
--- a/doc/update/7.10-to-7.11.md
+++ b/doc/update/7.10-to-7.11.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.10 to 7.11
### 0. Stop server
diff --git a/doc/update/7.11-to-7.12.md b/doc/update/7.11-to-7.12.md
index 3865186918c..530066e5fdb 100644
--- a/doc/update/7.11-to-7.12.md
+++ b/doc/update/7.11-to-7.12.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.11 to 7.12
### 0. Double-check your Git version
diff --git a/doc/update/7.12-to-7.13.md b/doc/update/7.12-to-7.13.md
index 4c8d8f1f741..8f413a2079a 100644
--- a/doc/update/7.12-to-7.13.md
+++ b/doc/update/7.12-to-7.13.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.12 to 7.13
### 0. Double-check your Git version
diff --git a/doc/update/7.13-to-7.14.md b/doc/update/7.13-to-7.14.md
index 934898da5a1..a8980662855 100644
--- a/doc/update/7.13-to-7.14.md
+++ b/doc/update/7.13-to-7.14.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.13 to 7.14
### 0. Double-check your Git version
diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md
index 25fa6d93f06..513afccff50 100644
--- a/doc/update/7.14-to-8.0.md
+++ b/doc/update/7.14-to-8.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.14 to 8.0
### 0. Double-check your Git version
diff --git a/doc/update/7.2-to-7.3.md b/doc/update/7.2-to-7.3.md
index d3391ddd225..a16f9de54e4 100644
--- a/doc/update/7.2-to-7.3.md
+++ b/doc/update/7.2-to-7.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.2 to 7.3
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.2-to-7.3.md) for the most up to date instructions.*
diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md
index 6d632dc3c8e..734c655f1d1 100644
--- a/doc/update/7.3-to-7.4.md
+++ b/doc/update/7.3-to-7.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.3 to 7.4
*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.3-to-7.4.md) for the most up to date instructions.*
diff --git a/doc/update/7.4-to-7.5.md b/doc/update/7.4-to-7.5.md
index ec50706d421..7a3a49ff948 100644
--- a/doc/update/7.4-to-7.5.md
+++ b/doc/update/7.4-to-7.5.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.4 to 7.5
### 0. Stop server
diff --git a/doc/update/7.5-to-7.6.md b/doc/update/7.5-to-7.6.md
index 331f5de080e..f0dfb177b79 100644
--- a/doc/update/7.5-to-7.6.md
+++ b/doc/update/7.5-to-7.6.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.5 to 7.6
### 0. Stop server
diff --git a/doc/update/7.6-to-7.7.md b/doc/update/7.6-to-7.7.md
index 918b10fbd95..85de6b0c546 100644
--- a/doc/update/7.6-to-7.7.md
+++ b/doc/update/7.6-to-7.7.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.6 to 7.7
### 0. Stop server
diff --git a/doc/update/7.7-to-7.8.md b/doc/update/7.7-to-7.8.md
index 84e0464a824..7cee5f79a13 100644
--- a/doc/update/7.7-to-7.8.md
+++ b/doc/update/7.7-to-7.8.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.7 to 7.8
### 0. Stop server
diff --git a/doc/update/7.8-to-7.9.md b/doc/update/7.8-to-7.9.md
index b0dc2ba1dbb..5a8b689dbc1 100644
--- a/doc/update/7.8-to-7.9.md
+++ b/doc/update/7.8-to-7.9.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.8 to 7.9
### 0. Stop server
diff --git a/doc/update/7.9-to-7.10.md b/doc/update/7.9-to-7.10.md
index 8f7f84b41ba..99df51dbb99 100644
--- a/doc/update/7.9-to-7.10.md
+++ b/doc/update/7.9-to-7.10.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 7.9 to 7.10
### 0. Stop server
diff --git a/doc/update/8.0-to-8.1.md b/doc/update/8.0-to-8.1.md
index 6ee0c0656ee..f612606af68 100644
--- a/doc/update/8.0-to-8.1.md
+++ b/doc/update/8.0-to-8.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.0 to 8.1
**NOTE:** GitLab 8.0 introduced several significant changes related to
diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md
index 4c9ff5c5c0a..2d0b19abd74 100644
--- a/doc/update/8.1-to-8.2.md
+++ b/doc/update/8.1-to-8.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.1 to 8.2
**NOTE:** GitLab 8.0 introduced several significant changes related to
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index e538983e603..df3e34f5cc6 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.10 to 8.11
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index 604166beb56..9d6a1f42375 100644
--- a/doc/update/8.11-to-8.12.md
+++ b/doc/update/8.11-to-8.12.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.11 to 8.12
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index d83965131f5..6225dee9802 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.12 to 8.13
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md
index aaadcec8ac0..d2508e3f980 100644
--- a/doc/update/8.13-to-8.14.md
+++ b/doc/update/8.13-to-8.14.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.13 to 8.14
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md
index a68fe3bb605..daf8d0f2ca6 100644
--- a/doc/update/8.14-to-8.15.md
+++ b/doc/update/8.14-to-8.15.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.14 to 8.15
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.15-to-8.16.md b/doc/update/8.15-to-8.16.md
index 9f8f0f714d4..3668142edd2 100644
--- a/doc/update/8.15-to-8.16.md
+++ b/doc/update/8.15-to-8.16.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.15 to 8.16
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md
index 74ffe0bc846..ee2e31c2aec 100644
--- a/doc/update/8.16-to-8.17.md
+++ b/doc/update/8.16-to-8.17.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.16 to 8.17
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md
index baab217b6b7..2e0c26a9092 100644
--- a/doc/update/8.17-to-9.0.md
+++ b/doc/update/8.17-to-9.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.17 to 9.0
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index 4b3c5bf6d64..3a0d647cbfe 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.2 to 8.3
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md
index 8b89455ca87..f5162dd5ff5 100644
--- a/doc/update/8.3-to-8.4.md
+++ b/doc/update/8.3-to-8.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.3 to 8.4
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md
index 0eedfaee2db..9e2f98add8d 100644
--- a/doc/update/8.4-to-8.5.md
+++ b/doc/update/8.4-to-8.5.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.4 to 8.5
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
index 851056161bb..55d8178c407 100644
--- a/doc/update/8.5-to-8.6.md
+++ b/doc/update/8.5-to-8.6.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.5 to 8.6
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md
index 34c727260aa..49db6f2967c 100644
--- a/doc/update/8.6-to-8.7.md
+++ b/doc/update/8.6-to-8.7.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.6 to 8.7
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.7-to-8.8.md b/doc/update/8.7-to-8.8.md
index 6feeb1919de..ee7ec6f7614 100644
--- a/doc/update/8.7-to-8.8.md
+++ b/doc/update/8.7-to-8.8.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.7 to 8.8
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.8-to-8.9.md b/doc/update/8.8-to-8.9.md
index 61cdf8854d4..7508443c30a 100644
--- a/doc/update/8.8-to-8.9.md
+++ b/doc/update/8.8-to-8.9.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.8 to 8.9
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/8.9-to-8.10.md b/doc/update/8.9-to-8.10.md
index 42132f690d8..915e7db819a 100644
--- a/doc/update/8.9-to-8.10.md
+++ b/doc/update/8.9-to-8.10.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 8.9 to 8.10
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md
index 6f1870a1366..f60bd92e236 100644
--- a/doc/update/9.0-to-9.1.md
+++ b/doc/update/9.0-to-9.1.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.0 to 9.1
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md
index ce72b313031..2fff6544797 100644
--- a/doc/update/9.1-to-9.2.md
+++ b/doc/update/9.1-to-9.2.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.1 to 9.2
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md
index 779ced0cf75..1b36cf53f4c 100644
--- a/doc/update/9.2-to-9.3.md
+++ b/doc/update/9.2-to-9.3.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.2 to 9.3
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md
index 78d8a6c7de5..210b6eb607d 100644
--- a/doc/update/9.3-to-9.4.md
+++ b/doc/update/9.3-to-9.4.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.3 to 9.4
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.4-to-9.5.md b/doc/update/9.4-to-9.5.md
index a7255142ef5..1bfc1167c36 100644
--- a/doc/update/9.4-to-9.5.md
+++ b/doc/update/9.4-to-9.5.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.4 to 9.5
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/9.5-to-10.0.md b/doc/update/9.5-to-10.0.md
index 8581e6511f2..8d1cf0f737b 100644
--- a/doc/update/9.5-to-10.0.md
+++ b/doc/update/9.5-to-10.0.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# From 9.5 to 10.0
Make sure you view this update guide from the tag (version) of GitLab you would
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index b2679d1ff22..e1857ce99c6 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Universal update guide for patch versions
## Select Version to Install
diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md
index eb7f14a96d5..746d6bf93e7 100644
--- a/doc/update/upgrader.md
+++ b/doc/update/upgrader.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# GitLab Upgrader (deprecated)
*DEPRECATED* We recommend to [switch to the Omnibus package and repository server](https://about.gitlab.com/update/) instead of using this script.
diff --git a/doc/user/discussions/img/image_resolved_discussion.png b/doc/user/discussions/img/image_resolved_discussion.png
new file mode 100644
index 00000000000..ed00b5c77fe
--- /dev/null
+++ b/doc/user/discussions/img/image_resolved_discussion.png
Binary files differ
diff --git a/doc/user/discussions/img/onion_skin_view.png b/doc/user/discussions/img/onion_skin_view.png
new file mode 100644
index 00000000000..91c3b396844
--- /dev/null
+++ b/doc/user/discussions/img/onion_skin_view.png
Binary files differ
diff --git a/doc/user/discussions/img/start_image_discussion.gif b/doc/user/discussions/img/start_image_discussion.gif
new file mode 100644
index 00000000000..43efbf2fbb2
--- /dev/null
+++ b/doc/user/discussions/img/start_image_discussion.gif
Binary files differ
diff --git a/doc/user/discussions/img/swipe_view.png b/doc/user/discussions/img/swipe_view.png
new file mode 100644
index 00000000000..82d6e52173c
--- /dev/null
+++ b/doc/user/discussions/img/swipe_view.png
Binary files differ
diff --git a/doc/user/discussions/img/two_up_view.png b/doc/user/discussions/img/two_up_view.png
new file mode 100644
index 00000000000..d9e90708e87
--- /dev/null
+++ b/doc/user/discussions/img/two_up_view.png
Binary files differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index ab46befb91c..2206b2860f4 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -153,42 +153,71 @@ comments in greater detail.
![Discussion comment](img/discussion_comment.png)
-## Locking discussions
+## Image discussions
-> [Introduced][ce-14531] in GitLab 10.1.
+> [Introduced][ce-14061] in GitLab 10.1.
+
+Sometimes a discussion is revolved around an image. With image discussions,
+you can easily target a specific coordinate of an image and start a discussion
+around it. Image discussions are available in merge requests and commit detail views.
+
+To start an image discussion, hover your mouse over the image. Your mouse pointer
+should convert into an icon, indicating that the image is available for commenting.
+Simply click anywhere on the image to create a new discussion.
+
+![Start image discussion](img/start_image_discussion.gif)
+
+After you click on the image, a comment form will be displayed that would be the start
+of your discussion. Once you save your comment, you will see a new badge displayed on
+top of your image. This badge represents your discussion.
+
+>**Note:**
+This discussion badge is typically associated with a number that is only used as a visual
+reference for each discussion. In the merge request discussion tab,
+this badge will be indicated with a comment icon since each discussion will render a new
+image section.
-There might be some cases where a discussion is better off if it's locked down.
-For example:
+Image discussions also work on diffs that replace an existing image. In this diff view
+mode, you can toggle the different view modes and still see the discussion point badges.
-- Discussions that are several years old and the issue/merge request is closed,
- but people continue to try to resurrect the discussion.
-- Discussions where someone or a group of people are trolling, are abusive, or
- in-general are causing the discussion to be unproductive.
+| 2-up | Swipe | Onion Skin |
+| :-----------: | :----------: | :----------: |
+| ![2-up view](img/two_up_view.png) | ![swipe view](img/swipe_view.png) | ![onion skin view](img/onion_skin_view.png) |
+
+Image discussions also work well with resolvable discussions. Resolved discussions
+on diffs (not on the merge request discussion tab) will appear collapsed on page
+load and will have a corresponding badge counter to match the counter on the image.
+
+![Image resolved discussion](img/image_resolved_discussion.png)
+
+## Lock discussions
+
+> [Introduced][ce-14531] in GitLab 10.1.
-In locked discussions, only team members can write new comments and edit the old
-ones.
+For large projects with many contributors, it may be useful to stop discussions
+in issues or merge requests in these scenarios:
-To lock or unlock a discussion, you need to have at least Master [permissions]:
+- The project maintainer has already resolved the discussion and it is not helpful
+for continued feedback. The project maintainer has already directed new conversation
+to newer issues or merge requests.
+- The people participating in the discussion are trolling, abusive, or otherwise
+being unproductive.
-1. Find the "Lock" section in the sidebar and click **Edit**
-1. In the dialog that will appear, you can choose to turn on or turn off the
- discussion lock
-1. Optionally, leave a comment to explain your reasoning behind that action
+In these cases, a user with Master permissions or higher in the project can lock (and unlock)
+an issue or a merge request, using the "Lock" section in the sidebar:
-| Turn off discussion lock | Turn on discussion lock |
+| Unlock | Lock |
| :-----------: | :----------: |
| ![Turn off discussion lock](img/turn_off_lock.png) | ![Turn on discussion lock](img/turn_on_lock.png) |
-Every change is indicated by a system note in the issue's or merge request's
-comments.
+System notes indicate locking and unlocking.
![Discussion lock system notes](img/discussion_lock_system_notes.png)
-Once an issue or merge request is locked, project members can see the indicator
-in the comment area, whereas non project members can only see the information
-that the discussion is locked.
+In a locked issue or merge request, only team members can add new comments and
+edit existing comments. Non-team members are restricted from adding or editing comments.
-| Team member | Not a member |
+| Team member | Non-team member |
| :-----------: | :----------: |
| ![Comment form member](img/lock_form_member.png) | ![Comment form non-member](img/lock_form_non_member.png) |
@@ -198,6 +227,7 @@ that the discussion is locked.
[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266
[ce-14053]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14053
+[ce-14061]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14061
[ce-14531]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14531
[resolve-discussion-button]: img/resolve_discussion_button.png
[resolve-comment-button]: img/resolve_comment_button.png
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 454988b9b80..fb61e360996 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -368,6 +368,37 @@ _Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._
>**Note:**
This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual].
+### Mermaid
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#mermaid
+
+It is possible to generate diagrams and flowcharts from text using [Mermaid][mermaid].
+
+In order to generate a diagram or flowchart, you should write your text inside the `mermaid` block.
+
+Example:
+
+ ```mermaid
+ graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+ ```
+
+Becomes:
+
+```mermaid
+graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+```
+
+For details see the [Mermaid official page][mermaid].
+
## Standard Markdown
### Headers
@@ -814,6 +845,7 @@ A link starting with a `/` is relative to the wiki root.
[^2]: This is my awesome footnote.
[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md
+[mermaid]: https://mermaidjs.github.io/ "Mermaid website"
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
[katex]: https://github.com/Khan/KaTeX "KaTeX website"
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index be72d42625a..b9532bf897f 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -25,7 +25,7 @@ The following table depicts the various user permission levels in a project.
| Create confidential issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| View confidential issues | (✓) [^2] | ✓ | ✓ | ✓ | ✓ |
| Leave comments | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| Lock comments | | | | ✓ | ✓ |
+| Lock discussions (issues and merge requests) | | | | ✓ | ✓ |
| See a list of jobs | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| See a job log | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| Download and browse job artifacts | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
@@ -54,7 +54,7 @@ The following table depicts the various user permission levels in a project.
| Create or update commit status | | | ✓ | ✓ | ✓ |
| Update a container registry | | | ✓ | ✓ | ✓ |
| Remove a container registry image | | | ✓ | ✓ | ✓ |
-| Create new milestones | | | | ✓ | ✓ |
+| Create/edit/delete project milestones | | | ✓ | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ |
| Enable/disable branch protection | | | | ✓ | ✓ |
@@ -76,6 +76,7 @@ The following table depicts the various user permission levels in a project.
| Force push to protected branches [^4] | | | | | |
| Remove protected branches [^4] | | | | | |
| Remove pages | | | | | ✓ |
+| Manage clusters | | | | ✓ | ✓ |
## Project features permissions
@@ -143,6 +144,7 @@ group.
| Manage group members | | | | | ✓ |
| Remove group | | | | | ✓ |
| Manage group labels | | ✓ | ✓ | ✓ | ✓ |
+| Create/edit/delete group milestones | | | ✓ | ✓ | ✓ |
### Subgroup permissions
@@ -195,6 +197,7 @@ instance and project. In addition, all admins can use the admin interface under
|---------------------------------------|-----------------|-------------|----------|--------|
| See commits and jobs | ✓ | ✓ | ✓ | ✓ |
| Retry or cancel job | | ✓ | ✓ | ✓ |
+| Erase job artifacts and trace | | ✓ [^7] | ✓ | ✓ |
| Remove project | | | ✓ | ✓ |
| Create project | | | ✓ | ✓ |
| Change project configuration | | | ✓ | ✓ |
@@ -259,5 +262,6 @@ only.
[^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.
+[^7]: Only if the build was triggered by the user
[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
[new-mod]: project/new_ci_build_permissions_model.md
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 5ebb88bf324..5fcc0501dc1 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -52,7 +52,7 @@ You can edit your account settings by navigating from the up-right corner menu b
From there, you can:
- Update your personal information
-- Manage [private tokens](../../api/README.md#private-tokens), email tokens, [2FA](account/two_factor_authentication.md)
+- Manage [2FA](account/two_factor_authentication.md)
- Change your username and [delete your account](account/delete_account.md)
- Manage applications that can
[use GitLab as an OAuth provider](../../integration/oauth_provider.md#introduction-to-oauth)
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index f28c034e74c..9b4fdd65e2f 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -2,17 +2,15 @@
> [Introduced][ce-3749] in GitLab 8.8.
-Personal access tokens are useful if you need access to the [GitLab API][api].
-Instead of using your private token which grants full access to your account,
-personal access tokens could be a better fit because of their
-[granular permissions](#limiting-scopes-of-a-personal-access-token).
+Personal access tokens are the preferred way for third party applications and scripts to
+authenticate with the [GitLab API][api], if using [OAuth2](../../api/oauth2.md) is not practical.
You can also use them to authenticate against Git over HTTP. They are the only
accepted method of authentication when you have
[Two-Factor Authentication (2FA)][2fa] enabled.
Once you have your token, [pass it to the API][usage] using either the
-`private_token` parameter or the `PRIVATE-TOKEN` header.
+`private_token` parameter or the `Private-Token` header.
The expiration of personal access tokens happens on the date you define,
at midnight UTC.
@@ -49,12 +47,14 @@ the following table.
|`read_user` | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed ([introduced][ce-5951] in GitLab 8.15). |
| `api` | Grants complete access to the API (read/write) ([introduced][ce-5951] in GitLab 8.15). Required for accessing Git repositories over HTTP when 2FA is enabled. |
| `read_registry` | Allows to read [container registry] images if a project is private and authorization is required ([introduced][ce-11845] in GitLab 9.3). |
+| `sudo` | Allows performing API actions as any user in the system (if the authenticated user is an admin) ([introduced][ce-14838] in GitLab 10.2). |
[2fa]: ../account/two_factor_authentication.md
[api]: ../../api/README.md
[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
[ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951
[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845
+[ce-14838]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14838
[container registry]: ../project/container_registry.md
[users]: ../../api/users.md
-[usage]: ../../api/README.md#basic-usage
+[usage]: ../../api/README.md#personal-access-tokens
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index f2ad42f21fd..022d6317555 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -55,9 +55,10 @@ You have 6 options here that you can use for your default dashboard view:
The project home page content setting allows you to choose what content you want to
see on a project’s home page.
-You can choose between 2 options:
+You can choose between 3 options:
- Show the files and the readme (default)
+- Show the readme
- Show the project’s activity
[rouge]: http://rouge.jneen.net/ "Rouge website"
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
new file mode 100644
index 00000000000..e2924c66e70
--- /dev/null
+++ b/doc/user/project/clusters/index.md
@@ -0,0 +1,113 @@
+# Connecting GitLab with a Kubernetes cluster
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35954) in 10.1.
+
+CAUTION: **Warning:**
+The Cluster integration is currently in **Beta**.
+
+With a cluster associated to your project, you can use Review Apps, deploy your
+applications, run your pipelines, and much more, in an easy way.
+
+Connect your project to Google Kubernetes Engine (GKE) or your own Kubernetes
+cluster in a few steps.
+
+NOTE: **Note:**
+The Cluster integration will eventually supersede the
+[Kubernetes integration](../integrations/kubernetes.md). For the moment,
+you can create only one cluster.
+
+## Prerequisites
+
+In order to be able to manage your GKE cluster through GitLab, the following
+prerequisites must be met:
+
+- The [Google authentication integration](../../../integration/google.md) must
+ be enabled in GitLab at the instance level. If that's not the case, ask your
+ administrator to enable it.
+- Your associated Google account must have the right privileges to manage
+ clusters on GKE. That would mean that a
+ [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account)
+ must be set up.
+- You must have Master [permissions] in order to be able to access the **Cluster**
+ page.
+
+If all of the above requirements are met, you can proceed to add a new GKE
+cluster.
+
+## Adding a cluster
+
+NOTE: **Note:**
+You need Master [permissions] and above to add a cluster.
+
+There are two options when adding a new cluster; either use Google Kubernetes
+Engine (GKE) or provide the credentials to your own Kubernetes cluster.
+
+To add a new cluster:
+
+1. Navigate to your project's **CI/CD > Cluster** page
+1. If you want to let GitLab create a cluster on GKE for you, go through the
+ following steps, otherwise skip to the next one.
+ 1. Click on **Create with GKE**
+ 1. Connect your Google account if you haven't done already by clicking the
+ **Sign in with Google** button
+ 1. Fill in the requested values:
+ - **Cluster name** (required) - The name you wish to give the cluster.
+ - **GCP project ID** (required) - The ID of the project you created in your GCP
+ console that will host the Kubernetes cluster. This must **not** be confused
+ with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
+ - **Zone** - The [zone](https://cloud.google.com/compute/docs/regions-zones/)
+ under which the cluster will be created.
+ - **Number of nodes** - The number of nodes you wish the cluster to have.
+ - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types)
+ of the Virtual Machine instance that the cluster will be based on.
+ - **Project namespace** - The unique namespace for this project. By default you
+ don't have to fill it in; by leaving it blank, GitLab will create one for you.
+1. If you want to use your own existing Kubernetes cluster, click on
+ **Add an existing cluster** and fill in the details as described in the
+ [Kubernetes integration](../integrations/kubernetes.md) documentation.
+1. Finally, click the **Create cluster** button
+
+After a few moments, your cluster should be created. If something goes wrong,
+you will be notified.
+
+You can now proceed to install some pre-defined applications and then
+enable the Cluster integration.
+
+## Installing applications
+
+GitLab provides a one-click install for various applications which will be
+added directly to your configured cluster. Those applications are needed for
+[Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md).
+
+| Application | GitLab version | Description |
+| ----------- | :------------: | ----------- |
+| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. |
+| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. |
+
+## Enabling or disabling the Cluster integration
+
+After you have successfully added your cluster information, you can enable the
+Cluster integration:
+
+1. Click the "Enabled/Disabled" switch
+1. Hit **Save** for the changes to take effect
+
+You can now start using your Kubernetes cluster for your deployments.
+
+To disable the Cluster integration, follow the same procedure.
+
+## Removing the Cluster integration
+
+NOTE: **Note:**
+You need Master [permissions] and above to remove a cluster integration.
+
+NOTE: **Note:**
+When you remove a cluster, you only remove its relation to GitLab, not the
+cluster itself. To remove the cluster, you can do so by visiting the GKE
+dashboard or using `kubectl`.
+
+To remove the Cluster integration from your project, simply click on the
+**Remove integration** button. You will then be able to follow the procedure
+and [add a cluster](#adding-a-cluster) again.
+
+[permissions]: ../../permissions.md
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 2c4dfcff4a6..394aa9209e4 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -27,7 +27,8 @@ to enable it.
1. First, ask your system administrator to enable GitLab Container Registry
following the [administration documentation](../../administration/container_registry.md).
If you are using GitLab.com, this is enabled by default so you can start using
- the Registry immediately.
+ the Registry immediately. Currently there is a soft (10GB) size restriction for
+ registry on GitLab.com, as part of the [repository size limit](repository/index.html#repository-size).
1. Go to your [project's General settings](settings/index.md#sharing-and-permissions)
and enable the **Container Registry** feature on your project. For new
projects this might be enabled by default. For existing projects
diff --git a/doc/user/project/img/label_priority_sort_order.png b/doc/user/project/img/label_priority_sort_order.png
new file mode 100644
index 00000000000..21c7a76a322
--- /dev/null
+++ b/doc/user/project/img/label_priority_sort_order.png
Binary files differ
diff --git a/doc/user/project/img/labels_filter_by_priority.png b/doc/user/project/img/labels_filter_by_priority.png
deleted file mode 100644
index 419e555e709..00000000000
--- a/doc/user/project/img/labels_filter_by_priority.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/priority_sort_order.png b/doc/user/project/img/priority_sort_order.png
new file mode 100644
index 00000000000..c558ec23b0e
--- /dev/null
+++ b/doc/user/project/img/priority_sort_order.png
Binary files differ
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index 6423beefc77..72def9d1d1d 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -24,6 +24,8 @@ constrains of a Sidekiq worker.
- the milestones (GitLab 8.7+)
- the labels (GitLab 8.7+)
- the release note descriptions (GitLab 8.12+)
+ - the pull request review comments (GitLab 10.2+)
+ - the regular issue and pull request comments
- References to pull requests and issues are preserved (GitLab 8.7+)
- Repository public access is retained. If a repository is private in GitHub
it will be created as private in GitLab as well.
@@ -43,10 +45,13 @@ the case the namespace is taken, the repository will be imported under the user'
namespace that started the import process.
The importer will also import branches on forks of projects related to open pull
-requests. These branches will be imported with a naming scheume similar to
+requests. These branches will be imported with a naming scheme similar to
GH-SHA-Username/Pull-Request-number/fork-name/branch. This may lead to a discrepency
in branches compared to the GitHub Repository.
+For a more technical description and an overview of the architecture you can
+refer to [Working with the GitHub importer][gh-import-dev-docs].
+
## Importing your GitHub repositories
The importer page is visible when you create a new project.
@@ -121,7 +126,29 @@ If you want, you can import all your GitHub projects in one go by hitting
You can also choose a different name for the project and a different namespace,
if you have the privileges to do so.
+## Making the import process go faster
+
+For large projects it may take a while to import all data. To reduce the time
+necessary you can increase the number of Sidekiq workers that process the
+following queues:
+
+* `github_importer`
+* `github_importer_advance_stage`
+
+For an optimal experience we recommend having at least 4 Sidekiq processes (each
+running a number of threads equal to the number of CPU cores) that _only_
+process these queues. We also recommend that these processes run on separate
+servers. For 4 servers with 8 cores this means you can import up to 32 objects
+(e.g. issues) in parallel.
+
+Reducing the time spent in cloning a repository can be done by increasing
+network throughput, CPU capacity, and disk performance (e.g. by using high
+performance SSDs) of the disks that store the Git repositories (for your GitLab
+instance). Increasing the number of Sidekiq workers will _not_ reduce the time
+spent cloning repositories.
+
[gh-import]: ../../../integration/github.md "GitHub integration"
[gh-rake]: ../../../administration/raketasks/github_import.md "GitHub rake task"
[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration
[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token
+[gh-import-dev-docs]: ../../../development/github_importer.md "Working with the GitHub importer"
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
index 8da6e2a8207..e2b285678c3 100644
--- a/doc/user/project/import/index.md
+++ b/doc/user/project/import/index.md
@@ -4,7 +4,7 @@
1. [From ClearCase](clearcase.md)
1. [From CVS](cvs.md)
1. [From FogBugz](fogbugz.md)
-1. [From GitHub.com of GitHub Enterprise](github.md)
+1. [From GitHub.com or GitHub Enterprise](github.md)
1. [From GitLab.com](gitlab_com.md)
1. [From Gitea](gitea.md)
1. [From Perforce](perforce.md)
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 03bbc46bd8c..97d0d529886 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -63,6 +63,8 @@ common actions on issues or merge requests
browse, and download job artifacts
- [Pipeline settings](pipelines/settings.md): Set up Git strategy (choose the default way your repository is fetched from GitLab in a job),
timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more
+ - [GKE cluster integration](clusters/index.md): Connecting your GitLab project
+ with Google Container Engine
- [GitLab Pages](pages/index.md): Build, test, and deploy your static
website with GitLab Pages
diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md
new file mode 100644
index 00000000000..757522c2ae3
--- /dev/null
+++ b/doc/user/project/integrations/custom_issue_tracker.md
@@ -0,0 +1,20 @@
+# Custom Issue Tracker Service
+
+To enable the Custom Issue Tracker integration in a project, navigate to the
+[Integrations page](project_services.md#accessing-the-project-services), click
+the **Customer Issue Tracker** service, and fill in the required details on the page as described
+in the table below.
+
+| Field | Description |
+| ----- | ----------- |
+| `title` | A title for the issue tracker (to differentiate between instances, for example) |
+| `description` | A name for the issue tracker (to differentiate between instances, for example) |
+| `project_url` | Currently unused. Will be changed in a future release. |
+| `issues_url` | The URL to the issue in the issue tracker project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. For example, `https://customissuetracker.com/project-name/:id`. |
+| `new_issue_url` | Currently unused. Will be changed in a future release. |
+
+
+## Referencing issues
+
+Issues are referenced with `#<ID>`, where `<ID>` is a number (example `#143`).
+So with the example above, `#143` would refer to `https://customissuetracker.com/project-name/143`. \ No newline at end of file
diff --git a/doc/user/project/integrations/img/webhook_logs.png b/doc/user/project/integrations/img/webhook_logs.png
index 917068d9398..803678db6b6 100644
--- a/doc/user/project/integrations/img/webhook_logs.png
+++ b/doc/user/project/integrations/img/webhook_logs.png
Binary files differ
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 51989ccaaea..a0405161495 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -43,6 +43,7 @@ Click on the service links to see further configuration instructions and details
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
| [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors |
+| Packagist | Update your project on Packagist, the main Composer repository |
| Pipelines emails | Email the pipeline status to a list of recipients |
| [Slack Notifications](slack.md) | Send GitLab events (e.g. issue created) to Slack as notifications |
| [Slack slash commands](slack_slash_commands.md) | Use slash commands in Slack to control GitLab |
diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md
index 518683965e8..a6673fa2a00 100644
--- a/doc/user/project/integrations/prometheus_library/kubernetes.md
+++ b/doc/user/project/integrations/prometheus_library/kubernetes.md
@@ -13,8 +13,8 @@ integration services must be enabled.
| Name | Query |
| ---- | ----- |
-| Average Memory Usage (MB) | (sum(container_memory_usage_bytes{container_name!="POD",%{environment_filter}}) / count(container_memory_usage_bytes{container_name!="POD",%{environment_filter}})) /1024/1024 |
-| Average CPU Utilization (%) | sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100 |
+| Average Memory Usage (MB) | (sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024 |
+| Average CPU Utilization (%) | sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100 |
## Configuring Prometheus to monitor for Kubernetes node metrics
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 47eb0b34f66..eafdd28071d 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -76,6 +76,7 @@ X-Gitlab-Event: Push Hook
"user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
"project_id": 15,
"project":{
+ "id": 15,
"name":"Diaspora",
"description":"",
"web_url":"http://example.com/mike/diaspora",
@@ -156,6 +157,7 @@ X-Gitlab-Event: Tag Push Hook
"user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
"project_id": 1,
"project":{
+ "id": 1,
"name":"Example",
"description":"",
"web_url":"http://example.com/jsmith/example",
@@ -205,7 +207,8 @@ X-Gitlab-Event: Issue Hook
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
- "project":{
+ "project": {
+ "id": 1,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlabhq/gitlab-test",
@@ -221,7 +224,7 @@ X-Gitlab-Event: Issue Hook
"ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
"http_url":"http://example.com/gitlabhq/gitlab-test.git"
},
- "repository":{
+ "repository": {
"name": "Gitlab Test",
"url": "http://example.com/gitlabhq/gitlab-test.git",
"description": "Aut reprehenderit ut est.",
@@ -266,7 +269,37 @@ X-Gitlab-Event: Issue Hook
"description": "API related issues",
"type": "ProjectLabel",
"group_id": 41
- }]
+ }],
+ "changes": {
+ "updated_by_id": [null, 1],
+ "updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"],
+ "labels": {
+ "previous": [{
+ "id": 206,
+ "title": "API",
+ "color": "#ffffff",
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "template": false,
+ "description": "API related issues",
+ "type": "ProjectLabel",
+ "group_id": 41
+ }],
+ "current": [{
+ "id": 205,
+ "title": "Platform",
+ "color": "#123123",
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "template": false,
+ "description": "Platform related issues",
+ "type": "ProjectLabel",
+ "group_id": 41
+ }]
+ }
+ }
}
```
@@ -305,6 +338,7 @@ X-Gitlab-Event: Note Hook
},
"project_id": 5,
"project":{
+ "id": 5,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlabhq/gitlab-test",
@@ -384,6 +418,7 @@ X-Gitlab-Event: Note Hook
},
"project_id": 5,
"project":{
+ "id": 5,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlab-org/gitlab-test",
@@ -510,6 +545,7 @@ X-Gitlab-Event: Note Hook
},
"project_id": 5,
"project":{
+ "id": 5,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlab-org/gitlab-test",
@@ -588,6 +624,7 @@ X-Gitlab-Event: Note Hook
},
"project_id": 5,
"project":{
+ "id": 5,
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/gitlab-org/gitlab-test",
@@ -661,6 +698,29 @@ X-Gitlab-Event: Merge Request Hook
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
+ "project": {
+ "id": 1,
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlabhq/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
+ "namespace":"GitlabHQ",
+ "visibility_level":20,
+ "path_with_namespace":"gitlabhq/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlabhq/gitlab-test",
+ "url":"http://example.com/gitlabhq/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "http_url":"http://example.com/gitlabhq/gitlab-test.git"
+ },
+ "repository": {
+ "name": "Gitlab Test",
+ "url": "http://example.com/gitlabhq/gitlab-test.git",
+ "description": "Aut reprehenderit ut est.",
+ "homepage": "http://example.com/gitlabhq/gitlab-test"
+ },
"object_attributes": {
"id": 99,
"target_branch": "master",
@@ -671,15 +731,13 @@ X-Gitlab-Event: Merge Request Hook
"title": "MS-Viewport",
"created_at": "2013-12-03T17:23:34Z",
"updated_at": "2013-12-03T17:23:34Z",
- "st_commits": null,
- "st_diffs": null,
"milestone_id": null,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 14,
"iid": 1,
"description": "",
- "source":{
+ "source": {
"name":"Awesome Project",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/awesome_space/awesome_project",
@@ -729,6 +787,48 @@ X-Gitlab-Event: Merge Request Hook
"username": "user1",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
}
+ },
+ "labels": [{
+ "id": 206,
+ "title": "API",
+ "color": "#ffffff",
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "template": false,
+ "description": "API related issues",
+ "type": "ProjectLabel",
+ "group_id": 41
+ }],
+ "changes": {
+ "updated_by_id": [null, 1],
+ "updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"],
+ "labels": {
+ "previous": [{
+ "id": 206,
+ "title": "API",
+ "color": "#ffffff",
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "template": false,
+ "description": "API related issues",
+ "type": "ProjectLabel",
+ "group_id": 41
+ }],
+ "current": [{
+ "id": 205,
+ "title": "Platform",
+ "color": "#123123",
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "template": false,
+ "description": "Platform related issues",
+ "type": "ProjectLabel",
+ "group_id": 41
+ }]
+ }
}
}
```
@@ -754,6 +854,7 @@ X-Gitlab-Event: Wiki Page Hook
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon"
},
"project": {
+ "id": 1,
"name": "awesome-project",
"description": "This is awesome",
"web_url": "http://example.com/root/awesome-project",
@@ -825,6 +926,7 @@ X-Gitlab-Event: Pipeline Hook
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
},
"project":{
+ "id": 1,
"name": "Gitlab Test",
"description": "Atque in sunt eos similique dolores voluptatem.",
"web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
@@ -1015,7 +1117,7 @@ X-Gitlab-Event: Build Hook
## Testing webhooks
-You can trigger the webhook manually. Sample data from the project will be used.Sample data will take from the project.
+You can trigger the webhook manually. Sample data from the project will be used.Sample data will take from the project.
> For example: for triggering `Push Events` your project should have at least one commit.
![Webhook testing](img/webhook_testing.png)
@@ -1036,6 +1138,18 @@ From this page, you can repeat delivery with the same data by clicking `Resend R
>**Note:** If URL or secret token of the webhook were updated, data will be delivered to the new address.
+### Receiving duplicate or multiple web hook requests triggered by one event
+
+When GitLab sends a webhook it expects a response in 10 seconds (set default value). If it does not receive one, it'll retry the webhook.
+If the endpoint doesn't send its HTTP response within those 10 seconds, GitLab may decide the hook failed and retry it.
+
+If you are receiving multiple requests, you can try increasing the default value to wait for the HTTP response after sending the webhook
+by uncommenting or adding the following setting to your `/etc/gitlab/gitlab.rb`:
+
+```
+gitlab_rails['webhook_timeout'] = 10
+```
+
## Example webhook receiver
If you want to see GitLab's webhooks in action for testing purposes you can use
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 96a5a23ee13..8c2690ec3b2 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -166,12 +166,26 @@ board itself.
![Remove issue from list](img/issue_boards_remove_issue.png)
-## Re-ordering an issue in a list
-
-> Introduced in GitLab 9.0.
-
-Issues can be re-ordered inside of lists. This is as simple as dragging and dropping
-an issue into the order you want.
+## Issue ordering in a list
+
+When visiting a board, issues appear ordered in any list. You are able to change
+that order simply by dragging and dropping the issues. The changed order will be saved
+to the system so that anybody who visits the same board later will see the reordering,
+with some exceptions.
+
+The first time a given issue appears in any board (i.e. the first time a user
+loads a board containing that issue), it will be ordered with
+respect to other issues in that list according to [Priority order][label-priority].
+At that point, that issue will be assigned a relative order value by the system
+representing its relative order with respect to the other issues in the list. Any time
+you drag-and-drop reorder that issue, its relative order value will change accordingly.
+Also, any time that issue appears in any board when it is loaded by a user,
+the updated relative order value will be used for the ordering. (It's only the first
+time an issue appears that it takes from the Priority order mentioned above.) This means that
+if issue `A` is drag-and-drop reordered to be above issue `B` by any user in
+a given board inside your GitLab instance, any time those two issues are subsequently
+loaded in any board in the same instance (could be a different project board or a different group board, for example),
+that ordering will be maintained.
## Filtering issues
diff --git a/doc/user/project/issues/automatic_issue_closing.md b/doc/user/project/issues/automatic_issue_closing.md
index d6f3a7d5555..b9607243c8a 100644
--- a/doc/user/project/issues/automatic_issue_closing.md
+++ b/doc/user/project/issues/automatic_issue_closing.md
@@ -1,8 +1,10 @@
# Automatic issue closing
->**Note:**
-This is the user docs. In order to change the default issue closing pattern,
-follow the steps in the [administration docs].
+>**Notes:**
+> - This is the user docs. In order to change the default issue closing pattern,
+> follow the steps in the [administration docs].
+> - For performance reasons, automatic issue closing is disabled for the very
+> first push from an existing repository.
When a commit or merge request resolves one or more issues, it is possible to
automatically have these issues closed when the commit or merge request lands
@@ -10,8 +12,9 @@ in the project's default branch.
If a commit message or merge request description contains a sentence matching
a certain regular expression, all issues referenced from the matched text will
-be closed. This happens when the commit is pushed to a project's **default**
-branch, or when a commit or merge request is merged into it.
+be closed. This happens when the commit is pushed to a project's
+[**default** branch](../repository/branches/index.md#default-branch), or when a
+commit or merge request is merged into it.
## Default closing pattern value
@@ -19,7 +22,7 @@ When not specified, the default issue closing pattern as shown below will be
used:
```bash
-((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)
+((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)
```
Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's
@@ -34,6 +37,7 @@ This translates to the following keywords:
- Close, Closes, Closed, Closing, close, closes, closed, closing
- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing
- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving
+- Implement, Implements, Implemented, Implementing, implement, implements, implemented, implementing
---
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index 21a2e1213ec..d7eb4bca89c 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -77,15 +77,32 @@ having their priority set to null.
![Prioritize labels](img/labels_prioritize.png)
-Now that you have labels prioritized, you can use the 'Priority' and 'Label
-priority' filters in the issues or merge requests tracker.
+Now that you have labels prioritized, you can use the 'Label priority' and 'Priority'
+sort orders in the issues or merge requests tracker.
-The 'Label priority' filter puts issues with the highest priority label on top.
+In the following, everything applies to both issues and merge requests, but we'll
+refer to just issues for brevity.
-The 'Priority' filter sorts issues by their soonest milestone due date, then by
-label priority.
+The 'Label priority' sort order positions issues with higher priority labels
+toward the top, and issues with lower priority labels toward the bottom. A non-prioritized
+label is considered to have the lowest priority. For a given issue, we _only_ consider the
+highest priority label assigned to it in the comparison. ([We are discussing](https://gitlab.com/gitlab-org/gitlab-ce/issues/18554)
+including all the labels in a given issue for this comparison.) Given two issues
+are equal according to this sort comparison, their relative order is equal, and
+therefore it's not guaranteed that one will be always above the other.
+
+![Label priority sort order](img/label_priority_sort_order.png)
+
+The 'Priority' sort order comparison first considers an issue's milestone's due date,
+(if the issue is assigned a milestone and the milestone's due date exists), and then
+secondarily considers the label priority comparison above. Sooner due dates results
+a higher sort order. If an issue doesn't have a milestone due date, it is equivalent to
+being assigned to a milestone that has a due date in the infinite future. Given two issues
+are equal according to this two-stage sort comparison, their relative order is equal, and
+therefore it's not guaranteed that one will be always above the other.
+
+![Priority sort order](img/priority_sort_order.png)
-![Filter labels by priority](img/labels_filter_by_priority.png)
## Subscribe to labels
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
index b8dd96087f1..43713855e26 100644
--- a/doc/user/project/members/index.md
+++ b/doc/user/project/members/index.md
@@ -21,7 +21,7 @@ want to add.
---
-Select the user and the [permission level](../../user/permissions.md)
+Select the user and the [permission level](../../permissions.md)
that you'd like to give the user. Note that you can select more than one user.
![Give user permissions](img/add_user_give_permissions.png)
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 6289fcf3c2b..4b2e042251b 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -34,7 +34,6 @@ With **[GitLab Enterprise Edition][ee]**, you can also:
- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Enterprise Edition Premium)
- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Enterprise Edition Starter)
- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Enterprise Edition Starter)
-- Enable [semi-linear history merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch (available in GitLab Enterprise Edition Starter)
- Analise the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Enterprise Edition Starter)
## Use cases
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index 876b98a4dc5..83adbd8cce2 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -29,7 +29,8 @@ In addition to that you will be able to filter issues or merge requests by group
## Milestone promotion
-You will be able to promote a project milestone to a group milestone [in the future](https://gitlab.com/gitlab-org/gitlab-ce/issues/35833).
+Project milestones can be promoted to group milestones if its project belongs to a group. When a milestone is promoted all other milestones across the group projects with the same title will be merged into it, which means all milestone's children like issues, merge requests and boards will be moved into the new promoted milestone.
+The promote button can be found in the milestone view or milestones list.
## Special milestone filters
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index 271adee7da1..17dcd152363 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -230,7 +230,7 @@ test:
- docker run $CI_REGISTRY/group/other-project:latest
```
-[job permissions]: ../permissions.md#jobs-permissions
+[job permissions]: ../permissions.md#job-permissions
[comment]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22484#note_16648302
[ext]: ../permissions.md#external-users
[gitsub]: ../../ci/git_submodules.md
diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md
index 453e10184f0..1e19f422d94 100644
--- a/doc/user/project/pages/getting_started_part_one.md
+++ b/doc/user/project/pages/getting_started_part_one.md
@@ -62,7 +62,7 @@ which is highly recommendable and much faster than hardcoding.
If you set up a GitLab Pages project on GitLab.com,
it will automatically be accessible under a
-[subdomain of `namespace.pages.io`](introduction.md#gitlab-pages-on-gitlab-com).
+[subdomain of `namespace.gitlab.io`](introduction.md#gitlab-pages-on-gitlab-com).
The `namespace` is defined by your username on GitLab.com,
or the group name you created this project under.
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
index 4fcdfa7b281..3ab88948fbd 100644
--- a/doc/user/project/pages/introduction.md
+++ b/doc/user/project/pages/introduction.md
@@ -3,7 +3,7 @@
> **Notes:**
> - This feature was [introduced][ee-80] in GitLab EE 8.3.
> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5.
-> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17.
+> - GitLab Pages [was ported][ce-14605] to Community Edition in GitLab 8.17.
> - This document is about the user guide. To learn how to enable GitLab Pages
> across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md).
@@ -174,7 +174,7 @@ job, the contents of the `public` directory will be served by GitLab Pages.
#### How `.gitlab-ci.yml` looks like when the static content is in your repository
-Supposedly your repository contained the following files:
+Supposed your repository contained the following files:
```
├── index.html
@@ -419,7 +419,7 @@ You can only create the highest level group website.
## Redirects in GitLab Pages
Since you cannot use any custom server configuration files, like `.htaccess` or
-any `.conf` file for that matter, if you want to redirect a web page to another
+any `.conf` file, if you want to redirect a page to another
location, you can use the [HTTP meta refresh tag][metarefresh].
Some static site generators provide plugins for that functionality so that you
@@ -434,7 +434,7 @@ Sure. All you need to do is download the artifacts archive from the job page.
### Can I use GitLab Pages if my project is private?
-Yes. GitLab Pages don't care whether you set your project's visibility level
+Yes. GitLab Pages doesn't care whether you set your project's visibility level
to private, internal or public.
### Do I need to create a user/group website before creating a project website?
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index 9ef6f9185c9..f9a268fb789 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -52,7 +52,8 @@ directly in the job artifacts browser without the need to download them.
>**Note:**
With [GitLab 10.1][ce-14399], HTML files in a public project can be previewed
-directly in a new tab without the need to download them.
+directly in a new tab without the need to download them when
+[GitLab Pages](../../../administration/pages/index.md) is enabled
After a job finishes, if you visit the job's specific page, there are three
buttons. You can download the artifacts archive or browse its contents, whereas
@@ -69,7 +70,8 @@ browse inside them.
Below you can see how browsing looks like. In this case we have browsed inside
the archive and at this point there is one directory, a couple files, and
-one HTML file that you can view directly online (opens in a new tab).
+one HTML file that you can view directly online when
+[GitLab Pages](../../../administration/pages/index.md) is enabled (opens in a new tab).
![Job artifacts browser](img/job_artifacts_browser.png)
diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md
index 9ad15a12c3c..2101e3b1d58 100644
--- a/doc/user/project/pipelines/schedules.md
+++ b/doc/user/project/pipelines/schedules.md
@@ -5,7 +5,7 @@
- In 9.2, the feature was [renamed to Pipeline Schedule][ce-10853].
- Cron notation is parsed by [Rufus-Scheduler](https://github.com/jmettraux/rufus-scheduler).
-Pipeline schedules can be used to run pipelines only once, or for example every
+Pipeline schedules can be used to run a pipeline at specific intervals, for example every
month on the 22nd for a certain branch.
## Using Pipeline schedules
@@ -44,7 +44,7 @@ GitLab CI so that they can be used in your `.gitlab-ci.yml` file.
To configure that a job can be executed only when the pipeline has been
scheduled (or the opposite), you can use
-[only and except](../../../ci/yaml/README.md#only-and-except) configuration keywords.
+[only and except](../../../ci/yaml/README.md#only-and-except-simplified) configuration keywords.
```
job:on-schedule:
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 56f58fd755a..daa5463d680 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -115,10 +115,12 @@ pages.
Depending on the status of your job, a badge can have the following values:
+- pending
- running
-- success
+- passed
- failed
- skipped
+- canceled
- unknown
You can access a pipeline status badge image using the following link:
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 6a5d2d40927..e81e935e37d 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -32,7 +32,7 @@ do.
| `/wip` | Toggle the Work In Progress status |
| <code>/estimate &lt;1w 3d 2h 14m&gt;</code> | Set time estimate |
| `/remove_estimate` | Remove estimated time |
-| <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or subtract spent time |
+| <code>/spend &lt;time(1h 30m &#124; -1h 5m)&gt; &lt;date(YYYY-MM-DD)&gt;</code> | Add or subtract spent time; optionally, specify the date that time was spent on |
| `/remove_time_spent` | Remove time spent |
| `/target_branch <Branch Name>` | Set target branch for current merge request |
| `/award :emoji:` | Toggle award for :emoji: |
diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md
index e1d3aebb8b3..26c55891b3c 100644
--- a/doc/user/project/repository/branches/index.md
+++ b/doc/user/project/repository/branches/index.md
@@ -37,7 +37,7 @@ This feature allows merged branches to be deleted in bulk. Only branches that
have been merged and [are not protected][protected] will be deleted as part of
this operation.
-It's particularly useful to clean up old branches that were not deleting
+It's particularly useful to clean up old branches that were not deleted
automatically when a merge request was merged.
[ce-6449]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6449 "Add button to delete all merged branches"
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index d47a3acdbe9..db0c3ed9d59 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -105,7 +105,7 @@ merge request.
Once you click it, a new branch will be created that diverges from the default
branch of your project, by default `master`. The branch name will be based on
-the title of the issue and as suffix it will have its ID. Thus, the example
+the title of the issue and as a prefix, it will have its internal ID. Thus, the example
screenshot above will yield a branch named
`2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`.
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 23b1c61cd16..1b8a84c9599 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -30,7 +30,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| ---------------- | --------------------- |
-| 10.0 to current | 0.2.0 |
+| 10.3 to current | 0.2.1 |
+| 10.0 | 0.2.0 |
| 9.4.0 | 0.1.8 |
| 9.2.0 | 0.1.7 |
| 8.17.0 | 0.1.6 |
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 6b2aba47f54..272f7807ac0 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -1,3 +1,7 @@
+---
+comments: false
+---
+
# Workflow
- [Automatic issue closing](../user/project/issues/automatic_issue_closing.md)
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 9d466ae1971..23b67310d25 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -1,6 +1,6 @@
![GitLab Flow](gitlab_flow.png)
-## Introduction
+# Introduction to GitLab Flow
Version management with git makes branching and merging much easier than older versioning systems such as SVN.
This allows a wide variety of branching strategies and workflows.
diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md
index f753708ad89..e0a445920b4 100644
--- a/doc/workflow/importing/README.md
+++ b/doc/workflow/importing/README.md
@@ -1 +1 @@
-This document was moved to a [new location](../../user/project/import/index.md).
+This document was moved to [another location](../../user/project/import/index.md).
diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md
index 248c3990372..ec9a11f390e 100644
--- a/doc/workflow/importing/import_projects_from_bitbucket.md
+++ b/doc/workflow/importing/import_projects_from_bitbucket.md
@@ -1 +1 @@
-This document was moved to a [new location](../../user/project/import/bitbucket.md).
+This document was moved to [another location](../../user/project/import/bitbucket.md).
diff --git a/doc/workflow/importing/import_projects_from_fogbugz.md b/doc/workflow/importing/import_projects_from_fogbugz.md
index 050746e2b4d..876eb0434f0 100644
--- a/doc/workflow/importing/import_projects_from_fogbugz.md
+++ b/doc/workflow/importing/import_projects_from_fogbugz.md
@@ -1 +1 @@
-This document was moved to a [new location](../../user/project/import/fogbugz.md).
+This document was moved to [another location](../../user/project/import/fogbugz.md).
diff --git a/doc/workflow/importing/import_projects_from_gitea.md b/doc/workflow/importing/import_projects_from_gitea.md
index cb90c490b0f..8b55b6c23eb 100644
--- a/doc/workflow/importing/import_projects_from_gitea.md
+++ b/doc/workflow/importing/import_projects_from_gitea.md
@@ -1 +1 @@
-This document was moved to a [new location](../../user/project/import/gitea.md).
+This document was moved to [another location](../../user/project/import/gitea.md).
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index 13639feaa04..72dfe5403c3 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -1 +1 @@
-This document was moved to a [new location](../../user/project/import/github.md).
+This document was moved to [another location](../../user/project/import/github.md).
diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md
index df4c180919a..3256088c014 100644
--- a/doc/workflow/importing/import_projects_from_gitlab_com.md
+++ b/doc/workflow/importing/import_projects_from_gitlab_com.md
@@ -1 +1 @@
-This document was moved to a [new location](../../user/project/import/gitlab_com.md).
+This document was moved to [another location](../../user/project/import/gitlab_com.md).
diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md
index 81df3fbcdbb..32a75a6c6af 100644
--- a/doc/workflow/importing/migrating_from_svn.md
+++ b/doc/workflow/importing/migrating_from_svn.md
@@ -1 +1 @@
-This document was moved to a [new location](../../user/project/import/svn.md).
+This document was moved to [another location](../../user/project/import/svn.md).
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index 87416008e98..2e1bd6bfe5c 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -9,7 +9,7 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>n</kbd> | Main navigation |
| <kbd>s</kbd> | Focus search |
| <kbd>f</kbd> | Focus filter |
-| <kbd>p b</kbd> | Show/hide the Performance Bar |
+| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar |
| <kbd>?</kbd> | Show/hide this dialog |
| <kbd>⌘</kbd> + <kbd>shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
| <kbd>↑</kbd> | Edit last comment (when focused on an empty textarea) |
diff --git a/features/explore/groups.feature b/features/explore/groups.feature
index 9eacbe0b25e..830810615e0 100644
--- a/features/explore/groups.feature
+++ b/features/explore/groups.feature
@@ -3,6 +3,7 @@ Feature: Explore Groups
Background:
Given group "TestGroup" has private project "Enterprise"
+ @javascript
Scenario: I should see group with private and internal projects as user
Given group "TestGroup" has internal project "Internal"
When I sign in as a user
@@ -10,6 +11,7 @@ Feature: Explore Groups
Then I should see project "Internal" items
And I should not see project "Enterprise" items
+ @javascript
Scenario: I should see group issues for internal project as user
Given group "TestGroup" has internal project "Internal"
When I sign in as a user
@@ -17,6 +19,7 @@ Feature: Explore Groups
Then I should see project "Internal" items
And I should not see project "Enterprise" items
+ @javascript
Scenario: I should see group merge requests for internal project as user
Given group "TestGroup" has internal project "Internal"
When I sign in as a user
@@ -24,6 +27,7 @@ Feature: Explore Groups
Then I should see project "Internal" items
And I should not see project "Enterprise" items
+ @javascript
Scenario: I should see group with private, internal and public projects as visitor
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
@@ -32,6 +36,7 @@ Feature: Explore Groups
And I should not see project "Internal" items
And I should not see project "Enterprise" items
+ @javascript
Scenario: I should see group issues for public project as visitor
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
@@ -40,6 +45,7 @@ Feature: Explore Groups
And I should not see project "Internal" items
And I should not see project "Enterprise" items
+ @javascript
Scenario: I should see group merge requests for public project as visitor
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
@@ -48,6 +54,7 @@ Feature: Explore Groups
And I should not see project "Internal" items
And I should not see project "Enterprise" items
+ @javascript
Scenario: I should see group with private, internal and public projects as user
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
@@ -57,6 +64,7 @@ Feature: Explore Groups
And I should see project "Internal" items
And I should not see project "Enterprise" items
+ @javascript
Scenario: I should see group issues for internal and public projects as user
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
@@ -66,6 +74,7 @@ Feature: Explore Groups
And I should see project "Internal" items
And I should not see project "Enterprise" items
+ @javascript
Scenario: I should see group merge requests for internal and public projects as user
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
@@ -75,17 +84,20 @@ Feature: Explore Groups
And I should see project "Internal" items
And I should not see project "Enterprise" items
+ @javascript
Scenario: I should see group with public project in public groups area
Given group "TestGroup" has public project "Community"
When I visit the public groups area
Then I should see group "TestGroup"
+ @javascript
Scenario: I should see group with public project in public groups area as user
Given group "TestGroup" has public project "Community"
When I sign in as a user
And I visit the public groups area
Then I should see group "TestGroup"
+ @javascript
Scenario: I should see group with internal project in public groups area as user
Given group "TestGroup" has internal project "Internal"
When I sign in as a user
diff --git a/features/explore/projects.feature b/features/explore/projects.feature
deleted file mode 100644
index 4e0f4486ab7..00000000000
--- a/features/explore/projects.feature
+++ /dev/null
@@ -1,144 +0,0 @@
-@public
-Feature: Explore Projects
- Background:
- Given public project "Community"
- And internal project "Internal"
- And private project "Enterprise"
-
- Scenario: I visit public area
- Given archived project "Archive"
- When I visit the public projects area
- Then I should see project "Community"
- And I should not see project "Internal"
- And I should not see project "Enterprise"
- And I should not see project "Archive"
-
- Scenario: I visit public project page
- When I visit project "Community" page
- Then I should see project "Community" home page
-
- Scenario: I visit internal project page
- When I visit project "Internal" page
- Then I should be redirected to sign in page
-
- Scenario: I visit private project page
- When I visit project "Enterprise" page
- Then I should be redirected to sign in page
-
- Scenario: I visit an empty public project page
- Given public empty project "Empty Public Project"
- When I visit empty project page
- Then I should see empty public project details
- And I should see empty public project details with http clone info
-
- Scenario: I visit an empty public project page as user with no ssh-keys
- Given I sign in as a user
- And I have no ssh keys
- And public empty project "Empty Public Project"
- When I visit empty project page
- Then I should see empty public project details
- And I should see empty public project details with http clone info
-
- Scenario: I visit an empty public project page as user with an ssh-key
- Given I sign in as a user
- And I have an ssh key
- And public empty project "Empty Public Project"
- When I visit empty project page
- Then I should see empty public project details
- And I should see empty public project details with ssh clone info
-
- Scenario: I visit public area as user
- Given archived project "Archive"
- And I sign in as a user
- When I visit the public projects area
- Then I should see project "Community"
- And I should see project "Internal"
- And I should not see project "Enterprise"
- And I should not see project "Archive"
-
- Scenario: I visit internal project page as user
- Given I sign in as a user
- When I visit project "Internal" page
- Then I should see project "Internal" home page
-
- Scenario: I visit public project page
- When I visit project "Community" page
- Then I should see project "Community" home page
- And I should see an http link to the repository
-
- Scenario: I visit public project page as user with no ssh-keys
- Given I sign in as a user
- And I have no ssh keys
- When I visit project "Community" page
- Then I should see project "Community" home page
- And I should see an http link to the repository
-
- Scenario: I visit public project page as user with an ssh-key
- Given I sign in as a user
- And I have an ssh key
- When I visit project "Community" page
- Then I should see project "Community" home page
- And I should see an ssh link to the repository
-
- Scenario: I visit an empty public project page
- Given public empty project "Empty Public Project"
- When I visit empty project page
- Then I should see empty public project details
-
- Scenario: I visit public project issues page as a non authorized user
- Given I visit project "Community" page
- Then I should not see command line instructions
- And I visit "Community" issues page
- Then I should see list of issues for "Community" project
-
- Scenario: I visit public project issues page as authorized user
- Given I sign in as a user
- Given I visit project "Community" page
- And I visit "Community" issues page
- Then I should see list of issues for "Community" project
-
- Scenario: I visit internal project issues page as authorized user
- Given I sign in as a user
- Given I visit project "Internal" page
- And I visit "Internal" issues page
- Then I should see list of issues for "Internal" project
-
- Scenario: I visit public project merge requests page as an authorized user
- Given I sign in as a user
- Given I visit project "Community" page
- And I visit "Community" merge requests page
- And project "Community" has "Bug fix" open merge request
- Then I should see list of merge requests for "Community" project
-
- Scenario: I visit public project merge requests page as a non authorized user
- Given I visit project "Community" page
- And I visit "Community" merge requests page
- And project "Community" has "Bug fix" open merge request
- Then I should see list of merge requests for "Community" project
-
- Scenario: I visit internal project merge requests page as an authorized user
- Given I sign in as a user
- Given I visit project "Internal" page
- And I visit "Internal" merge requests page
- And project "Internal" has "Feature implemented" open merge request
- Then I should see list of merge requests for "Internal" project
-
- Scenario: Trending page
- Given archived project "Archive"
- And project "Archive" has comments
- And I sign in as a user
- And project "Community" has comments
- And trending projects are refreshed
- When I visit the explore trending projects
- Then I should see project "Community"
- And I should not see project "Internal"
- And I should not see project "Enterprise"
- And I should not see project "Archive"
-
- Scenario: Most starred page
- Given archived project "Archive"
- And I sign in as a user
- When I visit the explore starred projects
- Then I should see project "Community"
- And I should see project "Internal"
- And I should not see project "Archive"
diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb
deleted file mode 100644
index 962e39dde9a..00000000000
--- a/features/steps/explore/projects.rb
+++ /dev/null
@@ -1,145 +0,0 @@
-class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
- include SharedUser
-
- step 'I should see project "Empty Public Project"' do
- expect(page).to have_content "Empty Public Project"
- end
-
- step 'I should see public project details' do
- expect(page).to have_content '32 branches'
- expect(page).to have_content '16 tags'
- end
-
- step 'I should see project readme' do
- expect(page).to have_content 'README.md'
- end
-
- step 'I should see empty public project details' do
- expect(page).not_to have_content 'Git global setup'
- end
-
- step 'I should see empty public project details with http clone info' do
- project = Project.find_by(name: 'Empty Public Project')
- page.all(:css, '.git-empty .clone').each do |element|
- expect(element.text).to include(project.http_url_to_repo)
- end
- end
-
- step 'I should see empty public project details with ssh clone info' do
- project = Project.find_by(name: 'Empty Public Project')
- page.all(:css, '.git-empty .clone').each do |element|
- expect(element.text).to include(project.url_to_repo)
- end
- end
-
- step 'I should see project "Community" home page' do
- page.within '.breadcrumbs .breadcrumb-item-text' do
- expect(page).to have_content 'Community'
- end
- end
-
- step 'I should see project "Internal" home page' do
- page.within '.breadcrumbs .breadcrumb-item-text' do
- expect(page).to have_content 'Internal'
- end
- end
-
- step 'I should see an http link to the repository' do
- project = Project.find_by(name: 'Community')
- expect(page).to have_field('project_clone', with: project.http_url_to_repo)
- end
-
- step 'I should see an ssh link to the repository' do
- project = Project.find_by(name: 'Community')
- expect(page).to have_field('project_clone', with: project.url_to_repo)
- end
-
- step 'I visit "Community" issues page' do
- create(:issue,
- title: "Bug",
- project: public_project
- )
- create(:issue,
- title: "New feature",
- project: public_project
- )
- visit project_issues_path(public_project)
- end
-
- step 'I should see list of issues for "Community" project' do
- expect(page).to have_content "Bug"
- expect(page).to have_content public_project.name
- expect(page).to have_content "New feature"
- end
-
- step 'I visit "Internal" issues page' do
- create(:issue,
- title: "Internal Bug",
- project: internal_project
- )
- create(:issue,
- title: "New internal feature",
- project: internal_project
- )
- visit project_issues_path(internal_project)
- end
-
- step 'I should see list of issues for "Internal" project' do
- expect(page).to have_content "Internal Bug"
- expect(page).to have_content internal_project.name
- expect(page).to have_content "New internal feature"
- end
-
- step 'I visit "Community" merge requests page' do
- visit project_merge_requests_path(public_project)
- end
-
- step 'project "Community" has "Bug fix" open merge request' do
- create(:merge_request,
- title: "Bug fix for public project",
- source_project: public_project,
- target_project: public_project
- )
- end
-
- step 'I should see list of merge requests for "Community" project' do
- expect(page).to have_content public_project.name
- expect(page).to have_content public_merge_request.source_project.name
- end
-
- step 'I visit "Internal" merge requests page' do
- visit project_merge_requests_path(internal_project)
- end
-
- step 'project "Internal" has "Feature implemented" open merge request' do
- create(:merge_request,
- title: "Feature implemented",
- source_project: internal_project,
- target_project: internal_project
- )
- end
-
- step 'I should see list of merge requests for "Internal" project' do
- expect(page).to have_content internal_project.name
- expect(page).to have_content internal_merge_request.source_project.name
- end
-
- def internal_project
- @internal_project ||= Project.find_by!(name: 'Internal')
- end
-
- def public_project
- @public_project ||= Project.find_by!(name: 'Community')
- end
-
- def internal_merge_request
- @internal_merge_request ||= MergeRequest.find_by!(title: 'Feature implemented')
- end
-
- def public_merge_request
- @public_merge_request ||= MergeRequest.find_by!(title: 'Bug fix for public project')
- end
-end
diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb
index 7e339443b75..f8eb0f01de8 100644
--- a/features/steps/profile/notifications.rb
+++ b/features/steps/profile/notifications.rb
@@ -11,7 +11,7 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps
end
step 'I select Mention setting from dropdown' do
- first(:link, "On mention").trigger('click')
+ first(:link, "On mention").click
end
step 'I should see Notification saved message' do
diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb
index ccaf3237815..c3ae33d2aa9 100644
--- a/features/steps/project/commits/branches.rb
+++ b/features/steps/project/commits/branches.rb
@@ -40,6 +40,7 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step 'I submit new branch form with invalid name' do
fill_in 'branch_name', with: '1.0 stable'
+ page.find("body").click # defocus the branch_name input
select_branch('master')
click_button 'Create branch'
end
@@ -70,17 +71,16 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step "I click branch 'improve/awesome' delete link" do
page.within '.js-branch-improve\/awesome' do
- find('.btn-remove').click
- sleep 0.05
+ accept_alert { find('.btn-remove').click }
end
end
step "I should not see branch 'improve/awesome'" do
- expect(page.all(visible: true)).not_to have_content 'improve/awesome'
+ expect(page).to have_css('.js-branch-improve\\/awesome', visible: :hidden)
end
def select_branch(branch_name)
- click_button 'master'
+ find('.git-revision-dropdown-toggle').click
page.within '#new-branch-form .dropdown-menu' do
click_link branch_name
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 318e054e978..c623a516c47 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -62,7 +62,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I should see additional file lines' do
- page.within @diff.parent do
+ page.within @diff.query_scope do
expect(first('.new_line').text).not_to have_content "..."
end
end
diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb
index d34fa694789..b467af53c98 100644
--- a/features/steps/project/issues/filter_labels.rb
+++ b/features/steps/project/issues/filter_labels.rb
@@ -28,12 +28,6 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
end
end
- step 'I click link "bug"' do
- page.find('.js-label-select', visible: true).click
- sleep 0.5
- execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
- end
-
step 'I click "dropdown close button"' do
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 2c3ef2efd52..3843374678c 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -20,11 +20,13 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I should see that I am subscribed' do
- expect(find('.issuable-subscribe-button span')).to have_content 'Unsubscribe'
+ wait_for_requests
+ expect(find('.js-issuable-subscribe-button span')).to have_content 'Unsubscribe'
end
step 'I should see that I am unsubscribed' do
- expect(find('.issuable-subscribe-button span')).to have_content 'Subscribe'
+ wait_for_requests
+ expect(find('.js-issuable-subscribe-button span')).to have_content 'Subscribe'
end
step 'I click link "Closed"' do
diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb
index dac18c537ac..196e0fff63a 100644
--- a/features/steps/project/issues/labels.rb
+++ b/features/steps/project/issues/labels.rb
@@ -16,7 +16,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I delete all labels' do
page.within '.labels' do
page.all('.remove-row').each do
- first('.remove-row').click
+ accept_confirm { first('.remove-row').click }
end
end
end
diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb
index 16a2d4a6f93..33a24e8913a 100644
--- a/features/steps/project/issues/milestones.rb
+++ b/features/steps/project/issues/milestones.rb
@@ -3,6 +3,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
include SharedProject
include SharedPaths
include SharedMarkdown
+ include CapybaraHelpers
step 'I should see milestone "v2.2"' do
milestone = @project.milestones.find_by(title: "v2.2")
@@ -65,7 +66,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
end
step 'I click link to remove milestone' do
- click_link 'Delete'
+ confirm_modal_if_present { click_link 'Delete' }
end
step 'I should see no milestones' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 621cae5d80d..6e04f09f322 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -46,10 +46,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(page).to have_content new_gitignore_content
end
- step 'I should see its content with new lines preserved at end of file' do
- expect(evaluate_script('ace.edit("editor").getValue()')).to eq "Sample\n\n\n"
- end
-
step 'I click link "Raw"' do
click_link 'Open raw'
end
@@ -70,20 +66,11 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
click_link 'Fork'
end
- step 'I can edit code' do
- set_new_content
- expect(evaluate_script('ace.edit("editor").getValue()')).to eq new_gitignore_content
- end
-
step 'I edit code' do
expect(page).to have_selector('.file-editor')
set_new_content
end
- step 'I edit code with new lines at end of file' do
- execute_script('ace.edit("editor").setValue("Sample\n\n\n")')
- end
-
step 'I fill the new file name' do
fill_in :file_name, with: new_file_name
end
@@ -395,6 +382,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
private
def set_new_content
+ find('#editor')
execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')")
end
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index c872bd6f861..aa32528a7ca 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -215,7 +215,7 @@ module SharedDiffNote
end
step 'I click side-by-side diff button' do
- find('#parallel-diff-btn').trigger('click')
+ find('#parallel-diff-btn').click
end
step 'I see side-by-side diff button' do
@@ -227,12 +227,11 @@ module SharedDiffNote
end
def click_diff_line(code)
- find(".line_holder[id='#{code}'] td:nth-of-type(1)").trigger 'mouseover'
- find(".line_holder[id='#{code}'] button").trigger 'click'
+ find(".line_holder[id='#{code}'] button").click
end
def click_parallel_diff_line(code, line_type)
- find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').trigger 'mouseover'
- find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click'
+ find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').hover
+ find(".line_holder.parallel button[data-line-code='#{code}']").click
end
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index 0cd7b506a95..95f0cd2156e 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -14,7 +14,7 @@ module SharedNote
find('.more-actions').click
find('.more-actions .dropdown-menu li', match: :first)
- find(".js-note-delete").click
+ accept_confirm { find(".js-note-delete").click }
end
end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index be69a96c3ee..bff0d58aaf4 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -222,7 +222,7 @@ module SharedPaths
end
step "I visit my project's commits page for a specific path" do
- visit project_commits_path(@project, root_ref + "/app/models/project.rb", { limit: 5 })
+ visit project_commits_path(@project, root_ref + "/files/ruby/regex.rb", { limit: 5 })
end
step 'I visit my project\'s commits stats page' do
@@ -454,19 +454,6 @@ module SharedPaths
# ----------------------------------------
# Public Projects
# ----------------------------------------
-
- step 'I visit the public projects area' do
- visit explore_projects_path
- end
-
- step 'I visit the explore trending projects' do
- visit trending_explore_projects_path
- end
-
- step 'I visit the explore starred projects' do
- visit starred_explore_projects_path
- end
-
step 'I visit the public groups area' do
visit explore_groups_path
end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 96cc0745e97..5e4edaf99a6 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -112,10 +112,6 @@ module SharedProject
# Visibility of archived project
# ----------------------------------------
- step 'archived project "Archive"' do
- create(:project, :archived, :public, :repository, name: 'Archive')
- end
-
step 'I should not see project "Archive"' do
project = Project.find_by(name: "Archive")
expect(page).not_to have_content project.name_with_namespace
@@ -126,11 +122,6 @@ module SharedProject
expect(page).to have_content project.name_with_namespace
end
- step 'project "Archive" has comments' do
- project = Project.find_by(name: "Archive")
- 2.times { create(:note_on_issue, project: project) }
- end
-
# ----------------------------------------
# Visibility level
# ----------------------------------------
@@ -209,15 +200,6 @@ module SharedProject
create :project_empty_repo, :public, name: "Empty Public Project"
end
- step 'project "Community" has comments' do
- project = Project.find_by(name: "Community")
- 2.times { create(:note_on_issue, project: project) }
- end
-
- step 'trending projects are refreshed' do
- TrendingProject.refresh!
- end
-
step 'project "Shop" has labels: "bug", "feature", "enhancement"' do
project = Project.find_by(name: "Shop")
create(:label, project: project, title: 'bug')
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index f4691647d4b..3c4db8b9601 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -1,22 +1,21 @@
-require 'capybara/poltergeist'
require 'capybara-screenshot/spinach'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
-Capybara.javascript_driver = :poltergeist
-Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(
- app,
- js_errors: true,
- timeout: timeout,
- window_size: [1366, 768],
- url_whitelist: %w[localhost 127.0.0.1],
- url_blacklist: %w[.mp4 .png .gif .avi .bmp .jpg .jpeg],
- phantomjs_options: [
- '--load-images=yes'
- ]
+Capybara.javascript_driver = :chrome
+Capybara.register_driver :chrome do |app|
+ extra_args = []
+ extra_args << 'headless' unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
+
+ capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
+ chromeOptions: {
+ 'args' => %w[no-sandbox disable-gpu --window-size=1240,1400] + extra_args
+ }
)
+
+ Capybara::Selenium::Driver
+ .new(app, browser: :chrome, desired_capabilities: capabilities)
end
Capybara.default_max_wait_time = timeout
@@ -24,6 +23,10 @@ Capybara.ignore_hidden_elements = false
# Keep only the screenshots generated from the last failing test suite
Capybara::Screenshot.prune_strategy = :keep_last_run
+# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326
+Capybara::Screenshot.register_driver(:chrome) do |driver, path|
+ driver.browser.save_screenshot(path)
+end
Spinach.hooks.before_run do
TestEnv.eager_load_driver_server
diff --git a/features/support/capybara_helpers.rb b/features/support/capybara_helpers.rb
new file mode 100644
index 00000000000..647f8d087c3
--- /dev/null
+++ b/features/support/capybara_helpers.rb
@@ -0,0 +1,10 @@
+module CapybaraHelpers
+ def confirm_modal_if_present
+ if Capybara.current_driver == Capybara.javascript_driver
+ accept_confirm { yield }
+ return
+ end
+
+ yield
+ end
+end
diff --git a/fixtures/emojis/aliases.json b/fixtures/emojis/aliases.json
index e2f47db0de2..415dd5a54e0 100644
--- a/fixtures/emojis/aliases.json
+++ b/fixtures/emojis/aliases.json
@@ -339,6 +339,7 @@
"baguette_bread":"french_bread",
"anguished":"frowning",
"white_frowning_face":"frowning2",
+ "rainbow_flag":"gay_pride_flag",
"goal_net":"goal",
"hammer_and_pick":"hammer_pick",
"raised_hand_with_fingers_splayed":"hand_splayed",
@@ -488,6 +489,7 @@
"slightly_smiling_face":"slight_smile",
"sneeze":"sneezing_face",
"speaking_head_in_silhouette":"speaking_head",
+ "left_speech_bubble":"speech_left",
"sleuth_or_spy":"spy",
"sleuth_or_spy_tone1":"spy_tone1",
"sleuth_or_spy_tone2":"spy_tone2",
@@ -537,4 +539,4 @@
"wrestling_tone4":"wrestlers_tone4",
"wrestling_tone5":"wrestlers_tone5",
"zipper_mouth_face":"zipper_mouth"
-}
+} \ No newline at end of file
diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json
index 589cff165f3..3c8f6426f93 100644
--- a/fixtures/emojis/digests.json
+++ b/fixtures/emojis/digests.json
@@ -1478,7 +1478,7 @@
},
"cartwheel_tone4": {
"category": "activity",
- "moji": "🤸ðŸ¾,",
+ "moji": "🤸ðŸ¾",
"description": "person doing cartwheel tone 4",
"unicodeVersion": "9.0",
"digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e"
@@ -5375,6 +5375,13 @@
"unicodeVersion": "6.0",
"digest": "180e66f19d9285e02d0a5e859722c608206826e80323942b9938fc49d44973b1"
},
+ "gay_pride_flag": {
+ "category": "flags",
+ "moji": "ðŸ³ðŸŒˆ",
+ "description": "gay_pride_flag",
+ "unicodeVersion": "6.0",
+ "digest": "924e668c559db61b7f4724a661223081c2fc60d55169f3fe1ad6156934d1d37f"
+ },
"gemini": {
"category": "symbols",
"moji": "♊",
@@ -7578,7 +7585,7 @@
"moji": "🤶",
"description": "mother christmas",
"unicodeVersion": "9.0",
- "digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076"
+ "digest": "357d769371305a8584f46d6087a962d647b6af22fab363a44702f38ab7814091"
},
"mrs_claus_tone1": {
"category": "people",
@@ -10709,6 +10716,13 @@
"unicodeVersion": "6.0",
"digest": "817100d9979456e7d2f253ac22e13b7a2302dc1590566214915b003e403c53ca"
},
+ "speech_left": {
+ "category": "symbols",
+ "moji": "🗨",
+ "description": "left speech bubble",
+ "unicodeVersion": "7.0",
+ "digest": "912797107d574f5665411498b6e349dbdec69846f085b6dc356548c4155e90b0"
+ },
"speedboat": {
"category": "travel",
"moji": "🚤",
diff --git a/fixtures/emojis/generate_aliases.rb b/fixtures/emojis/generate_aliases.rb
deleted file mode 100755
index 8838fb9a3af..00000000000
--- a/fixtures/emojis/generate_aliases.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'json'
-
-aliases = {}
-
-index_file = File.expand_path("./index.json")
-index = JSON.parse(File.read(index_file))
-
-index.each_pair do |key, data|
- data['aliases'].each do |a|
- a.tr!(':', '')
-
- aliases[a] = key
- end
-end
-
-puts JSON.pretty_generate(aliases, indent: ' ', space: '', space_before: '')
diff --git a/fixtures/emojis/index.json b/fixtures/emojis/index.json
index 2a990913b9c..f55571d31fa 100644
--- a/fixtures/emojis/index.json
+++ b/fixtures/emojis/index.json
@@ -4023,7 +4023,7 @@
],
"aliases_ascii": [],
"keywords": [],
- "moji": "🤸ðŸ¾,"
+ "moji": "🤸ðŸ¾"
},
"cartwheel_tone5": {
"unicode": "1F938-1F3FF",
@@ -14475,6 +14475,19 @@
],
"moji": "💎"
},
+ "gay_pride_flag": {
+ "unicode": "1F3F3-1F308",
+ "unicode_alternates": [],
+ "name": "gay_pride_flag",
+ "shortname": ":gay_pride_flag:",
+ "category": "extras",
+ "aliases": [
+ ":rainbow_flag:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "ðŸ³ðŸŒˆ"
+ },
"gemini": {
"unicode": "264A",
"unicode_alternates": [
@@ -16830,7 +16843,6 @@
"0:-)",
"0:)",
"0;^)",
- "O:-)",
"O:)",
"O;-)",
"O=)",
@@ -28506,6 +28518,21 @@
],
"moji": "💬"
},
+ "speech_left": {
+ "unicode": "1F5E8",
+ "unicode_alternates": [
+ "1F5E8-FE0F"
+ ],
+ "name": "left speech bubble",
+ "shortname": ":speech_left:",
+ "category": "symbols",
+ "aliases": [
+ ":left_speech_bubble:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🗨"
+ },
"speedboat": {
"unicode": "1F6A4",
"unicode_alternates": [],
@@ -33477,4 +33504,4 @@
],
"moji": "💤"
}
-}
+} \ No newline at end of file
diff --git a/lib/additional_email_headers_interceptor.rb b/lib/additional_email_headers_interceptor.rb
index 2358fa6bbfd..3cb1694b9f1 100644
--- a/lib/additional_email_headers_interceptor.rb
+++ b/lib/additional_email_headers_interceptor.rb
@@ -1,8 +1,6 @@
class AdditionalEmailHeadersInterceptor
def self.delivering_email(message)
- message.headers(
- 'Auto-Submitted' => 'auto-generated',
- 'X-Auto-Response-Suppress' => 'All'
- )
+ message.header['Auto-Submitted'] ||= 'auto-generated'
+ message.header['X-Auto-Response-Suppress'] ||= 'All'
end
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 79e55a2f4f7..8094597d238 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -4,6 +4,10 @@ module API
LOG_FILENAME = Rails.root.join("log", "api_json.log")
+ NO_SLASH_URL_PART_REGEX = %r{[^/]+}
+ PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze
+ COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze
+
use GrapeLogging::Middleware::RequestLogger,
logger: Logger.new(LOG_FILENAME),
formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
@@ -57,7 +61,10 @@ module API
mount ::API::V3::Variables
end
- before { header['X-Frame-Options'] = 'SAMEORIGIN' }
+ before do
+ header['X-Frame-Options'] = 'SAMEORIGIN'
+ header['X-Content-Type-Options'] = 'nosniff'
+ end
# The locale is set to the current user's locale when `current_user` is loaded
after { Gitlab::I18n.use_default_locale }
@@ -96,9 +103,6 @@ module API
helpers ::API::Helpers
helpers ::API::Helpers::CommonHelpers
- NO_SLASH_URL_PART_REGEX = %r{[^/]+}
- PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze
-
# Keep in alphabetical order
mount ::API::AccessRequests
mount ::API::AwardEmoji
@@ -130,6 +134,7 @@ module API
mount ::API::Namespaces
mount ::API::Notes
mount ::API::NotificationSettings
+ mount ::API::PagesDomains
mount ::API::Pipelines
mount ::API::PipelineSchedules
mount ::API::ProjectHooks
@@ -140,7 +145,6 @@ module API
mount ::API::Runner
mount ::API::Runners
mount ::API::Services
- mount ::API::Session
mount ::API::Settings
mount ::API::SidekiqMetrics
mount ::API::Snippets
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index e79f988f549..9aeebc34525 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -6,9 +6,6 @@ module API
module APIGuard
extend ActiveSupport::Concern
- PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN".freeze
- PRIVATE_TOKEN_PARAM = :private_token
-
included do |base|
# OAuth2 Resource Server Authentication
use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request|
@@ -42,87 +39,34 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
- # Invokes the doorkeeper guard.
- #
- # If token is presented and valid, then it sets @current_user.
- #
- # If the token does not have sufficient scopes to cover the requred scopes,
- # then it raises InsufficientScopeError.
- #
- # If the token is expired, then it raises ExpiredError.
- #
- # If the token is revoked, then it raises RevokedError.
- #
- # If the token is not found (nil), then it returns nil
- #
- # Arguments:
- #
- # scopes: (optional) scopes required for this guard.
- # Defaults to empty array.
- #
- def doorkeeper_guard(scopes: [])
- access_token = find_access_token
- return nil unless access_token
-
- case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
- when AccessTokenValidationService::INSUFFICIENT_SCOPE
- raise InsufficientScopeError.new(scopes)
-
- when AccessTokenValidationService::EXPIRED
- raise ExpiredError
-
- when AccessTokenValidationService::REVOKED
- raise RevokedError
-
- when AccessTokenValidationService::VALID
- User.find(access_token.resource_owner_id)
- end
- end
-
- def find_user_by_private_token(scopes: [])
- token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
-
- return nil unless token_string.present?
+ include Gitlab::Auth::UserAuthFinders
- user =
- find_user_by_authentication_token(token_string) ||
- find_user_by_personal_access_token(token_string, scopes)
+ def find_current_user!
+ user = find_user_from_access_token || find_user_from_warden
+ return unless user
- raise UnauthorizedError unless user
+ forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
user
end
private
- def find_user_by_authentication_token(token_string)
- User.find_by_authentication_token(token_string)
- end
-
- def find_user_by_personal_access_token(token_string, scopes)
- access_token = PersonalAccessToken.active.find_by_token(token_string)
- return unless access_token
-
- if AccessTokenValidationService.new(access_token, request: request).include_any_scope?(scopes)
- User.find(access_token.user_id)
- end
- end
-
- def find_access_token
- return @access_token if defined?(@access_token)
-
- token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
- return @access_token = nil unless token
-
- @access_token = Doorkeeper::AccessToken.by_token(token)
- raise UnauthorizedError unless @access_token
-
- @access_token.revoke_previous_refresh_token!
- @access_token
- end
-
- def doorkeeper_request
- @doorkeeper_request ||= ActionDispatch::Request.new(env)
+ # An array of scopes that were registered (using `allow_access_with_scope`)
+ # for the current endpoint class. It also returns scopes registered on
+ # `API::API`, since these are meant to apply to all API routes.
+ def scopes_registered_for_endpoint
+ @scopes_registered_for_endpoint ||=
+ begin
+ endpoint_classes = [options[:for].presence, ::API::API].compact
+ endpoint_classes.reduce([]) do |memo, endpoint|
+ if endpoint.respond_to?(:allowed_scopes)
+ memo.concat(endpoint.allowed_scopes)
+ else
+ memo
+ end
+ end
+ end
end
end
@@ -130,8 +74,11 @@ module API
private
def install_error_responders(base)
- error_classes = [MissingTokenError, TokenNotFoundError,
- ExpiredError, RevokedError, InsufficientScopeError]
+ error_classes = [Gitlab::Auth::MissingTokenError,
+ Gitlab::Auth::TokenNotFoundError,
+ Gitlab::Auth::ExpiredError,
+ Gitlab::Auth::RevokedError,
+ Gitlab::Auth::InsufficientScopeError]
base.__send__(:rescue_from, *error_classes, oauth2_bearer_token_error_handler) # rubocop:disable GitlabSecurity/PublicSend
end
@@ -140,25 +87,25 @@ module API
proc do |e|
response =
case e
- when MissingTokenError
+ when Gitlab::Auth::MissingTokenError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new
- when TokenNotFoundError
+ when Gitlab::Auth::TokenNotFoundError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Bad Access Token.")
- when ExpiredError
+ when Gitlab::Auth::ExpiredError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Token is expired. You can either do re-authorization or token refresh.")
- when RevokedError
+ when Gitlab::Auth::RevokedError
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Token was revoked. You have to re-authorize from the user.")
- when InsufficientScopeError
+ when Gitlab::Auth::InsufficientScopeError
# FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2)
# does not include WWW-Authenticate header, which breaks the standard.
Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(
@@ -171,22 +118,5 @@ module API
end
end
end
-
- #
- # Exceptions
- #
-
- MissingTokenError = Class.new(StandardError)
- TokenNotFoundError = Class.new(StandardError)
- ExpiredError = Class.new(StandardError)
- RevokedError = Class.new(StandardError)
- UnauthorizedError = Class.new(StandardError)
-
- class InsufficientScopeError < StandardError
- attr_reader :scopes
- def initialize(scopes)
- @scopes = scopes
- end
- end
end
end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 61a2d688282..0791a110c39 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -8,6 +8,16 @@ module API
before { authorize! :download_code, user_project }
+ helpers do
+ def find_branch!(branch_name)
+ begin
+ user_project.repository.find_branch(branch_name) || not_found!('Branch')
+ rescue Gitlab::Git::CommandError
+ render_api_error!('The branch refname is invalid', 400)
+ end
+ end
+ end
+
params do
requires :id, type: String, desc: 'The ID of a project'
end
@@ -19,12 +29,11 @@ module API
use :pagination
end
get ':id/repository/branches' do
- branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
+ repository = user_project.repository
+ branches = ::Kaminari.paginate_array(repository.branches.sort_by(&:name))
+ merged_branch_names = repository.merged_branch_names(branches.map(&:name))
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- present paginate(branches), with: Entities::Branch, project: user_project
- end
+ present paginate(branches), with: Entities::Branch, project: user_project, merged_branch_names: merged_branch_names
end
resource ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
@@ -38,8 +47,7 @@ module API
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
+ branch = find_branch!(params[:branch])
present branch, with: Entities::Branch, project: user_project
end
@@ -60,8 +68,7 @@ module API
put ':id/repository/branches/:branch/protect', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
authorize_admin_project
- branch = user_project.repository.find_branch(params[:branch])
- not_found!('Branch') unless branch
+ branch = find_branch!(params[:branch])
protected_branch = user_project.protected_branches.find_by(name: branch.name)
@@ -74,9 +81,9 @@ module API
service_args = [user_project, current_user, protected_branch_params]
protected_branch = if protected_branch
- ::ProtectedBranches::ApiUpdateService.new(*service_args).execute(protected_branch)
+ ::ProtectedBranches::LegacyApiUpdateService.new(*service_args).execute(protected_branch)
else
- ::ProtectedBranches::ApiCreateService.new(*service_args).execute
+ ::ProtectedBranches::LegacyApiCreateService.new(*service_args).execute
end
if protected_branch.valid?
@@ -96,8 +103,7 @@ module API
put ':id/repository/branches/:branch/unprotect', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
authorize_admin_project
- branch = user_project.repository.find_branch(params[:branch])
- not_found!("Branch") unless branch
+ branch = find_branch!(params[:branch])
protected_branch = user_project.protected_branches.find_by(name: branch.name)
protected_branch&.destroy
@@ -133,8 +139,7 @@ module API
delete ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
authorize_push_project
- branch = user_project.repository.find_branch(params[:branch])
- not_found!('Branch') unless branch
+ branch = find_branch!(params[:branch])
commit = user_project.repository.commit(branch.dereferenced_target)
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 4af37a2ad1d..38e05074353 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -4,8 +4,6 @@ module API
class Commits < Grape::API
include PaginationParams
- COMMIT_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: API::NO_SLASH_URL_PART_REGEX)
-
before { authorize! :download_code, user_project }
params do
@@ -85,7 +83,7 @@ module API
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
end
- get ':id/repository/commits/:sha', requirements: COMMIT_ENDPOINT_REQUIREMENTS do
+ get ':id/repository/commits/:sha', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
@@ -99,7 +97,7 @@ module API
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
end
- get ':id/repository/commits/:sha/diff', requirements: COMMIT_ENDPOINT_REQUIREMENTS do
+ get ':id/repository/commits/:sha/diff', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
@@ -115,11 +113,11 @@ module API
use :pagination
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
end
- get ':id/repository/commits/:sha/comments', requirements: COMMIT_ENDPOINT_REQUIREMENTS do
+ get ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
- notes = user_project.notes.where(commit_id: commit.id).order(:created_at)
+ notes = commit.notes.order(:created_at)
present paginate(notes), with: Entities::CommitNote
end
@@ -132,7 +130,7 @@ module API
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag to be cherry picked'
requires :branch, type: String, desc: 'The name of the branch'
end
- post ':id/repository/commits/:sha/cherry_pick', requirements: COMMIT_ENDPOINT_REQUIREMENTS do
+ post ':id/repository/commits/:sha/cherry_pick', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
authorize! :push_code, user_project
commit = user_project.commit(params[:sha])
@@ -169,7 +167,7 @@ module API
requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line'
end
end
- post ':id/repository/commits/:sha/comments', requirements: COMMIT_ENDPOINT_REQUIREMENTS do
+ post ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
@@ -182,11 +180,13 @@ module API
if params[:path]
commit.raw_diffs(limits: false).each do |diff|
next unless diff.new_path == params[:path]
+
lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
lines.each do |line|
next unless line.new_pos == params[:line] && line.type == params[:line_type]
- break opts[:line_code] = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
+
+ break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos)
end
break if opts[:line_code]
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 5f0bad14839..ce332fe85d2 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -57,10 +57,6 @@ module API
expose :admin?, as: :is_admin
end
- class UserWithPrivateDetails < UserWithAdmin
- expose :private_token
- end
-
class Email < Grape::Entity
expose :id, :email
end
@@ -84,16 +80,21 @@ module API
expose :group_access, as: :group_access_level
end
- class BasicProjectDetails < Grape::Entity
- expose :id, :description, :default_branch, :tag_list
- expose :ssh_url_to_repo, :http_url_to_repo, :web_url
+ class ProjectIdentity < Grape::Entity
+ expose :id, :description
expose :name, :name_with_namespace
expose :path, :path_with_namespace
+ expose :created_at
+ end
+
+ class BasicProjectDetails < ProjectIdentity
+ expose :default_branch, :tag_list
+ expose :ssh_url_to_repo, :http_url_to_repo, :web_url
expose :avatar_url do |project, options|
project.avatar_url(only_path: false)
end
expose :star_count, :forks_count
- expose :created_at, :last_activity_at
+ expose :last_activity_at
end
class Project < BasicProjectDetails
@@ -246,9 +247,10 @@ module API
end
expose :merged do |repo_branch, options|
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- options[:project].repository.merged_to_root_ref?(repo_branch.name)
+ if options[:merged_branch_names]
+ options[:merged_branch_names].include?(repo_branch.name)
+ else
+ options[:project].repository.merged_to_root_ref?(repo_branch)
end
end
@@ -482,6 +484,10 @@ module API
expose :subscribed do |merge_request, options|
merge_request.subscribed?(options[:current_user], options[:project])
end
+
+ expose :changes_count do |merge_request, _options|
+ merge_request.merge_request_diff.real_size
+ end
end
class MergeRequestChanges < MergeRequest
@@ -766,7 +772,10 @@ module API
expose(:default_project_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_project_visibility) }
expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) }
expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) }
- expose :password_authentication_enabled, as: :signin_enabled
+
+ # support legacy names, can be removed in v5
+ expose :password_authentication_enabled_for_web, as: :password_authentication_enabled
+ expose :password_authentication_enabled_for_web, as: :signin_enabled
end
class Release < Grape::Entity
@@ -823,16 +832,24 @@ module API
expose :id, :sha, :ref, :status
end
- class Job < Grape::Entity
+ class JobBasic < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
+ expose :duration
expose :user, with: User
- expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :commit, with: Commit
- expose :runner, with: Runner
expose :pipeline, with: PipelineBasic
end
+ class Job < JobBasic
+ expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
+ expose :runner, with: Runner
+ end
+
+ class JobBasicWithProject < JobBasic
+ expose :project, with: ProjectIdentity
+ end
+
class Trigger < Grape::Entity
expose :id
expose :token, :description
@@ -1043,5 +1060,38 @@ module API
expose :key
expose :value
end
+
+ class PagesDomainCertificateExpiration < Grape::Entity
+ expose :expired?, as: :expired
+ expose :expiration
+ end
+
+ class PagesDomainCertificate < Grape::Entity
+ expose :subject
+ expose :expired?, as: :expired
+ expose :certificate
+ expose :certificate_text
+ end
+
+ class PagesDomainBasic < Grape::Entity
+ expose :domain
+ expose :url
+ expose :certificate,
+ as: :certificate_expiration,
+ if: ->(pages_domain, _) { pages_domain.certificate? },
+ using: PagesDomainCertificateExpiration do |pages_domain|
+ pages_domain
+ end
+ end
+
+ class PagesDomain < Grape::Entity
+ expose :domain
+ expose :url
+ expose :certificate,
+ if: ->(pages_domain, _) { pages_domain.certificate? },
+ using: PagesDomainCertificate do |pages_domain|
+ pages_domain
+ end
+ end
end
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index e817dcbbc4b..bcf2e6dae1d 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -25,22 +25,7 @@ module API
optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
end
- def present_groups(groups, options = {})
- options = options.reverse_merge(
- with: Entities::Group,
- current_user: current_user
- )
-
- groups = groups.with_statistics if options[:statistics]
- present paginate(groups), options
- end
- end
-
- resource :groups do
- desc 'Get a groups list' do
- success Entities::Group
- end
- params do
+ params :group_list_params do
use :statistics_params
optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
@@ -50,14 +35,47 @@ module API
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
use :pagination
end
- get do
- find_params = { all_available: params[:all_available], owned: params[:owned] }
+
+ def find_groups(params)
+ find_params = {
+ all_available: params[:all_available],
+ custom_attributes: params[:custom_attributes],
+ owned: params[:owned]
+ }
+ find_params[:parent] = find_group!(params[:id]) if params[:id]
+
groups = GroupsFinder.new(current_user, find_params).execute
groups = groups.search(params[:search]) if params[:search].present?
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
groups = groups.reorder(params[:order_by] => params[:sort])
- present_groups groups, statistics: params[:statistics] && current_user.admin?
+ groups
+ end
+
+ def present_groups(params, groups)
+ options = {
+ with: Entities::Group,
+ current_user: current_user,
+ statistics: params[:statistics] && current_user.admin?
+ }
+
+ groups = groups.with_statistics if options[:statistics]
+ present paginate(groups), options
+ end
+ end
+
+ resource :groups do
+ include CustomAttributesEndpoints
+
+ desc 'Get a groups list' do
+ success Entities::Group
+ end
+ params do
+ use :group_list_params
+ end
+ get do
+ groups = find_groups(params)
+ present_groups params, groups
end
desc 'Create a group. Available only for users who can create groups.' do
@@ -159,6 +177,17 @@ module API
present paginate(projects), with: entity, current_user: current_user
end
+ desc 'Get a list of subgroups in this group.' do
+ success Entities::Group
+ end
+ params do
+ use :group_list_params
+ end
+ get ":id/subgroups" do
+ groups = find_groups(params)
+ present_groups params, groups
+ end
+
desc 'Transfer a project to the group namespace. Available only for admin.' do
success Entities::GroupDetail
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index a87297a604c..686bf7a3c2b 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -3,8 +3,6 @@ module API
include Gitlab::Utils
include Helpers::Pagination
- UnauthorizedError = Class.new(StandardError)
-
SUDO_HEADER = "HTTP_SUDO".freeze
SUDO_PARAM = :sudo
@@ -43,6 +41,8 @@ module API
sudo!
+ validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo?
+
@current_user
end
@@ -50,6 +50,10 @@ module API
initial_current_user != current_user
end
+ def user_namespace
+ @user_namespace ||= find_namespace!(params[:id])
+ end
+
def user_group
@group ||= find_group!(params[:id])
end
@@ -112,6 +116,24 @@ module API
end
end
+ def find_namespace(id)
+ if id.to_s =~ /^\d+$/
+ Namespace.find_by(id: id)
+ else
+ Namespace.find_by_full_path(id)
+ end
+ end
+
+ def find_namespace!(id)
+ namespace = find_namespace(id)
+
+ if can?(current_user, :read_namespace, namespace)
+ namespace
+ else
+ not_found!('Namespace')
+ end
+ end
+
def find_project_label(id)
label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
label || not_found!('Label')
@@ -155,6 +177,11 @@ module API
end
end
+ def authenticated_with_full_private_access!
+ authenticate!
+ forbidden! unless current_user.full_private_access?
+ end
+
def authenticated_as_admin!
authenticate!
forbidden! unless current_user.admin?
@@ -186,6 +213,14 @@ module API
end
end
+ def require_pages_enabled!
+ not_found! unless user_project.pages_available?
+ end
+
+ def require_pages_config_enabled!
+ not_found! unless Gitlab.config.pages.enabled
+ end
+
def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
@@ -324,6 +359,7 @@ module API
finder_params[:archived] = params[:archived]
finder_params[:search] = params[:search] if params[:search]
finder_params[:user] = params.delete(:user) if params[:user]
+ finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
finder_params
end
@@ -379,67 +415,35 @@ module API
private
- def private_token
- params[APIGuard::PRIVATE_TOKEN_PARAM] || env[APIGuard::PRIVATE_TOKEN_HEADER]
- end
-
- def warden
- env['warden']
- end
-
- # Check if the request is GET/HEAD, or if CSRF token is valid.
- def verified_request?
- Gitlab::RequestForgeryProtection.verified?(env)
- end
-
- # Check the Rails session for valid authentication details
- def find_user_from_warden
- warden.try(:authenticate) if verified_request?
- end
-
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)
begin
- @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user }
- rescue APIGuard::UnauthorizedError, UnauthorizedError
+ @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user! }
+ rescue Gitlab::Auth::UnauthorizedError
unauthorized!
end
end
- def find_current_user
- user =
- find_user_by_private_token(scopes: scopes_registered_for_endpoint) ||
- doorkeeper_guard(scopes: scopes_registered_for_endpoint) ||
- find_user_from_warden
-
- return nil unless user
-
- raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
-
- user
- end
-
def sudo!
return unless sudo_identifier
- return unless initial_current_user
+
+ unauthorized! unless initial_current_user
unless initial_current_user.admin?
forbidden!('Must be admin to use sudo')
end
- # Only private tokens should be used for the SUDO feature
- unless private_token == initial_current_user.private_token
- forbidden!('Private token must be specified in order to use sudo')
+ unless access_token
+ forbidden!('Must be authenticated using an OAuth or Personal Access Token to use sudo')
end
+ validate_access_token!(scopes: [:sudo])
+
sudoed_user = find_user(sudo_identifier)
+ not_found!("User with ID or username '#{sudo_identifier}'") unless sudoed_user
- if sudoed_user
- @current_user = sudoed_user
- else
- not_found!("No user id or username for: #{sudo_identifier}")
- end
+ @current_user = sudoed_user
end
def sudo_identifier
@@ -479,22 +483,5 @@ module API
exception.status == 500
end
-
- # An array of scopes that were registered (using `allow_access_with_scope`)
- # for the current endpoint class. It also returns scopes registered on
- # `API::API`, since these are meant to apply to all API routes.
- def scopes_registered_for_endpoint
- @scopes_registered_for_endpoint ||=
- begin
- endpoint_classes = [options[:for].presence, ::API::API].compact
- endpoint_classes.reduce([]) do |memo, endpoint|
- if endpoint.respond_to?(:allowed_scopes)
- memo.concat(endpoint.allowed_scopes)
- else
- memo
- end
- end
- end
- end
end
end
diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb
index 0a8f3073a50..dd4f6c41131 100644
--- a/lib/api/helpers/custom_validators.rb
+++ b/lib/api/helpers/custom_validators.rb
@@ -4,6 +4,7 @@ module API
class Absence < Grape::Validations::Base
def validate_param!(attr_name, params)
return if params.respond_to?(:key?) && !params.key?(attr_name)
+
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence)
end
end
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 4c0db4d42b1..d6dea4c30e3 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -2,8 +2,8 @@ module API
module Helpers
module InternalHelpers
SSH_GITALY_FEATURES = {
- 'git-receive-pack' => :ssh_receive_pack,
- 'git-upload-pack' => :ssh_upload_pack
+ 'git-receive-pack' => [:ssh_receive_pack, Gitlab::GitalyClient::MigrationStatus::OPT_IN],
+ 'git-upload-pack' => [:ssh_upload_pack, Gitlab::GitalyClient::MigrationStatus::OPT_OUT]
}.freeze
def wiki?
@@ -36,6 +36,18 @@ module API
{}
end
+ def fix_git_env_repository_paths(env, repository_path)
+ if obj_dir_relative = env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence
+ env['GIT_OBJECT_DIRECTORY'] = File.join(repository_path, obj_dir_relative)
+ end
+
+ if alt_obj_dirs_relative = env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE'].presence
+ env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = alt_obj_dirs_relative.map { |dir| File.join(repository_path, dir) }
+ end
+
+ env
+ end
+
def log_user_activity(actor)
commands = Gitlab::GitAccess::DOWNLOAD_COMMANDS
@@ -90,8 +102,8 @@ module API
# Return the Gitaly Address if it is enabled
def gitaly_payload(action)
- feature = SSH_GITALY_FEATURES[action]
- return unless feature && Gitlab::GitalyClient.feature_enabled?(feature)
+ feature, status = SSH_GITALY_FEATURES[action]
+ return unless feature && Gitlab::GitalyClient.feature_enabled?(feature, status: status)
{
repository: repository.gitaly_repository,
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index 282af32ca94..2cae53dba53 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -14,6 +14,7 @@ module API
def get_runner_version_from_params
return unless params['info'].present?
+
attributes_for_keys(%w(name version revision platform architecture), params['info'])
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 6e78ac2c903..451121a4cea 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -19,7 +19,9 @@ module API
status 200
# Stores some Git-specific env thread-safely
- Gitlab::Git::Env.set(parse_env)
+ env = parse_env
+ env = fix_git_env_repository_paths(env, repository_path) if project
+ Gitlab::Git::Env.set(env)
actor =
if params[:key_id]
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 0df41dcc903..e60e00d7956 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -68,7 +68,7 @@ module API
desc: 'Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`'
end
get do
- issues = find_issues
+ issues = paginate(find_issues)
options = {
with: Entities::IssueBasic,
@@ -76,7 +76,7 @@ module API
issuable_metadata: issuable_meta_data(issues, 'Issue')
}
- present paginate(issues), options
+ present issues, options
end
end
@@ -95,7 +95,7 @@ module API
get ":id/issues" do
group = find_group!(params[:id])
- issues = find_issues(group_id: group.id)
+ issues = paginate(find_issues(group_id: group.id))
options = {
with: Entities::IssueBasic,
@@ -103,7 +103,7 @@ module API
issuable_metadata: issuable_meta_data(issues, 'Issue')
}
- present paginate(issues), options
+ present issues, options
end
end
@@ -124,7 +124,7 @@ module API
get ":id/issues" do
project = find_project!(params[:id])
- issues = find_issues(project_id: project.id)
+ issues = paginate(find_issues(project_id: project.id))
options = {
with: Entities::IssueBasic,
@@ -133,7 +133,7 @@ module API
issuable_metadata: issuable_meta_data(issues, 'Issue')
}
- present paginate(issues), options
+ present issues, options
end
desc 'Get a single project issue' do
@@ -255,7 +255,9 @@ module API
authorize!(:destroy_issue, issue)
- destroy_conditionally!(issue)
+ destroy_conditionally!(issue) do |issue|
+ Issuable::DestroyService.new(user_project, current_user).execute(issue)
+ end
end
desc 'List merge requests closing issue' do
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 3c1c412ba42..a116ab3c9bd 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -136,7 +136,7 @@ module API
authorize_update_builds!
build = find_build!(params[:job_id])
- authorize!(:update_build, build)
+ authorize!(:erase_build, build)
return forbidden!('Job is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index be843ec8251..d34886fca2e 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, :timelogs)
+ .preload(:notes, :author, :assignee, :milestone, :latest_merge_request_diff, :labels, :timelogs)
end
params :merge_requests_params do
@@ -167,7 +167,9 @@ module API
authorize!(:destroy_merge_request, merge_request)
- destroy_conditionally!(merge_request)
+ destroy_conditionally!(merge_request) do |merge_request|
+ Issuable::DestroyService.new(user_project, current_user).execute(merge_request)
+ end
end
params do
@@ -295,7 +297,7 @@ module API
unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
- ::MergeRequest::MergeWhenPipelineSucceedsService
+ ::MergeRequests::MergeWhenPipelineSucceedsService
.new(merge_request.target_project, current_user)
.cancel(merge_request)
end
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index f1eaff6b0eb..32b77aedba8 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -19,6 +19,16 @@ module API
present paginate(namespaces), with: Entities::Namespace, current_user: current_user
end
+
+ desc 'Get a namespace by ID' do
+ success Entities::Namespace
+ end
+ params do
+ requires :id, type: String, desc: "Namespace's ID or path"
+ end
+ get ':id' do
+ present user_namespace, with: Entities::Namespace, current_user: current_user
+ end
end
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 0b9ab4eeb05..3588dc85c9e 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -18,6 +18,10 @@ module API
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
+ desc: 'Return notes ordered by `created_at` or `updated_at` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return notes sorted in `asc` or `desc` order.'
use :pagination
end
get ":id/#{noteables_str}/:noteable_id/notes" do
@@ -29,11 +33,12 @@ module API
# at the DB query level (which we cannot in that case), the current
# page can have less elements than :per_page even if
# there's more than one page.
+ raw_notes = noteable.notes.with_metadata.reorder(params[:order_by] => params[:sort])
notes =
# paginate() only works with a relation. This could lead to a
# mismatch between the pagination headers info and the actual notes
# array returned, but this is really a edge-case.
- paginate(noteable.notes)
+ paginate(raw_notes)
.reject { |n| n.cross_reference_not_visible_for?(current_user) }
present notes, with: Entities::Note
else
@@ -50,7 +55,7 @@ module API
end
get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
noteable = find_project_noteable(noteables_str, params[:noteable_id])
- note = noteable.notes.find(params[:note_id])
+ note = noteable.notes.with_metadata.find(params[:note_id])
can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
if can_read_note
diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb
new file mode 100644
index 00000000000..d7b613a717e
--- /dev/null
+++ b/lib/api/pages_domains.rb
@@ -0,0 +1,137 @@
+module API
+ class PagesDomains < Grape::API
+ include PaginationParams
+
+ before do
+ authenticate!
+ end
+
+ after_validation do
+ normalize_params_file_to_string
+ end
+
+ helpers do
+ def find_pages_domain!
+ user_project.pages_domains.find_by(domain: params[:domain]) || not_found!('PagesDomain')
+ end
+
+ def pages_domain
+ @pages_domain ||= find_pages_domain!
+ end
+
+ def normalize_params_file_to_string
+ params.each do |k, v|
+ if v.is_a?(Hash) && v.key?(:tempfile)
+ params[k] = v[:tempfile].to_a.join('')
+ end
+ end
+ end
+ end
+
+ resource :pages do
+ before do
+ require_pages_config_enabled!
+ authenticated_with_full_private_access!
+ end
+
+ desc "Get all pages domains" do
+ success Entities::PagesDomainBasic
+ end
+ params do
+ use :pagination
+ end
+ get "domains" do
+ present paginate(PagesDomain.all), with: Entities::PagesDomainBasic
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ before do
+ require_pages_enabled!
+ end
+
+ desc 'Get all pages domains' do
+ success Entities::PagesDomain
+ end
+ params do
+ use :pagination
+ end
+ get ":id/pages/domains" do
+ authorize! :read_pages, user_project
+
+ present paginate(user_project.pages_domains.order(:domain)), with: Entities::PagesDomain
+ end
+
+ desc 'Get a single pages domain' do
+ success Entities::PagesDomain
+ end
+ params do
+ requires :domain, type: String, desc: 'The domain'
+ end
+ get ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do
+ authorize! :read_pages, user_project
+
+ present pages_domain, with: Entities::PagesDomain
+ end
+
+ desc 'Create a new pages domain' do
+ success Entities::PagesDomain
+ end
+ params do
+ requires :domain, type: String, desc: 'The domain'
+ optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate'
+ optional :key, allow_blank: false, types: [File, String], desc: 'The key'
+ all_or_none_of :certificate, :key
+ end
+ post ":id/pages/domains" do
+ authorize! :update_pages, user_project
+
+ pages_domain_params = declared(params, include_parent_namespaces: false)
+ pages_domain = user_project.pages_domains.create(pages_domain_params)
+
+ if pages_domain.persisted?
+ present pages_domain, with: Entities::PagesDomain
+ else
+ render_validation_error!(pages_domain)
+ end
+ end
+
+ desc 'Updates a pages domain'
+ params do
+ requires :domain, type: String, desc: 'The domain'
+ optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate'
+ optional :key, allow_blank: false, types: [File, String], desc: 'The key'
+ end
+ put ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do
+ authorize! :update_pages, user_project
+
+ pages_domain_params = declared(params, include_parent_namespaces: false)
+
+ # Remove empty private key if certificate is not empty.
+ if pages_domain_params[:certificate] && !pages_domain_params[:key]
+ pages_domain_params.delete(:key)
+ end
+
+ if pages_domain.update(pages_domain_params)
+ present pages_domain, with: Entities::PagesDomain
+ else
+ render_validation_error!(pages_domain)
+ end
+ end
+
+ desc 'Delete a pages domain'
+ params do
+ requires :domain, type: String, desc: 'The domain'
+ end
+ delete ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do
+ authorize! :update_pages, user_project
+
+ status 204
+ pages_domain.destroy
+ end
+ end
+ end
+end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index aab7a6c3f93..4cd7e714aa2 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -119,6 +119,8 @@ module API
end
resource :projects do
+ include CustomAttributesEndpoints
+
desc 'Get a list of visible projects for authenticated user' do
success Entities::BasicProjectDetails
end
diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb
index 15fcb9e8e27..b5021e8a712 100644
--- a/lib/api/protected_branches.rb
+++ b/lib/api/protected_branches.rb
@@ -40,10 +40,10 @@ module API
params do
requires :name, type: String, desc: 'The name of the protected branch'
optional :push_access_level, type: Integer, default: Gitlab::Access::MASTER,
- values: ProtectedBranchAccess::ALLOWED_ACCESS_LEVELS,
+ values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS,
desc: 'Access levels allowed to push (defaults: `40`, master access level)'
optional :merge_access_level, type: Integer, default: Gitlab::Access::MASTER,
- values: ProtectedBranchAccess::ALLOWED_ACCESS_LEVELS,
+ values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS,
desc: 'Access levels allowed to merge (defaults: `40`, master access level)'
end
post ':id/protected_branches' do
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index ceee3226732..7887b886c03 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -57,7 +57,7 @@ module API
desc 'Get raw blob contents from the repository'
params do
- requires :sha, type: String, desc: 'The commit, branch name, or tag name'
+ requires :sha, type: String, desc: 'The commit hash'
end
get ':id/repository/blobs/:sha/raw' do
assign_blob_vars!
@@ -67,7 +67,7 @@ module API
desc 'Get a blob from the repository'
params do
- requires :sha, type: String, desc: 'The commit, branch name, or tag name'
+ requires :sha, type: String, desc: 'The commit hash'
end
get ':id/repository/blobs/:sha' do
assign_blob_vars!
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index d3559ef71be..996457c5dfe 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -84,6 +84,23 @@ module API
destroy_conditionally!(runner)
end
+
+ desc 'List jobs running on a runner' do
+ success Entities::JobBasicWithProject
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the runner'
+ optional :status, type: String, desc: 'Status of the job', values: Ci::Build::AVAILABLE_STATUSES
+ use :pagination
+ end
+ get ':id/jobs' do
+ runner = get_runner(params[:id])
+ authenticate_list_runners_jobs!(runner)
+
+ jobs = RunnerJobsFinder.new(runner, params).execute
+
+ present paginate(jobs), with: Entities::JobBasicWithProject
+ end
end
params do
@@ -165,17 +182,20 @@ module API
def authenticate_show_runner!(runner)
return if runner.is_shared || current_user.admin?
+
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
def authenticate_update_runner!(runner)
return if current_user.admin?
+
forbidden!("Runner is shared") if runner.is_shared?
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
def authenticate_delete_runner!(runner)
return if current_user.admin?
+
forbidden!("Runner is shared") if runner.is_shared?
forbidden!("Runner associated with more than one project") if runner.projects.count > 1
forbidden!("No access granted") unless user_can_access_runner?(runner)
@@ -185,6 +205,13 @@ module API
forbidden!("Runner is shared") if runner.is_shared?
forbidden!("Runner is locked") if runner.locked?
return if current_user.admin?
+
+ forbidden!("No access granted") unless user_can_access_runner?(runner)
+ end
+
+ def authenticate_list_runners_jobs!(runner)
+ return if current_user.admin?
+
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 2cbd0517dc3..bbcc851d07a 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -313,13 +313,13 @@ module API
desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com'
},
{
- required: false,
+ required: true,
name: :username,
type: String,
desc: 'The username of the user created to be used with GitLab/JIRA'
},
{
- required: false,
+ required: true,
name: :password,
type: String,
desc: 'The password of the user created to be used with GitLab/JIRA'
@@ -374,6 +374,26 @@ module API
desc: 'The Slack token'
}
],
+ 'packagist' => [
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'The username'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Packagist API token'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'The server'
+ }
+ ],
'pipelines-email' => [
{
required: true,
@@ -502,6 +522,12 @@ module API
name: :webhook,
type: String,
desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
+ },
+ {
+ required: false,
+ name: :username,
+ type: String,
+ desc: 'The username to use to post the message'
}
],
'teamcity' => [
@@ -551,6 +577,7 @@ module API
KubernetesService,
MattermostSlashCommandsService,
SlackSlashCommandsService,
+ PackagistService,
PipelinesEmailService,
PivotaltrackerService,
PrometheusService,
diff --git a/lib/api/session.rb b/lib/api/session.rb
deleted file mode 100644
index 016415c3023..00000000000
--- a/lib/api/session.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-module API
- class Session < Grape::API
- desc 'Login to get token' do
- success Entities::UserWithPrivateDetails
- end
- params do
- optional :login, type: String, desc: 'The username'
- optional :email, type: String, desc: 'The email of the user'
- requires :password, type: String, desc: 'The password of the user'
- at_least_one_of :login, :email
- end
- post "/session" do
- user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password])
-
- return unauthorized! unless user
- return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled?
- present user, with: Entities::UserWithPrivateDetails
- end
- end
-end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 851b226e9e5..cee4d309816 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -44,9 +44,11 @@ module API
requires :domain_blacklist, type: String, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
end
optional :after_sign_up_text, type: String, desc: 'Text shown after sign up'
- optional :password_authentication_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled'
- optional :signin_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled'
- mutually_exclusive :password_authentication_enabled, :signin_enabled
+ optional :password_authentication_enabled_for_web, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface'
+ optional :password_authentication_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface' # support legacy names, can be removed in v5
+ optional :signin_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface' # support legacy names, can be removed in v5
+ mutually_exclusive :password_authentication_enabled_for_web, :password_authentication_enabled, :signin_enabled
+ optional :password_authentication_enabled_for_git, type: Boolean, desc: 'Flag indicating if password authentication is enabled for Git over HTTP(S)'
optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to setup Two-factor authentication'
given require_two_factor_authentication: ->(val) { val } do
requires :two_factor_grace_period, type: Integer, desc: 'Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication'
@@ -121,6 +123,9 @@ module API
end
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.'
+ optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
+ optional :gitaly_timeout_medium, type: Integer, desc: 'Medium Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
+ optional :gitaly_timeout_fast, type: Integer, desc: 'Gitaly fast operation timeout, in seconds. Set to 0 to disable timeouts.'
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
optional :"#{type}_key_restriction",
@@ -135,8 +140,11 @@ module API
put "application/settings" do
attrs = declared_params(include_missing: false)
+ # support legacy names, can be removed in v5
if attrs.has_key?(:signin_enabled)
- attrs[:password_authentication_enabled] = attrs.delete(:signin_enabled)
+ attrs[:password_authentication_enabled_for_web] = attrs.delete(:signin_enabled)
+ elsif attrs.has_key?(:password_authentication_enabled)
+ attrs[:password_authentication_enabled_for_web] = attrs.delete(:password_authentication_enabled)
end
if current_settings.update_attributes(attrs)
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index 00eb7c60f16..c736cc32021 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -95,6 +95,7 @@ module API
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
+
authorize! :update_personal_snippet, snippet
attrs = declared_params(include_missing: false).merge(request: request, api: true)
diff --git a/lib/api/users.rb b/lib/api/users.rb
index b6f97a1eac2..0cd89b1bcf8 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -31,7 +31,6 @@ module API
optional :location, type: String, desc: 'The location of the user'
optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
- optional :skip_confirmation, type: Boolean, default: false, desc: 'Flag indicating the account is confirmed'
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
optional :avatar, type: File, desc: 'Avatar image for user'
all_or_none_of :extern_uid, :provider
@@ -101,6 +100,7 @@ module API
requires :email, type: String, desc: 'The email of the user'
optional :password, type: String, desc: 'The password of the new user'
optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token'
+ optional :skip_confirmation, type: Boolean, desc: 'Flag indicating the account is confirmed'
at_least_one_of :password, :reset_password
requires :name, type: String, desc: 'The name of the user'
requires :username, type: String, desc: 'The username of the user'
@@ -134,6 +134,7 @@ module API
requires :id, type: Integer, desc: 'The ID of the user'
optional :email, type: String, desc: 'The email of the user'
optional :password, type: String, desc: 'The password of the new user'
+ optional :skip_reconfirmation, type: Boolean, desc: 'Flag indicating the account skips the confirmation by email'
optional :name, type: String, desc: 'The name of the user'
optional :username, type: String, desc: 'The username of the user'
use :optional_attributes
@@ -507,9 +508,7 @@ module API
end
get do
entity =
- if sudo?
- Entities::UserWithPrivateDetails
- elsif current_user.admin?
+ if current_user.admin?
Entities::UserWithAdmin
else
Entities::UserPublic
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
index 69cd12de72c..b201bf77667 100644
--- a/lib/api/v3/branches.rb
+++ b/lib/api/v3/branches.rb
@@ -14,9 +14,11 @@ module API
success ::API::Entities::Branch
end
get ":id/repository/branches" do
- branches = user_project.repository.branches.sort_by(&:name)
+ repository = user_project.repository
+ branches = repository.branches.sort_by(&:name)
+ merged_branch_names = repository.merged_branch_names(branches.map(&:name))
- present branches, with: ::API::Entities::Branch, project: user_project
+ present branches, with: ::API::Entities::Branch, project: user_project, merged_branch_names: merged_branch_names
end
desc 'Delete a branch'
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
index c189d486f50..fa0bef39602 100644
--- a/lib/api/v3/builds.rb
+++ b/lib/api/v3/builds.rb
@@ -8,7 +8,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects 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',
@@ -169,7 +169,7 @@ module API
authorize_update_builds!
build = get_build!(params[:build_id])
- authorize!(:update_build, build)
+ authorize!(:erase_build, build)
return forbidden!('Build is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
index 345cb7e7c11..0ef26aa696a 100644
--- a/lib/api/v3/commits.rb
+++ b/lib/api/v3/commits.rb
@@ -11,7 +11,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 project repository commits' do
success ::API::Entities::Commit
end
@@ -72,7 +72,7 @@ module API
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
end
- get ":id/repository/commits/:sha" do
+ get ":id/repository/commits/:sha", requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! "Commit" unless commit
@@ -86,7 +86,7 @@ module API
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
end
- get ":id/repository/commits/:sha/diff" do
+ get ":id/repository/commits/:sha/diff", requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! "Commit" unless commit
@@ -102,11 +102,11 @@ module API
use :pagination
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
end
- get ':id/repository/commits/:sha/comments' do
+ get ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
- notes = Note.where(commit_id: commit.id).order(:created_at)
+ notes = commit.notes.order(:created_at)
present paginate(notes), with: ::API::Entities::CommitNote
end
@@ -119,7 +119,7 @@ module API
requires :sha, type: String, desc: 'A commit sha to be cherry picked'
requires :branch, type: String, desc: 'The name of the branch'
end
- post ':id/repository/commits/:sha/cherry_pick' do
+ post ':id/repository/commits/:sha/cherry_pick', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
authorize! :push_code, user_project
commit = user_project.commit(params[:sha])
@@ -156,7 +156,7 @@ module API
requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line'
end
end
- post ':id/repository/commits/:sha/comments' do
+ post ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
@@ -169,11 +169,13 @@ module API
if params[:path]
commit.raw_diffs(limits: false).each do |diff|
next unless diff.new_path == params[:path]
+
lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
lines.each do |line|
next unless line.new_pos == params[:line] && line.type == params[:line_type]
- break opts[:line_code] = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
+
+ break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos)
end
break if opts[:line_code]
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index afdd7b83998..c17b6f45ed8 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -172,8 +172,8 @@ module API
expose :id
expose :default_projects_limit
expose :signup_enabled
- expose :password_authentication_enabled
- expose :password_authentication_enabled, as: :signin_enabled
+ expose :password_authentication_enabled_for_web, as: :password_authentication_enabled
+ expose :password_authentication_enabled_for_web, as: :signin_enabled
expose :gravatar_enabled
expose :sign_in_text
expose :after_sign_up_text
diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb
index 41a7c6b83ae..f9a47101e27 100644
--- a/lib/api/v3/repositories.rb
+++ b/lib/api/v3/repositories.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
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
@@ -43,7 +43,7 @@ module API
requires :sha, type: String, desc: 'The commit, branch name, or tag name'
requires :filepath, type: String, desc: 'The path to the file to display'
end
- get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"] do
+ get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"], requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
repo = user_project.repository
commit = repo.commit(params[:sha])
not_found! "Commit" unless commit
@@ -56,7 +56,7 @@ module API
params do
requires :sha, type: String, desc: 'The commit, branch name, or tag name'
end
- get ':id/repository/raw_blobs/:sha' do
+ get ':id/repository/raw_blobs/:sha', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
repo = user_project.repository
begin
blob = Gitlab::Git::Blob.raw(repo, params[:sha])
diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb
index faa265f3314..c6d9957d452 100644
--- a/lib/api/v3/runners.rb
+++ b/lib/api/v3/runners.rb
@@ -51,6 +51,7 @@ module API
helpers do
def authenticate_delete_runner!(runner)
return if current_user.admin?
+
forbidden!("Runner is shared") if runner.is_shared?
forbidden!("Runner associated with more than one project") if runner.projects.count > 1
forbidden!("No access granted") unless user_can_access_runner?(runner)
diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb
index 2d13d6fabfd..44ed94d2869 100644
--- a/lib/api/v3/services.rb
+++ b/lib/api/v3/services.rb
@@ -395,6 +395,26 @@ module API
desc: 'The Slack token'
}
],
+ 'packagist' => [
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'The username'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Packagist API token'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'The server'
+ }
+ ],
'pipelines-email' => [
{
required: true,
diff --git a/lib/api/v3/settings.rb b/lib/api/v3/settings.rb
index 202011cfcbe..9b4ab7630fb 100644
--- a/lib/api/v3/settings.rb
+++ b/lib/api/v3/settings.rb
@@ -44,8 +44,8 @@ module API
requires :domain_blacklist, type: String, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
end
optional :after_sign_up_text, type: String, desc: 'Text shown after sign up'
- optional :password_authentication_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled'
- optional :signin_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled'
+ optional :password_authentication_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface'
+ optional :signin_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface'
mutually_exclusive :password_authentication_enabled, :signin_enabled
optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to setup Two-factor authentication'
given require_two_factor_authentication: ->(val) { val } do
@@ -131,7 +131,9 @@ module API
attrs = declared_params(include_missing: false)
if attrs.has_key?(:signin_enabled)
- attrs[:password_authentication_enabled] = attrs.delete(:signin_enabled)
+ attrs[:password_authentication_enabled_for_web] = attrs.delete(:signin_enabled)
+ elsif attrs.has_key?(:password_authentication_enabled)
+ attrs[:password_authentication_enabled_for_web] = attrs.delete(:password_authentication_enabled)
end
if current_settings.update_attributes(attrs)
diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb
index 0762fc02d70..126ec72248e 100644
--- a/lib/api/v3/snippets.rb
+++ b/lib/api/v3/snippets.rb
@@ -91,6 +91,7 @@ module API
put ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
+
authorize! :update_personal_snippet, snippet
attrs = declared_params(include_missing: false)
@@ -113,6 +114,7 @@ module API
delete ':id' do
snippet = snippets_for_current_user.find_by(id: params.delete(:id))
return not_found!('Snippet') unless snippet
+
authorize! :destroy_personal_snippet, snippet
snippet.destroy
no_content!
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 3cf3939994a..05aa79dc160 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -101,50 +101,52 @@ module Backup
end
def unpack
- Dir.chdir(backup_path)
-
- # check for existing backups in the backup dir
- if backup_file_list.empty?
- $progress.puts "No backups found in #{backup_path}"
- $progress.puts "Please make sure that file name ends with #{FILE_NAME_SUFFIX}"
- exit 1
- elsif backup_file_list.many? && ENV["BACKUP"].nil?
- $progress.puts 'Found more than one backup, please specify which one you want to restore:'
- $progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup'
- exit 1
- end
+ Dir.chdir(backup_path) do
+ # check for existing backups in the backup dir
+ if backup_file_list.empty?
+ $progress.puts "No backups found in #{backup_path}"
+ $progress.puts "Please make sure that file name ends with #{FILE_NAME_SUFFIX}"
+ exit 1
+ elsif backup_file_list.many? && ENV["BACKUP"].nil?
+ $progress.puts 'Found more than one backup, please specify which one you want to restore:'
+ $progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup'
+ exit 1
+ end
- tar_file = if ENV['BACKUP'].present?
- "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
- else
- backup_file_list.first
- end
+ tar_file = if ENV['BACKUP'].present?
+ "#{ENV['BACKUP']}#{FILE_NAME_SUFFIX}"
+ else
+ backup_file_list.first
+ end
- unless File.exist?(tar_file)
- $progress.puts "The backup file #{tar_file} does not exist!"
- exit 1
- end
+ unless File.exist?(tar_file)
+ $progress.puts "The backup file #{tar_file} does not exist!"
+ exit 1
+ end
- $progress.print 'Unpacking backup ... '
+ $progress.print 'Unpacking backup ... '
- unless Kernel.system(*%W(tar -xf #{tar_file}))
- $progress.puts 'unpacking backup failed'.color(:red)
- exit 1
- else
- $progress.puts 'done'.color(:green)
- end
+ unless Kernel.system(*%W(tar -xf #{tar_file}))
+ $progress.puts 'unpacking backup failed'.color(:red)
+ exit 1
+ else
+ $progress.puts 'done'.color(:green)
+ end
- ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
-
- # restoring mismatching backups can lead to unexpected problems
- if settings[:gitlab_version] != Gitlab::VERSION
- $progress.puts 'GitLab version mismatch:'.color(:red)
- $progress.puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red)
- $progress.puts ' Please switch to the following version and try again:'.color(:red)
- $progress.puts " version: #{settings[:gitlab_version]}".color(:red)
- $progress.puts
- $progress.puts "Hint: git checkout v#{settings[:gitlab_version]}"
- exit 1
+ ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
+
+ # restoring mismatching backups can lead to unexpected problems
+ if settings[:gitlab_version] != Gitlab::VERSION
+ $progress.puts(<<~HEREDOC.color(:red))
+ GitLab version mismatch:
+ Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!
+ Please switch to the following version and try again:
+ version: #{settings[:gitlab_version]}
+ HEREDOC
+ $progress.puts
+ $progress.puts "Hint: git checkout v#{settings[:gitlab_version]}"
+ exit 1
+ end
end
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 3ad09a1b421..b6d273b98c2 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -7,12 +7,16 @@ module Backup
prepare
Project.find_each(batch_size: 1000) do |project|
- progress.print " * #{project.full_path} ... "
+ progress.print " * #{display_repo_path(project)} ... "
path_to_project_repo = path_to_repo(project)
path_to_project_bundle = path_to_bundle(project)
- # Create namespace dir if missing
- FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.full_path)) if project.namespace
+ # Create namespace dir or hashed path if missing
+ if project.hashed_storage?(:repository)
+ FileUtils.mkdir_p(File.dirname(File.join(backup_repos_path, project.disk_path)))
+ else
+ FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.full_path)) if project.namespace
+ end
if empty_repo?(project)
progress.puts "[SKIPPED]".color(:cyan)
@@ -42,7 +46,7 @@ module Backup
path_to_wiki_bundle = path_to_bundle(wiki)
if File.exist?(path_to_wiki_repo)
- progress.print " * #{wiki.full_path} ... "
+ progress.print " * #{display_repo_path(wiki)} ... "
if empty_repo?(wiki)
progress.puts " [SKIPPED]".color(:cyan)
else
@@ -71,7 +75,7 @@ module Backup
end
Project.find_each(batch_size: 1000) do |project|
- progress.print " * #{project.full_path} ... "
+ progress.print " * #{display_repo_path(project)} ... "
path_to_project_repo = path_to_repo(project)
path_to_project_bundle = path_to_bundle(project)
@@ -104,7 +108,7 @@ module Backup
path_to_wiki_bundle = path_to_bundle(wiki)
if File.exist?(path_to_wiki_bundle)
- progress.print " * #{wiki.full_path} ... "
+ progress.print " * #{display_repo_path(wiki)} ... "
# If a wiki bundle exists, first remove the empty repo
# that was initialized with ProjectWiki.new() and then
@@ -185,14 +189,14 @@ module Backup
def progress_warn(project, cmd, output)
progress.puts "[WARNING] Executing #{cmd}".color(:orange)
- progress.puts "Ignoring error on #{project.full_path} - #{output}".color(:orange)
+ progress.puts "Ignoring error on #{display_repo_path(project)} - #{output}".color(:orange)
end
def empty_repo?(project_or_wiki)
project_or_wiki.repository.expire_exists_cache # protect backups from stale cache
project_or_wiki.repository.empty_repo?
rescue => e
- progress.puts "Ignoring repository error and continuing backing up project: #{project_or_wiki.full_path} - #{e.message}".color(:orange)
+ progress.puts "Ignoring repository error and continuing backing up project: #{display_repo_path(project_or_wiki)} - #{e.message}".color(:orange)
false
end
@@ -204,5 +208,9 @@ module Backup
def progress
$progress
end
+
+ def display_repo_path(project)
+ project.hashed_storage?(:repository) ? "#{project.full_path} (#{project.disk_path})" : project.full_path
+ end
end
end
diff --git a/lib/banzai.rb b/lib/banzai.rb
index 35ca234c1ba..5df98f66f3b 100644
--- a/lib/banzai.rb
+++ b/lib/banzai.rb
@@ -3,8 +3,8 @@ module Banzai
Renderer.render(text, context)
end
- def self.render_field(object, field)
- Renderer.render_field(object, field)
+ def self.render_field(object, field, context = {})
+ Renderer.render_field(object, field, context)
end
def self.cache_collection_render(texts_and_contexts)
diff --git a/lib/banzai/filter/absolute_link_filter.rb b/lib/banzai/filter/absolute_link_filter.rb
new file mode 100644
index 00000000000..1ec6201523f
--- /dev/null
+++ b/lib/banzai/filter/absolute_link_filter.rb
@@ -0,0 +1,34 @@
+require 'uri'
+
+module Banzai
+ module Filter
+ # HTML filter that converts relative urls into absolute ones.
+ class AbsoluteLinkFilter < HTML::Pipeline::Filter
+ def call
+ return doc unless context[:only_path] == false
+
+ doc.search('a.gfm').each do |el|
+ process_link_attr el.attribute('href')
+ end
+
+ doc
+ end
+
+ protected
+
+ def process_link_attr(html_attr)
+ return if html_attr.blank?
+ return if html_attr.value.start_with?('//')
+
+ uri = URI(html_attr.value)
+ html_attr.value = absolute_link_attr(uri) if uri.relative?
+ rescue URI::Error
+ # noop
+ end
+
+ def absolute_link_attr(uri)
+ URI.join(Gitlab.config.gitlab.url, uri).to_s
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index ef4578aabd6..8975395aff1 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -95,7 +95,7 @@ module Banzai
end
def call
- return doc if project.nil?
+ return doc unless project || group
ref_pattern = object_class.reference_pattern
link_pattern = object_class.link_reference_pattern
@@ -213,7 +213,8 @@ module Banzai
end
def object_link_text(object, matches)
- text = object.reference_link_text(context[:project])
+ parent = context[:project] || context[:group]
+ text = object.reference_link_text(parent)
extras = object_link_text_extras(object, matches)
text += " (#{extras.join(", ")})" if extras.any?
@@ -288,10 +289,14 @@ module Banzai
end
def current_project_path
+ return unless project
+
@current_project_path ||= project.full_path
end
def current_project_namespace_path
+ return unless project
+
@current_project_namespace_path ||= project.namespace.full_path
end
@@ -307,30 +312,6 @@ module Banzai
def project_refs_cache
RequestStore[:banzai_project_refs] ||= {}
end
-
- def cached_call(request_store_key, cache_key, path: [])
- if RequestStore.active?
- cache = RequestStore[request_store_key] ||= Hash.new do |hash, key|
- hash[key] = Hash.new { |h, k| h[k] = {} }
- end
-
- cache = cache.dig(*path) if path.any?
-
- get_or_set_cache(cache, cache_key) { yield }
- else
- yield
- end
- end
-
- def get_or_set_cache(cache, key)
- if cache.key?(key)
- cache[key]
- else
- value = yield
- cache[key] = value if key.present?
- value
- end
- end
end
end
end
diff --git a/lib/banzai/filter/mermaid_filter.rb b/lib/banzai/filter/mermaid_filter.rb
new file mode 100644
index 00000000000..b545b947a2c
--- /dev/null
+++ b/lib/banzai/filter/mermaid_filter.rb
@@ -0,0 +1,20 @@
+module Banzai
+ module Filter
+ class MermaidFilter < HTML::Pipeline::Filter
+ def call
+ doc.css('pre[lang="mermaid"]').add_class('mermaid')
+ doc.css('pre[lang="mermaid"]').add_class('js-render-mermaid')
+
+ # The `<code></code>` blocks are added in the lib/banzai/filter/syntax_highlight_filter.rb
+ # We want to keep context and consistency, so we the blocks are added for all filters.
+ # Details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15107/diffs?diff_id=7962900#note_45495859
+ doc.css('pre[lang="mermaid"]').each do |pre|
+ document = pre.at('code')
+ document.replace(document.content)
+ end
+
+ doc
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index 4fc5f211e84..bb5da310e09 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -56,7 +56,7 @@ module Banzai
end
def find_milestone_with_finder(project, params)
- finder_params = { project_ids: [project.id], order: nil }
+ finder_params = { project_ids: [project.id], order: nil, state: 'all' }
# We don't support IID lookups for group milestones, because IIDs can
# clash between group and project milestones.
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index a6f8650ed3d..b9d5ecf70ec 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -8,6 +8,8 @@ module Banzai
# :project (required) - Current project, ignored if reference is cross-project.
# :only_path - Generate path-only links.
class ReferenceFilter < HTML::Pipeline::Filter
+ include RequestStoreReferenceCache
+
class << self
attr_accessor :reference_type
end
@@ -55,6 +57,10 @@ module Banzai
context[:project]
end
+ def group
+ context[:group]
+ end
+
def skip_project_check?
context[:skip_project_check]
end
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index d8c8deea628..6786b9d07b6 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -75,9 +75,19 @@ module Banzai
begin
node['href'] = node['href'].strip
uri = Addressable::URI.parse(node['href'])
- uri.scheme = uri.scheme.downcase if uri.scheme
- node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme)
+ return unless uri.scheme
+
+ # Remove all invalid scheme characters before checking against the
+ # list of unsafe protocols.
+ #
+ # See https://tools.ietf.org/html/rfc3986#section-3.1
+ scheme = uri.scheme
+ .strip
+ .downcase
+ .gsub(/[^A-Za-z0-9\+\.\-]+/, '')
+
+ node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(scheme)
rescue Addressable::URI::InvalidURIError
node.remove_attribute('href')
end
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 7da565043d1..a79a0154846 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -14,23 +14,26 @@ module Banzai
end
def highlight_node(node)
- language = node.attr('lang')
code = node.text
- css_classes = "code highlight"
- lexer = lexer_for(language)
- lang = lexer.tag
-
- begin
- code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: lang)
-
- css_classes << " js-syntax-highlight #{lang}"
- rescue
- lang = nil
- # Gracefully handle syntax highlighter bugs/errors to ensure
- # users can still access an issue/comment/etc.
+ css_classes = 'code highlight js-syntax-highlight'
+ language = node.attr('lang')
+
+ if use_rouge?(language)
+ lexer = lexer_for(language)
+ language = lexer.tag
+
+ begin
+ code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: language)
+ css_classes << " #{language}"
+ rescue
+ # Gracefully handle syntax highlighter bugs/errors to ensure
+ # users can still access an issue/comment/etc.
+
+ language = nil
+ end
end
- highlighted = %(<pre class="#{css_classes}" lang="#{lang}" v-pre="true"><code>#{code}</code></pre>)
+ highlighted = %(<pre class="#{css_classes}" lang="#{language}" v-pre="true"><code>#{code}</code></pre>)
# Extracted to a method to measure it
replace_parent_pre_element(node, highlighted)
@@ -51,6 +54,10 @@ module Banzai
# Replace the parent `pre` element with the entire highlighted block
node.parent.replace(highlighted)
end
+
+ def use_rouge?(language)
+ %w(math mermaid plantuml).exclude?(language)
+ end
end
end
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index f3356d6c51e..c7fa8a8119f 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -24,7 +24,7 @@ module Banzai
end
def call
- return doc if project.nil? && !skip_project_check?
+ return doc if project.nil? && group.nil? && !skip_project_check?
ref_pattern = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/
@@ -60,10 +60,14 @@ module Banzai
self.class.references_in(text) do |match, username|
if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content)
- elsif namespace = namespaces[username.downcase]
- link_to_namespace(namespace, link_content: link_content) || match
else
- match
+ cached_call(:banzai_url_for_object, match, path: [User, username.downcase]) do
+ if namespace = namespaces[username.downcase]
+ link_to_namespace(namespace, link_content: link_content) || match
+ else
+ match
+ end
+ end
end
end
end
@@ -74,7 +78,10 @@ module Banzai
# The keys of this Hash are the namespace paths, the values the
# corresponding Namespace objects.
def namespaces
- @namespaces ||= Namespace.where_full_path_in(usernames).index_by(&:full_path).transform_keys(&:downcase)
+ @namespaces ||= Namespace.eager_load(:owner, :route)
+ .where_full_path_in(usernames)
+ .index_by(&:full_path)
+ .transform_keys(&:downcase)
end
# Returns all usernames referenced in the current document.
@@ -101,19 +108,12 @@ module Banzai
end
def link_to_all(link_content: nil)
- project = context[:project]
author = context[:author]
- if author && !project.team.member?(author)
+ if author && !team_member?(author)
link_content
else
- url = urls.project_url(project,
- only_path: context[:only_path])
-
- data = data_attribute(project: project.id, author: author.try(:id))
- content = link_content || User.reference_prefix + 'all'
-
- link_tag(url, data, content, 'All Project and Group Members')
+ parent_url(link_content, author)
end
end
@@ -144,6 +144,35 @@ module Banzai
def link_tag(url, data, link_content, title)
%(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
end
+
+ def parent
+ context[:project] || context[:group]
+ end
+
+ def parent_group?
+ parent.is_a?(Group)
+ end
+
+ def team_member?(user)
+ if parent_group?
+ parent.member?(user)
+ else
+ parent.team.member?(user)
+ end
+ end
+
+ def parent_url(link_content, author)
+ if parent_group?
+ url = urls.group_url(parent, only_path: context[:only_path])
+ data = data_attribute(group: group.id, author: author.try(:id))
+ else
+ url = urls.project_url(parent, only_path: context[:only_path])
+ data = data_attribute(project: project.id, author: author.try(:id))
+ end
+
+ content = link_content || User.reference_prefix + 'all'
+ link_tag(url, data, content, 'All Project and Group Members')
+ end
end
end
end
diff --git a/lib/banzai/note_renderer.rb b/lib/banzai/note_renderer.rb
deleted file mode 100644
index 2b7c10f1a0e..00000000000
--- a/lib/banzai/note_renderer.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module Banzai
- module NoteRenderer
- # Renders a collection of Note instances.
- #
- # notes - The notes to render.
- # project - The project to use for redacting.
- # user - The user viewing the notes.
- # path - The request path.
- # wiki - The project's wiki.
- # git_ref - The current Git reference.
- def self.render(notes, project, user = nil, path = nil, wiki = nil, git_ref = nil)
- renderer = ObjectRenderer.new(project,
- user,
- requested_path: path,
- project_wiki: wiki,
- ref: git_ref)
-
- renderer.render(notes, :note)
- end
- end
-end
diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb
index e40556e869c..ecb3affbba5 100644
--- a/lib/banzai/object_renderer.rb
+++ b/lib/banzai/object_renderer.rb
@@ -37,7 +37,7 @@ module Banzai
objects.each_with_index do |object, index|
redacted_data = redacted[index]
- object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe) # rubocop:disable GitlabSecurity/PublicSend
+ object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html(save_options).html_safe) # rubocop:disable GitlabSecurity/PublicSend
object.user_visible_reference_count = redacted_data[:visible_reference_count] if object.respond_to?(:user_visible_reference_count)
end
end
@@ -83,5 +83,11 @@ module Banzai
skip_redaction: true
)
end
+
+ def save_options
+ return {} unless base_context[:xhtml]
+
+ { save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML }
+ end
end
end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 3208abfc538..55874ad50a3 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -14,6 +14,7 @@ module Banzai
Filter::SyntaxHighlightFilter,
Filter::MathFilter,
+ Filter::MermaidFilter,
Filter::UploadLinkFilter,
Filter::VideoLinkFilter,
Filter::ImageLazyLoadFilter,
diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb
index 131ac3b0eec..dcd52bc03c7 100644
--- a/lib/banzai/pipeline/post_process_pipeline.rb
+++ b/lib/banzai/pipeline/post_process_pipeline.rb
@@ -3,9 +3,10 @@ module Banzai
class PostProcessPipeline < BasePipeline
def self.filters
FilterArray[
+ Filter::RedactorFilter,
Filter::RelativeLinkFilter,
Filter::IssuableStateFilter,
- Filter::RedactorFilter
+ Filter::AbsoluteLinkFilter
]
end
diff --git a/lib/banzai/querying.rb b/lib/banzai/querying.rb
index fb2faae02bc..a19a05e8c0d 100644
--- a/lib/banzai/querying.rb
+++ b/lib/banzai/querying.rb
@@ -52,8 +52,10 @@ module Banzai
children.each do |child|
next if child.text.blank?
+
node = nodes.shift
break unless node == child
+
filtered_nodes << node
end
end
diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb
index 4d336068861..8932d4f2905 100644
--- a/lib/banzai/reference_parser/user_parser.rb
+++ b/lib/banzai/reference_parser/user_parser.rb
@@ -31,6 +31,7 @@ module Banzai
nodes.each do |node|
if node.has_attribute?(group_attr)
next unless can_read_group_reference?(node, user, groups)
+
visible << node
elsif can_read_project_reference?(node)
visible << node
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index 5f91884a878..0050295eeda 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -32,12 +32,9 @@ module Banzai
# Convert a Markdown-containing field on an object into an HTML-safe String
# of HTML. This method is analogous to calling render(object.field), but it
# can cache the rendered HTML in the object, rather than Redis.
- #
- # The context to use is managed by the object and cannot be changed.
- # Use #render, passing it the field text, if a custom rendering is needed.
- def self.render_field(object, field)
+ def self.render_field(object, field, context = {})
unless object.respond_to?(:cached_markdown_fields)
- return cacheless_render_field(object, field)
+ return cacheless_render_field(object, field, context)
end
object.refresh_markdown_cache! unless object.cached_html_up_to_date?(field)
@@ -46,9 +43,9 @@ module Banzai
end
# Same as +render_field+, but without consulting or updating the cache field
- def self.cacheless_render_field(object, field, options = {})
+ def self.cacheless_render_field(object, field, context = {})
text = object.__send__(field) # rubocop:disable GitlabSecurity/PublicSend
- context = object.banzai_render_context(field).merge(options)
+ context = context.reverse_merge(object.banzai_render_context(field)) if object.respond_to?(:banzai_render_context)
cacheless_render(text, context)
end
@@ -152,6 +149,7 @@ module Banzai
def self.full_cache_key(cache_key, pipeline_name)
return unless cache_key
+
["banzai", *cache_key, pipeline_name || :full]
end
@@ -160,6 +158,7 @@ module Banzai
# method.
def self.full_cache_multi_key(cache_key, pipeline_name)
return unless cache_key
+
Rails.cache.__send__(:expanded_key, full_cache_key(cache_key, pipeline_name)) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/lib/banzai/request_store_reference_cache.rb b/lib/banzai/request_store_reference_cache.rb
new file mode 100644
index 00000000000..426131442a2
--- /dev/null
+++ b/lib/banzai/request_store_reference_cache.rb
@@ -0,0 +1,27 @@
+module Banzai
+ module RequestStoreReferenceCache
+ def cached_call(request_store_key, cache_key, path: [])
+ if RequestStore.active?
+ cache = RequestStore[request_store_key] ||= Hash.new do |hash, key|
+ hash[key] = Hash.new { |h, k| h[k] = {} }
+ end
+
+ cache = cache.dig(*path) if path.any?
+
+ get_or_set_cache(cache, cache_key) { yield }
+ else
+ yield
+ end
+ end
+
+ def get_or_set_cache(cache, key)
+ if cache.key?(key)
+ cache[key]
+ else
+ value = yield
+ cache[key] = value if key.present?
+ value
+ end
+ end
+ end
+end
diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb
index 6fc1d56d7a0..fd2ac2db0a9 100644
--- a/lib/constraints/group_url_constrainer.rb
+++ b/lib/constraints/group_url_constrainer.rb
@@ -2,7 +2,7 @@ class GroupUrlConstrainer
def matches?(request)
full_path = request.params[:group_id] || request.params[:id]
- return false unless DynamicPathValidator.valid_group_path?(full_path)
+ return false unless NamespacePathValidator.valid_path?(full_path)
Group.find_by_full_path(full_path, follow_redirects: request.get?).present?
end
diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb
index 5bef29eb1da..e90ecb5ec69 100644
--- a/lib/constraints/project_url_constrainer.rb
+++ b/lib/constraints/project_url_constrainer.rb
@@ -4,7 +4,7 @@ class ProjectUrlConstrainer
project_path = request.params[:project_id] || request.params[:id]
full_path = [namespace_path, project_path].join('/')
- return false unless DynamicPathValidator.valid_project_path?(full_path)
+ return false unless ProjectPathValidator.valid_path?(full_path)
# We intentionally allow SELECT(*) here so result of this query can be used
# as cache for further Project.find_by_full_path calls within request
diff --git a/lib/constraints/user_url_constrainer.rb b/lib/constraints/user_url_constrainer.rb
index d16ae7f3f40..b7633aa7cbb 100644
--- a/lib/constraints/user_url_constrainer.rb
+++ b/lib/constraints/user_url_constrainer.rb
@@ -2,7 +2,7 @@ class UserUrlConstrainer
def matches?(request)
full_path = request.params[:username]
- return false unless DynamicPathValidator.valid_user_path?(full_path)
+ return false unless UserPathValidator.valid_path?(full_path)
User.find_by_full_path(full_path, follow_redirects: request.get?).present?
end
diff --git a/lib/declarative_policy.rb b/lib/declarative_policy.rb
index ae65653645b..b1949d693ad 100644
--- a/lib/declarative_policy.rb
+++ b/lib/declarative_policy.rb
@@ -30,6 +30,7 @@ module DeclarativePolicy
policy_class = class_for_class(subject.class)
raise "no policy for #{subject.class.name}" if policy_class.nil?
+
policy_class
end
@@ -84,6 +85,7 @@ module DeclarativePolicy
while subject.respond_to?(:declarative_policy_delegate)
raise ArgumentError, "circular delegations" if seen.include?(subject.object_id)
+
seen << subject.object_id
subject = subject.declarative_policy_delegate
end
diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb
index b028169f500..47542194497 100644
--- a/lib/declarative_policy/base.rb
+++ b/lib/declarative_policy/base.rb
@@ -276,6 +276,7 @@ module DeclarativePolicy
# boolean `false`
def cache(key, &b)
return @cache[key] if cached?(key)
+
@cache[key] = yield
end
@@ -291,6 +292,7 @@ module DeclarativePolicy
@_conditions[name] ||=
begin
raise "invalid condition #{name}" unless self.class.conditions.key?(name)
+
ManifestCondition.new(self.class.conditions[name], self)
end
end
diff --git a/lib/declarative_policy/cache.rb b/lib/declarative_policy/cache.rb
index 0804edba016..780d8f707bd 100644
--- a/lib/declarative_policy/cache.rb
+++ b/lib/declarative_policy/cache.rb
@@ -3,6 +3,7 @@ module DeclarativePolicy
class << self
def user_key(user)
return '<anonymous>' if user.nil?
+
id_for(user)
end
@@ -15,6 +16,7 @@ module DeclarativePolicy
def subject_key(subject)
return '<nil>' if subject.nil?
return subject.inspect if subject.is_a?(Symbol)
+
"#{subject.class.name}:#{id_for(subject)}"
end
diff --git a/lib/declarative_policy/rule.rb b/lib/declarative_policy/rule.rb
index 7cfa82a9a9f..e309244a3b3 100644
--- a/lib/declarative_policy/rule.rb
+++ b/lib/declarative_policy/rule.rb
@@ -83,6 +83,7 @@ module DeclarativePolicy
def cached_pass?(context)
condition = context.condition(@name)
return nil unless condition.cached?
+
condition.pass?
end
@@ -109,6 +110,7 @@ module DeclarativePolicy
def delegated_context(context)
policy = context.delegated_policies[@delegate_name]
raise MissingDelegate if policy.nil?
+
policy
end
@@ -121,6 +123,7 @@ module DeclarativePolicy
def cached_pass?(context)
condition = delegated_context(context).condition(@name)
return nil unless condition.cached?
+
condition.pass?
rescue MissingDelegate
false
@@ -157,6 +160,7 @@ module DeclarativePolicy
def cached_pass?(context)
runner = context.runner(@ability)
return nil unless runner.cached?
+
runner.pass?
end
@@ -258,6 +262,7 @@ module DeclarativePolicy
def score(context)
return 0 unless cached_pass?(context).nil?
+
@rules.map { |r| r.score(context) }.inject(0, :+)
end
diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb
index 45ff2ef9ced..77c91817382 100644
--- a/lib/declarative_policy/runner.rb
+++ b/lib/declarative_policy/runner.rb
@@ -43,6 +43,7 @@ module DeclarativePolicy
# used by Rule::Ability. See #steps_by_score
def score
return 0 if cached?
+
steps.map(&:score).inject(0, :+)
end
diff --git a/lib/feature.rb b/lib/feature.rb
index 4bd29aed687..ac3bc65c0d5 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -5,6 +5,10 @@ class Feature
class FlipperFeature < Flipper::Adapters::ActiveRecord::Feature
# Using `self.table_name` won't work. ActiveRecord bug?
superclass.table_name = 'features'
+
+ def self.feature_names
+ pluck(:key)
+ end
end
class FlipperGate < Flipper::Adapters::ActiveRecord::Gate
@@ -22,11 +26,19 @@ class Feature
flipper.feature(key)
end
+ def persisted_names
+ if RequestStore.active?
+ RequestStore[:flipper_persisted_names] ||= FlipperFeature.feature_names
+ else
+ FlipperFeature.feature_names
+ end
+ end
+
def persisted?(feature)
# Flipper creates on-memory features when asked for a not-yet-created one.
# If we want to check if a feature has been actually set, we look for it
# on the persisted features list.
- all.map(&:name).include?(feature.name)
+ persisted_names.include?(feature.name)
end
def enabled?(key, thing = nil)
diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb
index de391de9059..69d981e8be9 100644
--- a/lib/file_size_validator.rb
+++ b/lib/file_size_validator.rb
@@ -8,6 +8,7 @@ class FileSizeValidator < ActiveModel::EachValidator
def initialize(options)
if range = (options.delete(:in) || options.delete(:within))
raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range)
+
options[:minimum], options[:maximum] = range.begin, range.end
options[:maximum] -= 1 if range.exclude_end?
end
diff --git a/lib/github/client.rb b/lib/github/client.rb
deleted file mode 100644
index 29bd9c1f39e..00000000000
--- a/lib/github/client.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-module Github
- class Client
- TIMEOUT = 60
- DEFAULT_PER_PAGE = 100
-
- attr_reader :connection, :rate_limit
-
- def initialize(options)
- @connection = Faraday.new(url: options.fetch(:url, root_endpoint)) do |faraday|
- faraday.options.open_timeout = options.fetch(:timeout, TIMEOUT)
- faraday.options.timeout = options.fetch(:timeout, TIMEOUT)
- faraday.authorization 'token', options.fetch(:token)
- faraday.adapter :net_http
- faraday.ssl.verify = verify_ssl
- end
-
- @rate_limit = RateLimit.new(connection)
- end
-
- def get(url, query = {})
- exceed, reset_in = rate_limit.get
- sleep reset_in if exceed
-
- Github::Response.new(connection.get(url, { per_page: DEFAULT_PER_PAGE }.merge(query)))
- end
-
- private
-
- def root_endpoint
- custom_endpoint || github_endpoint
- end
-
- def custom_endpoint
- github_omniauth_provider.dig('args', 'client_options', 'site')
- end
-
- def verify_ssl
- # If there is no config, we're connecting to github.com
- # and we should verify ssl.
- github_omniauth_provider.fetch('verify_ssl', true)
- end
-
- def github_endpoint
- OmniAuth::Strategies::GitHub.default_options[:client_options][:site]
- end
-
- def github_omniauth_provider
- @github_omniauth_provider ||=
- Gitlab.config.omniauth.providers
- .find { |provider| provider.name == 'github' }
- .to_h
- end
- end
-end
diff --git a/lib/github/collection.rb b/lib/github/collection.rb
deleted file mode 100644
index 014b2038c4b..00000000000
--- a/lib/github/collection.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module Github
- class Collection
- attr_reader :options
-
- def initialize(options)
- @options = options
- end
-
- def fetch(url, query = {})
- return [] if url.blank?
-
- Enumerator.new do |yielder|
- loop do
- response = client.get(url, query)
- response.body.each { |item| yielder << item }
-
- raise StopIteration unless response.rels.key?(:next)
- url = response.rels[:next]
- end
- end.lazy
- end
-
- private
-
- def client
- @client ||= Github::Client.new(options)
- end
- end
-end
diff --git a/lib/github/error.rb b/lib/github/error.rb
deleted file mode 100644
index 66d7afaa787..00000000000
--- a/lib/github/error.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-module Github
- RepositoryFetchError = Class.new(StandardError)
-end
diff --git a/lib/github/import.rb b/lib/github/import.rb
deleted file mode 100644
index 55f8387f27a..00000000000
--- a/lib/github/import.rb
+++ /dev/null
@@ -1,376 +0,0 @@
-require_relative 'error'
-require_relative 'import/issue'
-require_relative 'import/legacy_diff_note'
-require_relative 'import/merge_request'
-require_relative 'import/note'
-
-module Github
- class Import
- include Gitlab::ShellAdapter
-
- attr_reader :project, :repository, :repo, :repo_url, :wiki_url,
- :options, :errors, :cached, :verbose, :last_fetched_at
-
- def initialize(project, options = {})
- @project = project
- @repository = project.repository
- @repo = project.import_source
- @repo_url = project.import_url
- @wiki_url = project.import_url.sub(/\.git\z/, '.wiki.git')
- @options = options.reverse_merge(token: project.import_data&.credentials&.fetch(:user))
- @verbose = options.fetch(:verbose, false)
- @cached = Hash.new { |hash, key| hash[key] = Hash.new }
- @errors = []
- @last_fetched_at = nil
- end
-
- # rubocop: disable Rails/Output
- def execute
- puts 'Fetching repository...'.color(:aqua) if verbose
- setup_and_fetch_repository
- puts 'Fetching labels...'.color(:aqua) if verbose
- fetch_labels
- puts 'Fetching milestones...'.color(:aqua) if verbose
- fetch_milestones
- puts 'Fetching pull requests...'.color(:aqua) if verbose
- fetch_pull_requests
- puts 'Fetching issues...'.color(:aqua) if verbose
- fetch_issues
- puts 'Fetching releases...'.color(:aqua) if verbose
- fetch_releases
- puts 'Cloning wiki repository...'.color(:aqua) if verbose
- fetch_wiki_repository
- puts 'Expiring repository cache...'.color(:aqua) if verbose
- expire_repository_cache
-
- errors.empty?
- rescue Github::RepositoryFetchError
- expire_repository_cache
- false
- ensure
- keep_track_of_errors
- end
-
- private
-
- def setup_and_fetch_repository
- begin
- project.ensure_repository
- project.repository.add_remote('github', repo_url)
- project.repository.set_import_remote_as_mirror('github')
- project.repository.add_remote_fetch_config('github', '+refs/pull/*/head:refs/merge-requests/*/head')
- fetch_remote(forced: true)
- rescue Gitlab::Git::Repository::NoRepository, Gitlab::Shell::Error => e
- error(:project, repo_url, e.message)
- raise Github::RepositoryFetchError
- end
- end
-
- def fetch_remote(forced: false)
- @last_fetched_at = Time.now
- project.repository.fetch_remote('github', forced: forced)
- end
-
- def fetch_wiki_repository
- return if project.wiki.repository_exists?
-
- wiki_path = "#{project.disk_path}.wiki"
- gitlab_shell.import_repository(project.repository_storage_path, wiki_path, wiki_url)
- rescue Gitlab::Shell::Error => e
- # GitHub error message when the wiki repo has not been created,
- # this means that repo has wiki enabled, but have no pages. So,
- # we can skip the import.
- if e.message !~ /repository not exported/
- error(:wiki, wiki_url, e.message)
- end
- end
-
- def fetch_labels
- url = "/repos/#{repo}/labels"
-
- while url
- response = Github::Client.new(options).get(url)
-
- response.body.each do |raw|
- begin
- representation = Github::Representation::Label.new(raw)
-
- label = project.labels.find_or_create_by!(title: representation.title) do |label|
- label.color = representation.color
- end
-
- cached[:label_ids][representation.title] = label.id
- rescue => e
- error(:label, representation.url, e.message)
- end
- end
-
- url = response.rels[:next]
- end
- end
-
- def fetch_milestones
- url = "/repos/#{repo}/milestones"
-
- while url
- response = Github::Client.new(options).get(url, state: :all)
-
- response.body.each do |raw|
- begin
- milestone = Github::Representation::Milestone.new(raw)
- next if project.milestones.where(iid: milestone.iid).exists?
-
- project.milestones.create!(
- iid: milestone.iid,
- title: milestone.title,
- description: milestone.description,
- due_date: milestone.due_date,
- state: milestone.state,
- created_at: milestone.created_at,
- updated_at: milestone.updated_at
- )
- rescue => e
- error(:milestone, milestone.url, e.message)
- end
- end
-
- url = response.rels[:next]
- end
- end
-
- def fetch_pull_requests
- url = "/repos/#{repo}/pulls"
-
- while url
- response = Github::Client.new(options).get(url, state: :all, sort: :created, direction: :asc)
-
- response.body.each do |raw|
- pull_request = Github::Representation::PullRequest.new(raw, options.merge(project: project))
- merge_request = MergeRequest.find_or_initialize_by(iid: pull_request.iid, source_project_id: project.id)
- next unless merge_request.new_record? && pull_request.valid?
-
- begin
- # If the PR has been created/updated after we last fetched the
- # remote, we fetch again to get the up-to-date refs.
- fetch_remote if pull_request.updated_at > last_fetched_at
-
- author_id = user_id(pull_request.author, project.creator_id)
- description = format_description(pull_request.description, pull_request.author)
-
- merge_request.attributes = {
- iid: pull_request.iid,
- title: pull_request.title,
- description: description,
- ref_fetched: true,
- source_project: pull_request.source_project,
- source_branch: pull_request.source_branch_name,
- source_branch_sha: pull_request.source_branch_sha,
- target_project: pull_request.target_project,
- target_branch: pull_request.target_branch_name,
- target_branch_sha: pull_request.target_branch_sha,
- state: pull_request.state,
- milestone_id: milestone_id(pull_request.milestone),
- author_id: author_id,
- assignee_id: user_id(pull_request.assignee),
- created_at: pull_request.created_at,
- updated_at: pull_request.updated_at
- }
-
- merge_request.save!(validate: false)
- merge_request.merge_request_diffs.create
-
- review_comments_url = "/repos/#{repo}/pulls/#{pull_request.iid}/comments"
- fetch_comments(merge_request, :review_comment, review_comments_url, LegacyDiffNote)
- rescue => e
- error(:pull_request, pull_request.url, e.message)
- end
- end
-
- url = response.rels[:next]
- end
- end
-
- def fetch_issues
- url = "/repos/#{repo}/issues"
-
- while url
- response = Github::Client.new(options).get(url, state: :all, sort: :created, direction: :asc)
-
- response.body.each { |raw| populate_issue(raw) }
-
- 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.labels? || representation.comments?
-
- merge_request = MergeRequest.find_by!(target_project_id: project.id, iid: representation.iid)
-
- if representation.labels?
- merge_request.update_attribute(:label_ids, label_ids(representation.labels))
- end
-
- fetch_comments_conditionally(merge_request, representation)
- else
- return if Issue.exists?(iid: representation.iid, project_id: project.id)
-
- 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.milestone_id = milestone_id(representation.milestone)
- issue.author_id = author_id
- issue.created_at = representation.created_at
- issue.updated_at = representation.updated_at
- issue.save!(validate: false)
-
- issue.update(
- label_ids: label_ids(representation.labels),
- assignee_ids: assignee_ids(representation.assignees))
-
- fetch_comments_conditionally(issue, representation)
- end
- rescue => e
- error(:issue, representation.url, e.message)
- end
- end
-
- def fetch_comments_conditionally(issuable, representation)
- if representation.comments?
- comments_url = "/repos/#{repo}/issues/#{issuable.iid}/comments"
- fetch_comments(issuable, :comment, comments_url)
- end
- end
-
- def fetch_comments(noteable, type, url, klass = Note)
- while url
- comments = Github::Client.new(options).get(url)
-
- ActiveRecord::Base.no_touching do
- comments.body.each do |raw|
- begin
- representation = Github::Representation::Comment.new(raw, options)
- author_id = user_id(representation.author, project.creator_id)
-
- note = klass.new
- note.project_id = project.id
- note.noteable = noteable
- note.note = format_description(representation.note, representation.author)
- note.commit_id = representation.commit_id
- note.line_code = representation.line_code
- note.author_id = author_id
- note.created_at = representation.created_at
- note.updated_at = representation.updated_at
- note.save!(validate: false)
- rescue => e
- error(type, representation.url, e.message)
- end
- end
- end
-
- url = comments.rels[:next]
- end
- end
-
- def fetch_releases
- url = "/repos/#{repo}/releases"
-
- while url
- response = Github::Client.new(options).get(url)
-
- response.body.each do |raw|
- representation = Github::Representation::Release.new(raw)
- next unless representation.valid?
-
- release = ::Release.find_or_initialize_by(project_id: project.id, tag: representation.tag)
- next unless release.new_record?
-
- begin
- release.description = representation.description
- release.created_at = representation.created_at
- release.updated_at = representation.updated_at
- release.save!(validate: false)
- rescue => e
- error(:release, representation.url, e.message)
- end
- end
-
- url = response.rels[:next]
- end
- end
-
- def label_ids(labels)
- labels.map { |label| cached[:label_ids][label.title] }.compact
- end
-
- def assignee_ids(assignees)
- assignees.map { |assignee| user_id(assignee) }.compact
- end
-
- def milestone_id(milestone)
- return unless milestone.present?
-
- project.milestones.select(:id).find_by(iid: milestone.iid)&.id
- end
-
- def user_id(user, fallback_id = nil)
- return unless user.present?
- return cached[:user_ids][user.id] if cached[:user_ids][user.id].present?
-
- gitlab_user_id = user_id_by_external_uid(user.id) || user_id_by_email(user.email)
-
- cached[:gitlab_user_ids][user.id] = gitlab_user_id.present?
- cached[:user_ids][user.id] = gitlab_user_id || fallback_id
- end
-
- def user_id_by_email(email)
- return nil unless email
-
- ::User.find_by_any_email(email)&.id
- end
-
- def user_id_by_external_uid(id)
- return nil unless id
-
- ::User.select(:id)
- .joins(:identities)
- .merge(::Identity.where(provider: :github, extern_uid: id))
- .first&.id
- end
-
- def format_description(body, author)
- return body if cached[:gitlab_user_ids][author.id]
-
- "*Created by: #{author.username}*\n\n#{body}"
- end
-
- def expire_repository_cache
- repository.expire_content_cache if project.repository_exists?
- end
-
- def keep_track_of_errors
- return unless errors.any?
-
- project.update_column(:import_error, {
- message: 'The remote data could not be fully imported.',
- errors: errors
- }.to_json)
- end
-
- def error(type, url, message)
- errors << { type: type, url: Gitlab::UrlSanitizer.sanitize(url), error: message }
- end
- end
-end
diff --git a/lib/github/import/issue.rb b/lib/github/import/issue.rb
deleted file mode 100644
index 171f0872666..00000000000
--- a/lib/github/import/issue.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Github
- class Import
- class Issue < ::Issue
- self.table_name = 'issues'
-
- self.reset_callbacks :save
- self.reset_callbacks :create
- self.reset_callbacks :commit
- self.reset_callbacks :update
- self.reset_callbacks :validate
- end
- end
-end
diff --git a/lib/github/import/legacy_diff_note.rb b/lib/github/import/legacy_diff_note.rb
deleted file mode 100644
index 18adff560b6..00000000000
--- a/lib/github/import/legacy_diff_note.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-module Github
- class Import
- class LegacyDiffNote < ::LegacyDiffNote
- self.table_name = 'notes'
- self.store_full_sti_class = false
-
- self.reset_callbacks :commit
- self.reset_callbacks :update
- self.reset_callbacks :validate
- end
- end
-end
diff --git a/lib/github/import/merge_request.rb b/lib/github/import/merge_request.rb
deleted file mode 100644
index c258e5d5e0e..00000000000
--- a/lib/github/import/merge_request.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Github
- class Import
- class MergeRequest < ::MergeRequest
- self.table_name = 'merge_requests'
-
- self.reset_callbacks :create
- self.reset_callbacks :save
- self.reset_callbacks :commit
- self.reset_callbacks :update
- self.reset_callbacks :validate
- end
- end
-end
diff --git a/lib/github/import/note.rb b/lib/github/import/note.rb
deleted file mode 100644
index 8cf4f30e6b7..00000000000
--- a/lib/github/import/note.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Github
- class Import
- class Note < ::Note
- self.table_name = 'notes'
- self.store_full_sti_class = false
-
- self.reset_callbacks :save
- self.reset_callbacks :commit
- self.reset_callbacks :update
- self.reset_callbacks :validate
- end
- end
-end
diff --git a/lib/github/rate_limit.rb b/lib/github/rate_limit.rb
deleted file mode 100644
index 884693d093c..00000000000
--- a/lib/github/rate_limit.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Github
- class RateLimit
- SAFE_REMAINING_REQUESTS = 100
- SAFE_RESET_TIME = 500
- RATE_LIMIT_URL = '/rate_limit'.freeze
-
- attr_reader :connection
-
- def initialize(connection)
- @connection = connection
- end
-
- def get
- response = connection.get(RATE_LIMIT_URL)
-
- # GitHub Rate Limit API returns 404 when the rate limit is disabled
- return false unless response.status != 404
-
- body = Oj.load(response.body, class_cache: false, mode: :compat)
- remaining = body.dig('rate', 'remaining').to_i
- reset_in = body.dig('rate', 'reset').to_i
- exceed = remaining <= SAFE_REMAINING_REQUESTS
-
- [exceed, reset_in]
- end
- end
-end
diff --git a/lib/github/repositories.rb b/lib/github/repositories.rb
deleted file mode 100644
index c1c9448f305..00000000000
--- a/lib/github/repositories.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module Github
- class Repositories
- attr_reader :options
-
- def initialize(options)
- @options = options
- end
-
- def fetch
- Collection.new(options).fetch(repos_url)
- end
-
- private
-
- def repos_url
- '/user/repos'
- end
- end
-end
diff --git a/lib/github/representation/base.rb b/lib/github/representation/base.rb
deleted file mode 100644
index f26bdbdd546..00000000000
--- a/lib/github/representation/base.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module Github
- module Representation
- class Base
- def initialize(raw, options = {})
- @raw = raw
- @options = options
- end
-
- def id
- raw['id']
- end
-
- def url
- raw['url']
- end
-
- def created_at
- raw['created_at']
- end
-
- def updated_at
- raw['updated_at']
- end
-
- private
-
- attr_reader :raw, :options
- end
- end
-end
diff --git a/lib/github/representation/branch.rb b/lib/github/representation/branch.rb
deleted file mode 100644
index 0087a3d3c4f..00000000000
--- a/lib/github/representation/branch.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-module Github
- module Representation
- class Branch < Representation::Base
- attr_reader :repository
-
- def user
- raw.dig('user', 'login') || 'unknown'
- end
-
- def repo?
- raw['repo'].present?
- end
-
- def repo
- return unless repo?
-
- @repo ||= Github::Representation::Repo.new(raw['repo'])
- end
-
- def ref
- raw['ref']
- end
-
- def sha
- raw['sha']
- end
-
- def short_sha
- Commit.truncate_sha(sha)
- end
-
- def valid?
- sha.present? && ref.present?
- end
-
- def restore!(name)
- repository.create_branch(name, sha)
- rescue Gitlab::Git::Repository::InvalidRef => e
- Rails.logger.error("#{self.class.name}: Could not restore branch #{name}: #{e}")
- end
-
- def remove!(name)
- repository.delete_branch(name)
- rescue Gitlab::Git::Repository::DeleteBranchError => e
- Rails.logger.error("#{self.class.name}: Could not remove branch #{name}: #{e}")
- end
-
- private
-
- def repository
- @repository ||= options.fetch(:repository)
- end
- end
- end
-end
diff --git a/lib/github/representation/comment.rb b/lib/github/representation/comment.rb
deleted file mode 100644
index 1b5be91461b..00000000000
--- a/lib/github/representation/comment.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-module Github
- module Representation
- class Comment < Representation::Base
- def note
- raw['body'] || ''
- end
-
- def author
- @author ||= Github::Representation::User.new(raw['user'], options)
- end
-
- def commit_id
- raw['commit_id']
- end
-
- def line_code
- return unless on_diff?
-
- parsed_lines = Gitlab::Diff::Parser.new.parse(diff_hunk.lines)
- generate_line_code(parsed_lines.to_a.last)
- end
-
- private
-
- def generate_line_code(line)
- Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
- end
-
- def on_diff?
- diff_hunk.present?
- end
-
- def diff_hunk
- raw['diff_hunk']
- end
-
- def file_path
- raw['path']
- end
- end
- end
-end
diff --git a/lib/github/representation/issuable.rb b/lib/github/representation/issuable.rb
deleted file mode 100644
index 768ba3b993c..00000000000
--- a/lib/github/representation/issuable.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-module Github
- module Representation
- class Issuable < Representation::Base
- def iid
- raw['number']
- end
-
- def title
- raw['title']
- end
-
- def description
- raw['body'] || ''
- end
-
- def milestone
- return unless raw['milestone'].present?
-
- @milestone ||= Github::Representation::Milestone.new(raw['milestone'])
- end
-
- def author
- @author ||= Github::Representation::User.new(raw['user'], options)
- end
-
- def labels?
- raw['labels'].any?
- end
-
- def labels
- @labels ||= Array(raw['labels']).map do |label|
- Github::Representation::Label.new(label, options)
- end
- end
- end
- end
-end
diff --git a/lib/github/representation/issue.rb b/lib/github/representation/issue.rb
deleted file mode 100644
index 4f1a02cb90f..00000000000
--- a/lib/github/representation/issue.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Github
- module Representation
- class Issue < Representation::Issuable
- def state
- raw['state'] == 'closed' ? 'closed' : 'opened'
- end
-
- def comments?
- raw['comments'] > 0
- end
-
- def pull_request?
- raw['pull_request'].present?
- end
-
- def assigned?
- raw['assignees'].present?
- end
-
- def assignees
- @assignees ||= Array(raw['assignees']).map do |user|
- Github::Representation::User.new(user, options)
- end
- end
- end
- end
-end
diff --git a/lib/github/representation/label.rb b/lib/github/representation/label.rb
deleted file mode 100644
index 60aa51f9569..00000000000
--- a/lib/github/representation/label.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Github
- module Representation
- class Label < Representation::Base
- def color
- "##{raw['color']}"
- end
-
- def title
- raw['name']
- end
- end
- end
-end
diff --git a/lib/github/representation/milestone.rb b/lib/github/representation/milestone.rb
deleted file mode 100644
index 917e6394ad4..00000000000
--- a/lib/github/representation/milestone.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module Github
- module Representation
- class Milestone < Representation::Base
- def iid
- raw['number']
- end
-
- def title
- raw['title']
- end
-
- def description
- raw['description']
- end
-
- def due_date
- raw['due_on']
- end
-
- def state
- raw['state'] == 'closed' ? 'closed' : 'active'
- end
- end
- end
-end
diff --git a/lib/github/representation/pull_request.rb b/lib/github/representation/pull_request.rb
deleted file mode 100644
index 0171179bb0f..00000000000
--- a/lib/github/representation/pull_request.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-module Github
- module Representation
- class PullRequest < Representation::Issuable
- delegate :sha, to: :source_branch, prefix: true
- delegate :sha, to: :target_branch, prefix: true
-
- def source_project
- project
- end
-
- def source_branch_name
- # Mimic the "user:branch" displayed in the MR widget,
- # i.e. "Request to merge rymai:add-external-mounts into master"
- cross_project? ? "#{source_branch.user}:#{source_branch.ref}" : source_branch.ref
- end
-
- def target_project
- project
- end
-
- def target_branch_name
- target_branch.ref
- end
-
- def state
- return 'merged' if raw['state'] == 'closed' && raw['merged_at'].present?
- return 'closed' if raw['state'] == 'closed'
-
- 'opened'
- end
-
- def opened?
- state == 'opened'
- end
-
- def valid?
- source_branch.valid? && target_branch.valid?
- end
-
- def assigned?
- raw['assignee'].present?
- end
-
- def assignee
- return unless assigned?
-
- @assignee ||= Github::Representation::User.new(raw['assignee'], options)
- end
-
- private
-
- def project
- @project ||= options.fetch(:project)
- end
-
- def source_branch
- @source_branch ||= Representation::Branch.new(raw['head'], repository: project.repository)
- end
-
- def target_branch
- @target_branch ||= Representation::Branch.new(raw['base'], repository: project.repository)
- end
-
- def cross_project?
- return true unless source_branch.repo?
-
- source_branch.repo.id != target_branch.repo.id
- end
- end
- end
-end
diff --git a/lib/github/representation/release.rb b/lib/github/representation/release.rb
deleted file mode 100644
index e7e4b428c1a..00000000000
--- a/lib/github/representation/release.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module Github
- module Representation
- class Release < Representation::Base
- def description
- raw['body']
- end
-
- def tag
- raw['tag_name']
- end
-
- def valid?
- !raw['draft']
- end
- end
- end
-end
diff --git a/lib/github/representation/repo.rb b/lib/github/representation/repo.rb
deleted file mode 100644
index 6938aa7db05..00000000000
--- a/lib/github/representation/repo.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-module Github
- module Representation
- class Repo < Representation::Base
- end
- end
-end
diff --git a/lib/github/representation/user.rb b/lib/github/representation/user.rb
deleted file mode 100644
index 18591380e25..00000000000
--- a/lib/github/representation/user.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module Github
- module Representation
- class User < Representation::Base
- def email
- return @email if defined?(@email)
-
- @email = Github::User.new(username, options).get.fetch('email', nil)
- end
-
- def username
- raw['login']
- end
- end
- end
-end
diff --git a/lib/github/response.rb b/lib/github/response.rb
deleted file mode 100644
index 761c524b553..00000000000
--- a/lib/github/response.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module Github
- class Response
- attr_reader :raw, :headers, :status
-
- def initialize(response)
- @raw = response
- @headers = response.headers
- @status = response.status
- end
-
- def body
- Oj.load(raw.body, class_cache: false, mode: :compat)
- end
-
- def rels
- links = headers['Link'].to_s.split(', ').map do |link|
- href, name = link.match(/<(.*?)>; rel="(\w+)"/).captures
-
- [name.to_sym, href]
- end
-
- Hash[*links.flatten]
- end
- end
-end
diff --git a/lib/github/user.rb b/lib/github/user.rb
deleted file mode 100644
index f88a29e590b..00000000000
--- a/lib/github/user.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module Github
- class User
- attr_reader :username, :options
-
- def initialize(username, options)
- @username = username
- @options = options
- end
-
- def get
- client.get(user_url).body
- end
-
- private
-
- def client
- @client ||= Github::Client.new(options)
- end
-
- def user_url
- "/users/#{username}"
- end
- end
-end
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index b4012ebbb99..7127948cf00 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -58,9 +58,9 @@ module Gitlab
def protection_options
{
"Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE,
- "Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch." => PROTECTION_DEV_CAN_MERGE,
- "Partially protected: Developers can push new commits, but cannot force push or delete the branch. Masters can do all of those." => PROTECTION_DEV_CAN_PUSH,
- "Fully protected: Developers cannot push new commits, force push, or delete the branch. Only masters can do any of those." => PROTECTION_FULL
+ "Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch. Masters can push to the branch." => PROTECTION_DEV_CAN_MERGE,
+ "Partially protected: Both developers and masters can push new commits, but cannot force push or delete the branch." => PROTECTION_DEV_CAN_PUSH,
+ "Fully protected: Developers cannot push new commits, but masters can. No-one can force push or delete the branch." => PROTECTION_FULL
}
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 87aeb76b66a..65d7fd3ec70 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -1,11 +1,11 @@
module Gitlab
module Auth
- MissingPersonalTokenError = Class.new(StandardError)
+ MissingPersonalAccessTokenError = Class.new(StandardError)
REGISTRY_SCOPES = [:read_registry].freeze
# Scopes used for GitLab API access
- API_SCOPES = [:api, :read_user].freeze
+ API_SCOPES = [:api, :read_user, :sudo].freeze
# Scopes used for OpenID Connect
OPENID_SCOPES = [:openid].freeze
@@ -25,7 +25,7 @@ module Gitlab
result =
service_request_check(login, password, project) ||
build_access_token_check(login, password) ||
- lfs_token_check(login, password) ||
+ lfs_token_check(login, password, project) ||
oauth_access_token_check(login, password) ||
personal_access_token_check(password) ||
user_with_password_for_git(login, password) ||
@@ -34,17 +34,21 @@ module Gitlab
rate_limit!(ip, success: result.success?, login: login)
Gitlab::Auth::UniqueIpsLimiter.limit_user!(result.actor)
- return result if result.success? || current_application_settings.password_authentication_enabled? || Gitlab::LDAP::Config.enabled?
+ return result if result.success? || authenticate_using_internal_or_ldap_password?
# If sign-in is disabled and LDAP is not configured, recommend a
# personal access token on failed auth attempts
- raise Gitlab::Auth::MissingPersonalTokenError
+ raise Gitlab::Auth::MissingPersonalAccessTokenError
end
def find_with_user_password(login, password)
# 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 authenticate_using_internal_or_ldap_password?
+
Gitlab::Auth::UniqueIpsLimiter.limit_user! do
user = User.by_login(login)
@@ -52,10 +56,8 @@ module Gitlab
# LDAP users are only authenticated via LDAP
if user.nil? || user.ldap_user?
# Second chance - try LDAP authentication
- return unless Gitlab::LDAP::Config.enabled?
-
Gitlab::LDAP::Authentication.login(login, password)
- else
+ elsif current_application_settings.password_authentication_enabled_for_git?
user if user.active? && user.valid_password?(password)
end
end
@@ -84,6 +86,10 @@ module Gitlab
private
+ def authenticate_using_internal_or_ldap_password?
+ current_application_settings.password_authentication_enabled_for_git? || Gitlab::LDAP::Config.enabled?
+ end
+
def service_request_check(login, password, project)
matched_login = /(?<service>^[a-zA-Z]*-ci)-token$/.match(login)
@@ -106,7 +112,7 @@ module Gitlab
user = find_with_user_password(login, password)
return unless user
- raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled?
+ raise Gitlab::Auth::MissingPersonalAccessTokenError if user.two_factor_enabled?
Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
end
@@ -128,7 +134,7 @@ module Gitlab
token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
if token && valid_scoped_token?(token, available_scopes)
- Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes))
+ Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes))
end
end
@@ -140,13 +146,18 @@ module Gitlab
AccessTokenValidationService.new(token).include_any_scope?(scopes)
end
- def abilities_for_scope(scopes)
- scopes.map do |scope|
- self.public_send(:"#{scope}_scope_authentication_abilities") # rubocop:disable GitlabSecurity/PublicSend
- end.flatten.uniq
+ def abilities_for_scopes(scopes)
+ abilities_by_scope = {
+ api: full_authentication_abilities,
+ read_registry: [:read_container_image]
+ }
+
+ scopes.flat_map do |scope|
+ abilities_by_scope.fetch(scope.to_sym, [])
+ end.uniq
end
- def lfs_token_check(login, password)
+ def lfs_token_check(login, password, project)
deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)
actor =
@@ -163,6 +174,8 @@ module Gitlab
authentication_abilities =
if token_handler.user?
full_authentication_abilities
+ elsif token_handler.deploy_key_pushable?(project)
+ read_write_authentication_abilities
else
read_authentication_abilities
end
@@ -208,26 +221,23 @@ module Gitlab
]
end
- def full_authentication_abilities
+ def read_write_authentication_abilities
read_authentication_abilities + [
:push_code,
- :create_container_image,
- :admin_container_image
+ :create_container_image
]
end
- alias_method :api_scope_authentication_abilities, :full_authentication_abilities
-
- def read_registry_scope_authentication_abilities
- [:read_container_image]
- end
- # The currently used auth method doesn't allow any actions for this scope
- def read_user_scope_authentication_abilities
- []
+ def full_authentication_abilities
+ read_write_authentication_abilities + [
+ :admin_container_image
+ ]
end
- def available_scopes
- API_SCOPES + registry_scopes
+ def available_scopes(current_user = nil)
+ scopes = API_SCOPES + registry_scopes
+ scopes.delete(:sudo) if current_user && !current_user.admin?
+ scopes
end
# Other available scopes
diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb
new file mode 100644
index 00000000000..46ec040ce92
--- /dev/null
+++ b/lib/gitlab/auth/request_authenticator.rb
@@ -0,0 +1,25 @@
+# Use for authentication only, in particular for Rack::Attack.
+# Does not perform authorization of scopes, etc.
+module Gitlab
+ module Auth
+ class RequestAuthenticator
+ include UserAuthFinders
+
+ attr_reader :request
+
+ def initialize(request)
+ @request = request
+ end
+
+ def user
+ find_sessionless_user || find_user_from_warden
+ end
+
+ def find_sessionless_user
+ find_user_from_access_token || find_user_from_rss_token
+ rescue Gitlab::Auth::AuthenticationError
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
new file mode 100644
index 00000000000..b4114a3ac96
--- /dev/null
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -0,0 +1,109 @@
+module Gitlab
+ module Auth
+ #
+ # Exceptions
+ #
+
+ AuthenticationError = Class.new(StandardError)
+ MissingTokenError = Class.new(AuthenticationError)
+ TokenNotFoundError = Class.new(AuthenticationError)
+ ExpiredError = Class.new(AuthenticationError)
+ RevokedError = Class.new(AuthenticationError)
+ UnauthorizedError = Class.new(AuthenticationError)
+
+ class InsufficientScopeError < AuthenticationError
+ attr_reader :scopes
+ def initialize(scopes)
+ @scopes = scopes.map { |s| s.try(:name) || s }
+ end
+ end
+
+ module UserAuthFinders
+ include Gitlab::Utils::StrongMemoize
+
+ PRIVATE_TOKEN_HEADER = 'HTTP_PRIVATE_TOKEN'.freeze
+ PRIVATE_TOKEN_PARAM = :private_token
+
+ # Check the Rails session for valid authentication details
+ def find_user_from_warden
+ current_request.env['warden']&.authenticate if verified_request?
+ end
+
+ def find_user_from_rss_token
+ return unless current_request.path.ends_with?('.atom') || current_request.format.atom?
+
+ token = current_request.params[:rss_token].presence
+ return unless token
+
+ User.find_by_rss_token(token) || raise(UnauthorizedError)
+ end
+
+ def find_user_from_access_token
+ return unless access_token
+
+ validate_access_token!
+
+ access_token.user || raise(UnauthorizedError)
+ end
+
+ def validate_access_token!(scopes: [])
+ return unless access_token
+
+ case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
+ when AccessTokenValidationService::INSUFFICIENT_SCOPE
+ raise InsufficientScopeError.new(scopes)
+ when AccessTokenValidationService::EXPIRED
+ raise ExpiredError
+ when AccessTokenValidationService::REVOKED
+ raise RevokedError
+ end
+ end
+
+ private
+
+ def access_token
+ strong_memoize(:access_token) do
+ find_oauth_access_token || find_personal_access_token
+ end
+ end
+
+ def find_personal_access_token
+ token =
+ current_request.params[PRIVATE_TOKEN_PARAM].presence ||
+ current_request.env[PRIVATE_TOKEN_HEADER].presence
+
+ return unless token
+
+ # Expiration, revocation and scopes are verified in `validate_access_token!`
+ PersonalAccessToken.find_by(token: token) || raise(UnauthorizedError)
+ end
+
+ def find_oauth_access_token
+ token = Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
+ return unless token
+
+ # Expiration, revocation and scopes are verified in `validate_access_token!`
+ oauth_token = OauthAccessToken.by_token(token)
+ raise UnauthorizedError unless oauth_token
+
+ oauth_token.revoke_previous_refresh_token!
+ oauth_token
+ end
+
+ # Check if the request is GET/HEAD, or if CSRF token is valid.
+ def verified_request?
+ Gitlab::RequestForgeryProtection.verified?(current_request.env)
+ end
+
+ def ensure_action_dispatch_request(request)
+ return request if request.is_a?(ActionDispatch::Request)
+
+ ActionDispatch::Request.new(request.env)
+ end
+
+ def current_request
+ @current_request ||= ensure_action_dispatch_request(request)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/.rubocop.yml b/lib/gitlab/background_migration/.rubocop.yml
new file mode 100644
index 00000000000..8242821cedc
--- /dev/null
+++ b/lib/gitlab/background_migration/.rubocop.yml
@@ -0,0 +1,52 @@
+# For background migrations we define a custom set of rules to make it less
+# difficult to review these migrations. To reduce the complexity of these
+# migrations some rules may be stricter than the defaults set in the root
+# .rubocop.yml file.
+---
+inherit_from: ../../../.rubocop.yml
+
+Metrics/AbcSize:
+ Enabled: true
+ Max: 30
+ Details: >
+ Code that involves a lot of branches can be very hard to wrap your head
+ around.
+
+Metrics/PerceivedComplexity:
+ Enabled: true
+
+Metrics/LineLength:
+ Enabled: true
+ Details: >
+ Long lines are very hard to read and make it more difficult to review
+ changes.
+
+Metrics/MethodLength:
+ Enabled: true
+ Max: 30
+ Details: >
+ Long methods can be very hard to review. Consider splitting this method up
+ into separate methods.
+
+Metrics/ClassLength:
+ Enabled: true
+ Details: >
+ Long classes can be very hard to review. Consider splitting this class up
+ into multiple classes.
+
+Metrics/BlockLength:
+ Enabled: true
+ Details: >
+ Long blocks can be hard to read. Consider splitting the code into separate
+ methods.
+
+Style/Documentation:
+ Enabled: true
+ Details: >
+ Adding documentation makes it easier to figure out what a migration is
+ supposed to do.
+
+Style/FrozenStringLiteralComment:
+ Enabled: true
+ Details: >-
+ This removes the need for calling "freeze", reducing noise in the code.
diff --git a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
index 4b468e9cd58..03b17b319fa 100644
--- a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
+++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/LineLength
+# rubocop:disable Style/Documentation
+
module Gitlab
module BackgroundMigration
class CreateForkNetworkMembershipsRange
@@ -46,10 +50,25 @@ module Gitlab
FROM fork_network_members
WHERE fork_network_members.project_id = forked_project_links.forked_to_project_id
)
+ AND EXISTS (
+ SELECT true
+ FROM projects
+ WHERE forked_project_links.forked_from_project_id = projects.id
+ )
+ AND NOT EXISTS (
+ SELECT true
+ FROM forked_project_links AS parent_links
+ WHERE parent_links.forked_to_project_id = forked_project_links.forked_from_project_id
+ AND NOT EXISTS (
+ SELECT true
+ FROM projects
+ WHERE parent_links.forked_from_project_id = projects.id
+ )
+ )
AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
MISSING_MEMBERS
- ForkNetworkMember.count_by_sql(count_sql) > 0
+ ForkedProjectLink.count_by_sql(count_sql) > 0
end
def log(message)
diff --git a/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb b/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb
index e94719db72e..c2bf42f846d 100644
--- a/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb
+++ b/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/LineLength
+# rubocop:disable Style/Documentation
+
class Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys
class GpgKey < ActiveRecord::Base
self.table_name = 'gpg_keys'
diff --git a/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb b/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb
index b1411be3016..a1af045a71f 100644
--- a/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb
+++ b/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/LineLength
+# rubocop:disable Style/Documentation
+
module Gitlab
module BackgroundMigration
class DeleteConflictingRedirectRoutesRange
diff --git a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb
index 8e5c95f2287..fd5cbf76e47 100644
--- a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb
+++ b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb
@@ -1,3 +1,9 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/MethodLength
+# rubocop:disable Metrics/LineLength
+# rubocop:disable Metrics/AbcSize
+# rubocop:disable Style/Documentation
+
module Gitlab
module BackgroundMigration
class DeserializeMergeRequestDiffsAndCommits
@@ -81,6 +87,7 @@ module Gitlab
def single_diff_rows(merge_request_diff)
sha_attribute = Gitlab::Database::ShaAttribute.new
commits = YAML.load(merge_request_diff.st_commits) rescue []
+ commits ||= []
commit_rows = commits.map.with_index do |commit, index|
commit_hash = commit.to_hash.with_indifferent_access.except(:parent_ids)
diff --git a/lib/gitlab/background_migration/migrate_build_stage_id_reference.rb b/lib/gitlab/background_migration/migrate_build_stage_id_reference.rb
index 91540127ea9..0a8a4313cd5 100644
--- a/lib/gitlab/background_migration/migrate_build_stage_id_reference.rb
+++ b/lib/gitlab/background_migration/migrate_build_stage_id_reference.rb
@@ -1,3 +1,6 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
module Gitlab
module BackgroundMigration
class MigrateBuildStageIdReference
diff --git a/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb b/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb
index 432f7c3e706..84ac00f1a5c 100644
--- a/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb
+++ b/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/LineLength
+# rubocop:disable Style/Documentation
+
module Gitlab
module BackgroundMigration
# Class that migrates events for the new push event payloads setup. All
diff --git a/lib/gitlab/background_migration/migrate_stage_status.rb b/lib/gitlab/background_migration/migrate_stage_status.rb
index b1ff0900709..0e5c7f092f2 100644
--- a/lib/gitlab/background_migration/migrate_stage_status.rb
+++ b/lib/gitlab/background_migration/migrate_stage_status.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/AbcSize
+# rubocop:disable Style/Documentation
+
module Gitlab
module BackgroundMigration
class MigrateStageStatus
diff --git a/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb b/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb
index 0881244ed49..7f243073fd0 100644
--- a/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb
+++ b/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/LineLength
+# rubocop:disable Style/Documentation
+
module Gitlab
module BackgroundMigration
class MigrateSystemUploadsToNewFolder
diff --git a/lib/gitlab/background_migration/move_personal_snippet_files.rb b/lib/gitlab/background_migration/move_personal_snippet_files.rb
index 07cec96bcc3..a4ef51fd0e8 100644
--- a/lib/gitlab/background_migration/move_personal_snippet_files.rb
+++ b/lib/gitlab/background_migration/move_personal_snippet_files.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/LineLength
+# rubocop:disable Style/Documentation
+
module Gitlab
module BackgroundMigration
class MovePersonalSnippetFiles
diff --git a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
index bc53e6d7f94..85749366bfd 100644
--- a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
+++ b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
@@ -1,3 +1,10 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/MethodLength
+# rubocop:disable Metrics/LineLength
+# rubocop:disable Metrics/ClassLength
+# rubocop:disable Metrics/BlockLength
+# rubocop:disable Style/Documentation
+
module Gitlab
module BackgroundMigration
class NormalizeLdapExternUidsRange
diff --git a/lib/gitlab/background_migration/populate_fork_networks_range.rb b/lib/gitlab/background_migration/populate_fork_networks_range.rb
index 6c355ed1e75..a976cb4c243 100644
--- a/lib/gitlab/background_migration/populate_fork_networks_range.rb
+++ b/lib/gitlab/background_migration/populate_fork_networks_range.rb
@@ -1,28 +1,101 @@
+# frozen_string_literal: true
+
module Gitlab
module BackgroundMigration
+ # This background migration is going to create all `fork_networks` and
+ # the `fork_network_members` for the roots of fork networks based on the
+ # existing `forked_project_links`.
+ #
+ # When the source of a fork is deleted, we will create the fork with the
+ # target project as the root. This way, when there are forks of the target
+ # project, they will be joined into the same fork network.
+ #
+ # When the `fork_networks` and memberships for the root projects are created
+ # the `CreateForkNetworkMembershipsRange` migration is scheduled. This
+ # migration will create the memberships for all remaining forks-of-forks
class PopulateForkNetworksRange
def perform(start_id, end_id)
- log("Creating fork networks for forked project links: #{start_id} - #{end_id}")
+ create_fork_networks_for_existing_projects(start_id, end_id)
+ create_fork_networks_for_missing_projects(start_id, end_id)
+ create_fork_networks_memberships_for_root_projects(start_id, end_id)
+
+ delay = BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY # rubocop:disable Metrics/LineLength
+ BackgroundMigrationWorker.perform_in(
+ delay, "CreateForkNetworkMembershipsRange", [start_id, end_id]
+ )
+ end
+ def create_fork_networks_for_existing_projects(start_id, end_id)
+ log("Creating fork networks: #{start_id} - #{end_id}")
ActiveRecord::Base.connection.execute <<~INSERT_NETWORKS
INSERT INTO fork_networks (root_project_id)
SELECT DISTINCT forked_project_links.forked_from_project_id
FROM forked_project_links
+ -- Exclude the forks that are not the first level fork of a project
WHERE NOT EXISTS (
SELECT true
FROM forked_project_links inner_links
WHERE inner_links.forked_to_project_id = forked_project_links.forked_from_project_id
)
+
+ /* Exclude the ones that are already created, in case the fork network
+ was already created for another fork of the project.
+ */
AND NOT EXISTS (
SELECT true
FROM fork_networks
WHERE forked_project_links.forked_from_project_id = fork_networks.root_project_id
)
+
+ -- Only create a fork network for a root project that still exists
+ AND EXISTS (
+ SELECT true
+ FROM projects
+ WHERE projects.id = forked_project_links.forked_from_project_id
+ )
+ AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
+ INSERT_NETWORKS
+ end
+
+ def create_fork_networks_for_missing_projects(start_id, end_id)
+ log("Creating fork networks with missing root: #{start_id} - #{end_id}")
+ ActiveRecord::Base.connection.execute <<~INSERT_NETWORKS
+ INSERT INTO fork_networks (root_project_id)
+ SELECT DISTINCT forked_project_links.forked_to_project_id
+
+ FROM forked_project_links
+
+ -- Exclude forks that are not the root forks
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM forked_project_links inner_links
+ WHERE inner_links.forked_to_project_id = forked_project_links.forked_from_project_id
+ )
+
+ /* Exclude the ones that are already created, in case this migration is
+ re-run
+ */
+ AND NOT EXISTS (
+ SELECT true
+ FROM fork_networks
+ WHERE forked_project_links.forked_to_project_id = fork_networks.root_project_id
+ )
+
+ /* Exclude projects for which the project still exists, those are
+ Processed in the previous step of this migration
+ */
+ AND NOT EXISTS (
+ SELECT true
+ FROM projects
+ WHERE projects.id = forked_project_links.forked_from_project_id
+ )
AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
INSERT_NETWORKS
+ end
+ def create_fork_networks_memberships_for_root_projects(start_id, end_id)
log("Creating memberships for root projects: #{start_id} - #{end_id}")
ActiveRecord::Base.connection.execute <<~INSERT_ROOT
@@ -31,8 +104,12 @@ module Gitlab
FROM fork_networks
+ /* Joining both on forked_from- and forked_to- so we could create the
+ memberships for forks for which the source was deleted
+ */
INNER JOIN forked_project_links
ON forked_project_links.forked_from_project_id = fork_networks.root_project_id
+ OR forked_project_links.forked_to_project_id = fork_networks.root_project_id
WHERE NOT EXISTS (
SELECT true
@@ -41,9 +118,6 @@ module Gitlab
)
AND forked_project_links.id BETWEEN #{start_id} AND #{end_id}
INSERT_ROOT
-
- delay = BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY
- BackgroundMigrationWorker.perform_in(delay, "CreateForkNetworkMembershipsRange", [start_id, end_id])
end
def log(message)
diff --git a/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb b/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb
new file mode 100644
index 00000000000..dcac355e1b0
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class PopulateMergeRequestsLatestMergeRequestDiffId
+ BATCH_SIZE = 1_000
+
+ class MergeRequest < ActiveRecord::Base
+ self.table_name = 'merge_requests'
+
+ include ::EachBatch
+ end
+
+ def perform(start_id, stop_id)
+ update = '
+ latest_merge_request_diff_id = (
+ SELECT MAX(id)
+ FROM merge_request_diffs
+ WHERE merge_requests.id = merge_request_diffs.merge_request_id
+ )'.squish
+
+ MergeRequest
+ .where(id: start_id..stop_id)
+ .where(latest_merge_request_diff_id: nil)
+ .each_batch(of: BATCH_SIZE) do |relation|
+
+ relation.update_all(update)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bare_repository_import/importer.rb b/lib/gitlab/bare_repository_import/importer.rb
new file mode 100644
index 00000000000..196de667805
--- /dev/null
+++ b/lib/gitlab/bare_repository_import/importer.rb
@@ -0,0 +1,101 @@
+module Gitlab
+ module BareRepositoryImport
+ class Importer
+ NoAdminError = Class.new(StandardError)
+
+ def self.execute(import_path)
+ import_path << '/' unless import_path.ends_with?('/')
+ repos_to_import = Dir.glob(import_path + '**/*.git')
+
+ unless user = User.admins.order_id_asc.first
+ raise NoAdminError.new('No admin user found to import repositories')
+ end
+
+ repos_to_import.each do |repo_path|
+ bare_repo = Gitlab::BareRepositoryImport::Repository.new(import_path, repo_path)
+
+ if bare_repo.hashed? || bare_repo.wiki?
+ log " * Skipping repo #{bare_repo.repo_path}".color(:yellow)
+
+ next
+ end
+
+ log "Processing #{repo_path}".color(:yellow)
+
+ new(user, bare_repo).create_project_if_needed
+ end
+ end
+
+ attr_reader :user, :project_name, :bare_repo
+
+ delegate :log, to: :class
+ delegate :project_name, :project_full_path, :group_path, :repo_path, :wiki_path, to: :bare_repo
+
+ def initialize(user, bare_repo)
+ @user = user
+ @bare_repo = bare_repo
+ end
+
+ def create_project_if_needed
+ if project = Project.find_by_full_path(project_full_path)
+ log " * #{project.name} (#{project_full_path}) exists"
+
+ return project
+ end
+
+ create_project
+ end
+
+ private
+
+ def create_project
+ group = find_or_create_groups
+
+ project = Projects::CreateService.new(user,
+ name: project_name,
+ path: project_name,
+ skip_disk_validation: true,
+ namespace_id: group&.id).execute
+
+ if project.persisted? && mv_repo(project)
+ log " * Created #{project.name} (#{project_full_path})".color(:green)
+
+ ProjectCacheWorker.perform_async(project.id)
+ else
+ log " * Failed trying to create #{project.name} (#{project_full_path})".color(:red)
+ log " Errors: #{project.errors.messages}".color(:red) if project.errors.any?
+ end
+
+ project
+ end
+
+ def mv_repo(project)
+ FileUtils.mv(repo_path, File.join(project.repository_storage_path, project.disk_path + '.git'))
+
+ if bare_repo.wiki_exists?
+ FileUtils.mv(wiki_path, File.join(project.repository_storage_path, project.disk_path + '.wiki.git'))
+ end
+
+ true
+ rescue => e
+ log " * Failed to move repo: #{e.message}".color(:red)
+
+ false
+ end
+
+ def find_or_create_groups
+ return nil unless group_path.present?
+
+ log " * Using namespace: #{group_path}"
+
+ Groups::NestedCreateService.new(user, group_path: group_path).execute
+ end
+
+ # This is called from within a rake task only used by Admins, so allow writing
+ # to STDOUT
+ def self.log(message)
+ puts message # rubocop:disable Rails/Output
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bare_repository_import/repository.rb b/lib/gitlab/bare_repository_import/repository.rb
new file mode 100644
index 00000000000..8574ac6eb30
--- /dev/null
+++ b/lib/gitlab/bare_repository_import/repository.rb
@@ -0,0 +1,42 @@
+module Gitlab
+ module BareRepositoryImport
+ class Repository
+ attr_reader :group_path, :project_name, :repo_path
+
+ def initialize(root_path, repo_path)
+ @root_path = root_path
+ @repo_path = repo_path
+
+ # Split path into 'all/the/namespaces' and 'project_name'
+ @group_path, _, @project_name = repo_relative_path.rpartition('/')
+ end
+
+ def wiki_exists?
+ File.exist?(wiki_path)
+ end
+
+ def wiki?
+ @wiki ||= repo_path.end_with?('.wiki.git')
+ end
+
+ def wiki_path
+ @wiki_path ||= repo_path.sub(/\.git$/, '.wiki.git')
+ end
+
+ def hashed?
+ @hashed ||= group_path.start_with?('@hashed')
+ end
+
+ def project_full_path
+ @project_full_path ||= "#{group_path}/#{project_name}"
+ end
+
+ private
+
+ def repo_relative_path
+ # Remove root path and `.git` at the end
+ repo_path[@root_path.size...-4]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bare_repository_importer.rb b/lib/gitlab/bare_repository_importer.rb
deleted file mode 100644
index 1d98d187805..00000000000
--- a/lib/gitlab/bare_repository_importer.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-module Gitlab
- class BareRepositoryImporter
- NoAdminError = Class.new(StandardError)
-
- def self.execute
- Gitlab.config.repositories.storages.each do |storage_name, repository_storage|
- git_base_path = repository_storage['path']
- repos_to_import = Dir.glob(git_base_path + '/**/*.git')
-
- repos_to_import.each do |repo_path|
- if repo_path.end_with?('.wiki.git')
- log " * Skipping wiki repo"
- next
- end
-
- log "Processing #{repo_path}".color(:yellow)
-
- repo_relative_path = repo_path[repository_storage['path'].length..-1]
- .sub(/^\//, '') # Remove leading `/`
- .sub(/\.git$/, '') # Remove `.git` at the end
- new(storage_name, repo_relative_path).create_project_if_needed
- end
- end
- end
-
- attr_reader :storage_name, :full_path, :group_path, :project_path, :user
- delegate :log, to: :class
-
- def initialize(storage_name, repo_path)
- @storage_name = storage_name
- @full_path = repo_path
-
- unless @user = User.admins.order_id_asc.first
- raise NoAdminError.new('No admin user found to import repositories')
- end
-
- @group_path, @project_path = File.split(repo_path)
- @group_path = nil if @group_path == '.'
- end
-
- def create_project_if_needed
- if project = Project.find_by_full_path(full_path)
- log " * #{project.name} (#{full_path}) exists"
- return project
- end
-
- create_project
- end
-
- private
-
- def create_project
- group = find_or_create_group
-
- project_params = {
- name: project_path,
- path: project_path,
- repository_storage: storage_name,
- namespace_id: group&.id,
- skip_disk_validation: true
- }
-
- project = Projects::CreateService.new(user, project_params).execute
-
- if project.persisted?
- log " * Created #{project.name} (#{full_path})".color(:green)
- ProjectCacheWorker.perform_async(project.id)
- else
- log " * Failed trying to create #{project.name} (#{full_path})".color(:red)
- log " Errors: #{project.errors.messages}".color(:red)
- end
-
- project
- end
-
- def find_or_create_group
- return nil unless group_path
-
- if namespace = Namespace.find_by_full_path(group_path)
- log " * Namespace #{group_path} exists.".color(:green)
- return namespace
- end
-
- log " * Creating Group: #{group_path}"
- Groups::NestedCreateService.new(user, group_path: group_path).execute
- end
-
- # This is called from within a rake task only used by Admins, so allow writing
- # to STDOUT
- #
- # rubocop:disable Rails/Output
- def self.log(message)
- puts message
- end
- # rubocop:enable Rails/Output
- end
-end
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index d1979bb7ed3..d48ae17aeaf 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -61,9 +61,9 @@ module Gitlab
def import_wiki
return if project.wiki.repository_exists?
- path_with_namespace = "#{project.full_path}.wiki"
+ disk_path = project.wiki.disk_path
import_url = project.import_url.sub(/\.git\z/, ".git/wiki")
- gitlab_shell.import_repository(project.repository_storage_path, path_with_namespace, import_url)
+ gitlab_shell.import_repository(project.repository_storage_path, disk_path, import_url)
rescue StandardError => e
errors << { type: :wiki, errors: e.message }
end
@@ -241,7 +241,7 @@ module Gitlab
end
def generate_line_code(pr_comment)
- Gitlab::Diff::LineCode.generate(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos)
+ Gitlab::Git.diff_line_code(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos)
end
def pull_request_comment_attributes(comment)
diff --git a/lib/gitlab/changes_list.rb b/lib/gitlab/changes_list.rb
index 5b32fca00a4..9c9e6668e6f 100644
--- a/lib/gitlab/changes_list.rb
+++ b/lib/gitlab/changes_list.rb
@@ -16,6 +16,7 @@ module Gitlab
@changes ||= begin
@raw_changes.map do |change|
next if change.blank?
+
oldrev, newrev, ref = change.strip.split(' ')
{ oldrev: oldrev, newrev: newrev, ref: ref }
end.compact
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index b6805230348..ef92fc5a0a0 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -12,7 +12,8 @@ module Gitlab
change_existing_tags: 'You are not allowed to change existing tags on this project.',
update_protected_tag: 'Protected tags cannot be updated.',
delete_protected_tag: 'Protected tags cannot be deleted.',
- create_protected_tag: 'You are not allowed to create this tag as it is protected.'
+ create_protected_tag: 'You are not allowed to create this tag as it is protected.',
+ lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
}.freeze
attr_reader :user_access, :project, :skip_authorization, :protocol
@@ -36,6 +37,7 @@ module Gitlab
push_checks
branch_checks
tag_checks
+ lfs_objects_exist_check
true
end
@@ -136,6 +138,14 @@ module Gitlab
def matching_merge_request?
Checks::MatchingMergeRequest.new(@newrev, @branch_name, @project).match?
end
+
+ def lfs_objects_exist_check
+ lfs_check = Checks::LfsIntegrity.new(project, @newrev)
+
+ if lfs_check.objects_missing?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing]
+ end
+ end
end
end
end
diff --git a/lib/gitlab/checks/lfs_integrity.rb b/lib/gitlab/checks/lfs_integrity.rb
new file mode 100644
index 00000000000..f7276a380dc
--- /dev/null
+++ b/lib/gitlab/checks/lfs_integrity.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Checks
+ class LfsIntegrity
+ REV_LIST_OBJECT_LIMIT = 2_000
+
+ def initialize(project, newrev)
+ @project = project
+ @newrev = newrev
+ end
+
+ def objects_missing?
+ return false unless @newrev && @project.lfs_enabled?
+
+ new_lfs_pointers = Gitlab::Git::LfsChanges.new(@project.repository, @newrev).new_pointers(object_limit: REV_LIST_OBJECT_LIMIT)
+
+ return false unless new_lfs_pointers.present?
+
+ existing_count = @project.lfs_storage_project
+ .lfs_objects
+ .where(oid: new_lfs_pointers.map(&:lfs_oid))
+ .count
+
+ existing_count != new_lfs_pointers.count
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index a788fb3fcbc..0bbd60d8ffe 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -98,6 +98,7 @@ module Gitlab
def read_string(gz)
string_size = read_uint32(gz)
return nil unless string_size
+
gz.read(string_size)
end
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
index 22941d48edf..5b2f09e03ea 100644
--- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -43,6 +43,7 @@ module Gitlab
def parent
return nil unless has_parent?
+
self.class.new(@path.to_s.chomp(basename), @entries)
end
@@ -64,6 +65,7 @@ module Gitlab
def directories(opts = {})
return [] unless directory?
+
dirs = children.select(&:directory?)
return dirs unless has_parent? && opts[:parent]
@@ -74,6 +76,7 @@ module Gitlab
def files
return [] unless directory?
+
children.select(&:file?)
end
diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb
index b88b2e36d53..c811f88f483 100644
--- a/lib/gitlab/ci/build/image.rb
+++ b/lib/gitlab/ci/build/image.rb
@@ -8,6 +8,7 @@ module Gitlab
def from_image(job)
image = Gitlab::Ci::Build::Image.new(job.options[:image])
return unless image.valid?
+
image
end
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index 6555c589173..2844be80a84 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -37,6 +37,7 @@ module Gitlab
def value
return { name: @config } if string?
return @config if hash?
+
{}
end
end
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
index 0159179f0a9..eb606b57667 100644
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -111,6 +111,7 @@ module Gitlab
def validate_string_or_regexp(value)
return false unless value.is_a?(String)
return validate_regexp(value) if look_like_regexp?(value)
+
true
end
end
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
index 8ad3e57e59d..2d9166d6bdd 100644
--- a/lib/gitlab/ci/status/build/cancelable.rb
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def action_icon
- 'icon_action_cancel'
+ 'cancel'
end
def action_path
diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb
index e42d3574357..dc90f398c7e 100644
--- a/lib/gitlab/ci/status/build/failed_allowed.rb
+++ b/lib/gitlab/ci/status/build/failed_allowed.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def icon
- 'icon_status_warning'
+ 'status_warning'
end
def group
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index c7726543599..b7b45466d3b 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -12,7 +12,7 @@ module Gitlab
end
def action_icon
- 'icon_action_play'
+ 'play'
end
def action_title
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
index 8c8fdc56d75..44ffe783e50 100644
--- a/lib/gitlab/ci/status/build/retryable.rb
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def action_icon
- 'icon_action_retry'
+ 'retry'
end
def action_title
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index d464738deaf..46e730797e4 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -12,7 +12,7 @@ module Gitlab
end
def action_icon
- 'icon_action_stop'
+ 'stop'
end
def action_title
diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb
index e5fdc1f8136..e6195a60d4f 100644
--- a/lib/gitlab/ci/status/canceled.rb
+++ b/lib/gitlab/ci/status/canceled.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_canceled'
+ 'status_canceled'
end
def favicon
diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb
index d188bd286a6..846f00b83dd 100644
--- a/lib/gitlab/ci/status/created.rb
+++ b/lib/gitlab/ci/status/created.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_created'
+ 'status_created'
end
def favicon
diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb
index 38e45714c22..27ce85bd3ed 100644
--- a/lib/gitlab/ci/status/failed.rb
+++ b/lib/gitlab/ci/status/failed.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_failed'
+ 'status_failed'
end
def favicon
diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb
index a4a7edadac9..fc387e2fd25 100644
--- a/lib/gitlab/ci/status/manual.rb
+++ b/lib/gitlab/ci/status/manual.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_manual'
+ 'status_manual'
end
def favicon
diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb
index 5164260b861..6780780db32 100644
--- a/lib/gitlab/ci/status/pending.rb
+++ b/lib/gitlab/ci/status/pending.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_pending'
+ 'status_pending'
end
def favicon
diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb
index 993937e98ca..ee13905e46d 100644
--- a/lib/gitlab/ci/status/running.rb
+++ b/lib/gitlab/ci/status/running.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_running'
+ 'status_running'
end
def favicon
diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb
index 0c942920b02..0dbdc4de426 100644
--- a/lib/gitlab/ci/status/skipped.rb
+++ b/lib/gitlab/ci/status/skipped.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_skipped'
+ 'status_skipped'
end
def favicon
diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb
index d7af98857b0..731013ec017 100644
--- a/lib/gitlab/ci/status/success.rb
+++ b/lib/gitlab/ci/status/success.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def icon
- 'icon_status_success'
+ 'status_success'
end
def favicon
diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb
index 4d7d82e04cf..32b4cf43e48 100644
--- a/lib/gitlab/ci/status/success_warning.rb
+++ b/lib/gitlab/ci/status/success_warning.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def icon
- 'icon_status_warning'
+ 'status_warning'
end
def group
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
index 98dfe900044..2a0cb640a14 100644
--- a/lib/gitlab/conflict/file.rb
+++ b/lib/gitlab/conflict/file.rb
@@ -4,82 +4,29 @@ module Gitlab
include Gitlab::Routing
include IconsHelper
- MissingResolution = Class.new(ResolutionError)
-
CONTEXT_LINES = 3
- attr_reader :merge_file_result, :their_path, :our_path, :our_mode, :merge_request, :repository
-
- def initialize(merge_file_result, conflict, merge_request:)
- @merge_file_result = merge_file_result
- @their_path = conflict[:theirs][:path]
- @our_path = conflict[:ours][:path]
- @our_mode = conflict[:ours][:mode]
- @merge_request = merge_request
- @repository = merge_request.project.repository
- @match_line_headers = {}
- end
-
- def content
- merge_file_result[:data]
- end
+ attr_reader :merge_request
- def our_blob
- @our_blob ||= repository.blob_at(merge_request.diff_refs.head_sha, our_path)
- end
+ # 'raw' holds the Gitlab::Git::Conflict::File that this instance wraps
+ attr_reader :raw
- def type
- lines unless @type
+ delegate :type, :content, :their_path, :our_path, :our_mode, :our_blob, :repository, to: :raw
- @type.inquiry
+ def initialize(raw, merge_request:)
+ @raw = raw
+ @merge_request = merge_request
+ @match_line_headers = {}
end
- # Array of Gitlab::Diff::Line objects
def lines
return @lines if defined?(@lines)
- begin
- @type = 'text'
- @lines = Gitlab::Conflict::Parser.new.parse(content,
- our_path: our_path,
- their_path: their_path,
- parent_file: self)
- rescue Gitlab::Conflict::Parser::ParserError
- @type = 'text-editor'
- @lines = nil
- end
+ @lines = raw.lines.nil? ? nil : map_raw_lines(raw.lines)
end
def resolve_lines(resolution)
- section_id = nil
-
- lines.map do |line|
- unless line.type
- section_id = nil
- next line
- end
-
- section_id ||= line_code(line)
-
- case resolution[section_id]
- when 'head'
- next unless line.type == 'new'
- when 'origin'
- next unless line.type == 'old'
- else
- raise MissingResolution, "Missing resolution for section ID: #{section_id}"
- end
-
- line
- end.compact
- end
-
- def resolve_content(resolution)
- if resolution == content
- raise MissingResolution, "Resolved content has no changes for file #{our_path}"
- end
-
- resolution
+ map_raw_lines(raw.resolve_lines(resolution))
end
def highlight_lines!
@@ -163,7 +110,7 @@ module Gitlab
end
def line_code(line)
- Gitlab::Diff::LineCode.generate(our_path, line.new_pos, line.old_pos)
+ Gitlab::Git.diff_line_code(our_path, line.new_pos, line.old_pos)
end
def create_match_line(line)
@@ -227,15 +174,14 @@ module Gitlab
new_path: our_path)
end
- # Don't try to print merge_request or repository.
- def inspect
- instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode, :type].map do |instance_variable|
- value = instance_variable_get("@#{instance_variable}")
+ private
- "#{instance_variable}=\"#{value}\""
+ def map_raw_lines(raw_lines)
+ raw_lines.map do |raw_line|
+ Gitlab::Diff::Line.new(raw_line[:full_line], raw_line[:type],
+ raw_line[:line_obj_index], raw_line[:line_old],
+ raw_line[:line_new], parent_file: self)
end
-
- "#<#{self.class} #{instance_variables.join(' ')}>"
end
end
end
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
index 90f83e0f810..fb28e80ff73 100644
--- a/lib/gitlab/conflict/file_collection.rb
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -1,48 +1,29 @@
module Gitlab
module Conflict
class FileCollection
- ConflictSideMissing = Class.new(StandardError)
-
- attr_reader :merge_request, :our_commit, :their_commit, :project
-
- delegate :repository, to: :project
-
- class << self
- # We can only write when getting the merge index from the source
- # project, because we will write to that project. We don't use this all
- # the time because this fetches a ref into the source project, which
- # isn't needed for reading.
- def for_resolution(merge_request)
- project = merge_request.source_project
-
- new(merge_request, project).tap do |file_collection|
- project
- .repository
- .with_repo_branch_commit(merge_request.target_project.repository.raw_repository, merge_request.target_branch) do
-
- yield file_collection
- end
- end
- end
-
- # We don't need to do `with_repo_branch_commit` here, because the target
- # project always fetches source refs when creating merge request diffs.
- def read_only(merge_request)
- new(merge_request, merge_request.target_project)
- end
+ attr_reader :merge_request, :resolver
+
+ def initialize(merge_request)
+ source_repo = merge_request.source_project.repository.raw
+ our_commit = merge_request.source_branch_head.raw
+ their_commit = merge_request.target_branch_head.raw
+ target_repo = merge_request.target_project.repository.raw
+ @resolver = Gitlab::Git::Conflict::Resolver.new(source_repo, our_commit, target_repo, their_commit)
+ @merge_request = merge_request
end
- def merge_index
- @merge_index ||= repository.rugged.merge_commits(our_commit, their_commit)
+ def resolve(user, commit_message, files)
+ args = {
+ source_branch: merge_request.source_branch,
+ target_branch: merge_request.target_branch,
+ commit_message: commit_message || default_commit_message
+ }
+ resolver.resolve_conflicts(user, files, args)
end
def files
- @files ||= merge_index.conflicts.map do |conflict|
- raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]
-
- Gitlab::Conflict::File.new(merge_index.merge_file(conflict[:ours][:path]),
- conflict,
- merge_request: merge_request)
+ @files ||= resolver.conflicts.map do |conflict_file|
+ Gitlab::Conflict::File.new(conflict_file, merge_request: merge_request)
end
end
@@ -61,8 +42,8 @@ module Gitlab
end
def default_commit_message
- conflict_filenames = merge_index.conflicts.map do |conflict|
- "# #{conflict[:ours][:path]}"
+ conflict_filenames = files.map do |conflict|
+ "# #{conflict.our_path}"
end
<<EOM.chomp
@@ -72,15 +53,6 @@ Merge branch '#{merge_request.target_branch}' into '#{merge_request.source_branc
#{conflict_filenames.join("\n")}
EOM
end
-
- private
-
- def initialize(merge_request, project)
- @merge_request = merge_request
- @our_commit = merge_request.source_branch_head.raw.rugged_commit
- @their_commit = merge_request.target_branch_head.raw.rugged_commit
- @project = project
- end
end
end
end
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
deleted file mode 100644
index e3678c914db..00000000000
--- a/lib/gitlab/conflict/parser.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-module Gitlab
- module Conflict
- class Parser
- UnresolvableError = Class.new(StandardError)
- UnmergeableFile = Class.new(UnresolvableError)
- UnsupportedEncoding = Class.new(UnresolvableError)
-
- # Recoverable errors - the conflict can be resolved in an editor, but not with
- # sections.
- ParserError = Class.new(StandardError)
- UnexpectedDelimiter = Class.new(ParserError)
- MissingEndDelimiter = Class.new(ParserError)
-
- def parse(text, our_path:, their_path:, parent_file: nil)
- validate_text!(text)
-
- line_obj_index = 0
- line_old = 1
- line_new = 1
- type = nil
- lines = []
- conflict_start = "<<<<<<< #{our_path}"
- conflict_middle = '======='
- conflict_end = ">>>>>>> #{their_path}"
-
- text.each_line.map do |line|
- full_line = line.delete("\n")
-
- if full_line == conflict_start
- validate_delimiter!(type.nil?)
-
- type = 'new'
- elsif full_line == conflict_middle
- validate_delimiter!(type == 'new')
-
- type = 'old'
- elsif full_line == conflict_end
- validate_delimiter!(type == 'old')
-
- type = nil
- elsif line[0] == '\\'
- type = 'nonewline'
- lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
- else
- lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
- line_old += 1 if type != 'new'
- line_new += 1 if type != 'old'
-
- line_obj_index += 1
- end
- end
-
- raise MissingEndDelimiter unless type.nil?
-
- lines
- end
-
- private
-
- def validate_text!(text)
- raise UnmergeableFile if text.blank? # Typically a binary file
- raise UnmergeableFile if text.length > 200.kilobytes
-
- text.force_encoding('UTF-8')
-
- raise UnsupportedEncoding unless text.valid_encoding?
- end
-
- def validate_delimiter!(condition)
- raise UnexpectedDelimiter unless condition
- end
- end
- end
-end
diff --git a/lib/gitlab/conflict/resolution_error.rb b/lib/gitlab/conflict/resolution_error.rb
deleted file mode 100644
index 0b61256b35a..00000000000
--- a/lib/gitlab/conflict/resolution_error.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module Gitlab
- module Conflict
- ResolutionError = Class.new(StandardError)
- end
-end
diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
index 2479b4a7706..9230894877f 100644
--- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -3,7 +3,6 @@ module Gitlab
class PlanEventFetcher < BaseEventFetcher
def initialize(*args)
@projections = [mr_diff_table[:id],
- mr_diff_table[:st_commits],
issue_metrics_table[:first_mentioned_in_commit_at]]
super(*args)
@@ -37,12 +36,7 @@ module Gitlab
def first_time_reference_commit(event)
return nil unless event && merge_request_diff_commits
- commits =
- if event['st_commits'].present?
- YAML.load(event['st_commits'])
- else
- merge_request_diff_commits[event['id'].to_i]
- end
+ commits = merge_request_diff_commits[event['id'].to_i]
return nil if commits.blank?
diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb
index dfd17e35707..633de9f9776 100644
--- a/lib/gitlab/daemon.rb
+++ b/lib/gitlab/daemon.rb
@@ -2,6 +2,7 @@ module Gitlab
class Daemon
def self.initialize_instance(*args)
raise "#{name} singleton instance already initialized" if @instance
+
@instance = new(*args)
Kernel.at_exit(&@instance.method(:stop))
@instance
@@ -43,7 +44,7 @@ module Gitlab
if thread
thread.wakeup if thread.alive?
- thread.join
+ thread.join unless Thread.current == thread
@thread = nil
end
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 357f16936c6..cd7b4c043da 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -4,6 +4,10 @@ module Gitlab
# https://www.postgresql.org/docs/9.2/static/datatype-numeric.html
# http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
MAX_INT_VALUE = 2147483647
+ # The max value between MySQL's TIMESTAMP and PostgreSQL's timestampz:
+ # https://www.postgresql.org/docs/9.1/static/datatype-datetime.html
+ # https://dev.mysql.com/doc/refman/5.7/en/datetime.html
+ MAX_TIMESTAMP_VALUE = Time.at((1 << 31) - 1).freeze
def self.config
ActiveRecord::Base.configurations[Rails.env]
@@ -104,20 +108,45 @@ module Gitlab
end
end
- def self.bulk_insert(table, rows)
+ # Bulk inserts a number of rows into a table, optionally returning their
+ # IDs.
+ #
+ # table - The name of the table to insert the rows into.
+ # rows - An Array of Hash instances, each mapping the columns to their
+ # values.
+ # return_ids - When set to true the return value will be an Array of IDs of
+ # the inserted rows, this only works on PostgreSQL.
+ def self.bulk_insert(table, rows, return_ids: false)
return if rows.empty?
keys = rows.first.keys
columns = keys.map { |key| connection.quote_column_name(key) }
+ return_ids = false if mysql?
tuples = rows.map do |row|
row.values_at(*keys).map { |value| connection.quote(value) }
end
- connection.execute <<-EOF
+ sql = <<-EOF
INSERT INTO #{table} (#{columns.join(', ')})
VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
EOF
+
+ if return_ids
+ sql << 'RETURNING id'
+ end
+
+ result = connection.execute(sql)
+
+ if return_ids
+ result.values.map { |tuple| tuple[0].to_i }
+ else
+ []
+ end
+ end
+
+ def self.sanitize_timestamp(timestamp)
+ MAX_TIMESTAMP_VALUE > timestamp ? timestamp : MAX_TIMESTAMP_VALUE.dup
end
# pool_size - The size of the DB pool.
diff --git a/lib/gitlab/database/grant.rb b/lib/gitlab/database/grant.rb
index aee3981e79a..9f76967fc77 100644
--- a/lib/gitlab/database/grant.rb
+++ b/lib/gitlab/database/grant.rb
@@ -6,28 +6,36 @@ module Gitlab
if Database.postgresql?
'information_schema.role_table_grants'
else
- 'mysql.user'
+ 'information_schema.schema_privileges'
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)
+ .where('grantee = user')
else
- where(Trigger_priv: 'Y')
+ queries = [
+ Grant.select(1)
+ .from('information_schema.user_privileges')
+ .where("PRIVILEGE_TYPE = 'SUPER'")
+ .where("GRANTEE = CONCAT('\\'', REPLACE(CURRENT_USER(), '@', '\\'@\\''), '\\'')"),
+
+ Grant.select(1)
+ .from('information_schema.schema_privileges')
+ .where("PRIVILEGE_TYPE = 'TRIGGER'")
+ .where('TABLE_SCHEMA = ?', Gitlab::Database.database_name)
+ .where("GRANTEE = CONCAT('\\'', REPLACE(CURRENT_USER(), '@', '\\'@\\''), '\\'')")
+ ]
+
+ union = SQL::Union.new(queries).to_sql
+
+ Grant.from("(#{union}) privs")
end
- priv.scope_to_current_user.any?
+ priv.any?
end
end
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 2c35da8f1aa..c276c3566b4 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -220,6 +220,15 @@ module Gitlab
# column - The name of the column to update.
# value - The value for the column.
#
+ # The `value` argument is typically a literal. To perform a computed
+ # update, an Arel literal can be used instead:
+ #
+ # update_value = Arel.sql('bar * baz')
+ #
+ # update_column_in_batches(:projects, :foo, update_value) do |table, query|
+ # query.where(table[:some_column].eq('hello'))
+ # end
+ #
# Rubocop's Metrics/AbcSize metric is disabled for this method as Rubocop
# determines this method to be too complex while there's no way to make it
# less "complex" without introducing extra methods (which actually will
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
index 5481024db8e..7e492938eac 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
@@ -68,6 +68,11 @@ module Gitlab
has_one :route, as: :source
self.table_name = 'projects'
+ HASHED_STORAGE_FEATURES = {
+ repository: 1,
+ attachments: 2
+ }.freeze
+
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage]['path']
end
@@ -76,6 +81,13 @@ module Gitlab
def self.name
'Project'
end
+
+ def hashed_storage?(feature)
+ raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature)
+ return false unless respond_to?(:storage_version)
+
+ self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]
+ end
end
end
end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
index 75a75f61953..d32616862f0 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
@@ -22,9 +22,11 @@ module Gitlab
end
def move_project_folders(project, old_full_path, new_full_path)
- move_repository(project, old_full_path, new_full_path)
- move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
- move_uploads(old_full_path, new_full_path)
+ unless project.hashed_storage?(:repository)
+ move_repository(project, old_full_path, new_full_path)
+ move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
+ end
+ move_uploads(old_full_path, new_full_path) unless project.hashed_storage?(:attachments)
move_pages(old_full_path, new_full_path)
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 599c3c5deab..d0cfe2386ca 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -25,6 +25,10 @@ module Gitlab
@repository = repository
@diff_refs = diff_refs
@fallback_diff_refs = fallback_diff_refs
+
+ # Ensure items are collected in the the batch
+ new_blob
+ old_blob
end
def position(position_marker, position_type: :text)
@@ -49,7 +53,7 @@ module Gitlab
def line_code(line)
return if line.meta?
- Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
+ Gitlab::Git.diff_line_code(file_path, line.new_pos, line.old_pos)
end
def line_for_line_code(code)
@@ -95,21 +99,15 @@ module Gitlab
end
def new_blob
- return @new_blob if defined?(@new_blob)
-
- sha = new_content_sha
- return @new_blob = nil unless sha
+ return unless new_content_sha
- @new_blob = repository.blob_at(sha, file_path)
+ Blob.lazy(repository.project, new_content_sha, file_path)
end
def old_blob
- return @old_blob if defined?(@old_blob)
-
- sha = old_content_sha
- return @old_blob = nil unless sha
+ return unless old_content_sha
- @old_blob = repository.blob_at(sha, old_path)
+ Blob.lazy(repository.project, old_content_sha, old_path)
end
def content_sha
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index 88ae65cb468..a6007ebf531 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -22,10 +22,7 @@ module Gitlab
end
def diff_files
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37445
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) }
- end
+ @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) }
end
def diff_file_with_old_path(old_path)
diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb
index 55708d42161..2d7b57120a6 100644
--- a/lib/gitlab/diff/inline_diff.rb
+++ b/lib/gitlab/diff/inline_diff.rb
@@ -102,6 +102,7 @@ module Gitlab
new_char = b[pos]
break if old_char != new_char
+
length += 1
end
diff --git a/lib/gitlab/diff/line_code.rb b/lib/gitlab/diff/line_code.rb
deleted file mode 100644
index f3578ab3d35..00000000000
--- a/lib/gitlab/diff/line_code.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Gitlab
- module Diff
- class LineCode
- def self.generate(file_path, new_line_position, old_line_position)
- "#{Digest::SHA1.hexdigest(file_path)}_#{old_line_position}_#{new_line_position}"
- end
- end
- end
-end
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 742f989c50b..8302f30a0a2 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -17,7 +17,9 @@ module Gitlab
# without having to instantiate all the others that come after it.
Enumerator.new do |yielder|
@lines.each do |line|
- next if filename?(line)
+ # We're expecting a filename parameter only in a meta-part of the diff content
+ # when type is defined then we're already in a content-part
+ next if filename?(line) && type.nil?
full_line = line.delete("\n")
@@ -28,6 +30,7 @@ module Gitlab
line_new = line.match(/\+[0-9]*/)[0].to_i.abs rescue 0
next if line_old <= 1 && line_new <= 1 # top of file
+
yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
line_obj_index += 1
next
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index bd0a9502a5e..690b27cde81 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -94,7 +94,9 @@ module Gitlab
end
def diff_file(repository)
- @diff_file ||= begin
+ return @diff_file if defined?(@diff_file)
+
+ @diff_file = begin
if RequestStore.active?
key = {
project_id: repository.project.id,
@@ -122,8 +124,9 @@ module Gitlab
def find_diff_file(repository)
return unless diff_refs.complete?
+ return unless comparison = diff_refs.compare_in(repository.project)
- diff_refs.compare_in(repository.project).diffs(paths: paths, expanded: true).diff_files.first
+ comparison.diffs(paths: paths, expanded: true).diff_files.first
end
def get_formatter_class(type)
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index c4c60d1dfee..4a9d3e52fae 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -2,8 +2,8 @@
module Gitlab
# Checks if a set of migrations requires downtime or not.
class EeCompatCheck
- DEFAULT_CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
- EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
+ DEFAULT_CE_PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-ce'.freeze
+ EE_REPO_URL = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
CHECK_DIR = Rails.root.join('ee_compat_check')
IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
PLEASE_READ_THIS_BANNER = %Q{
@@ -17,28 +17,36 @@ module Gitlab
============================================================\n
}.freeze
- attr_reader :ee_repo_dir, :patches_dir, :ce_repo, :ce_branch, :ee_branch_found
- attr_reader :failed_files
+ attr_reader :ee_repo_dir, :patches_dir, :ce_project_url, :ce_repo_url, :ce_branch, :ee_branch_found
+ attr_reader :job_id, :failed_files
- def initialize(branch:, ce_repo: DEFAULT_CE_REPO)
+ def initialize(branch:, ce_project_url: DEFAULT_CE_PROJECT_URL, job_id: nil)
@ee_repo_dir = CHECK_DIR.join('ee-repo')
@patches_dir = CHECK_DIR.join('patches')
@ce_branch = branch
- @ce_repo = ce_repo
+ @ce_project_url = ce_project_url
+ @ce_repo_url = "#{ce_project_url}.git"
+ @job_id = job_id
end
def check
ensure_patches_dir
- generate_patch(ce_branch, ce_patch_full_path)
+ add_remote('canonical-ce', "#{DEFAULT_CE_PROJECT_URL}.git")
+ generate_patch(branch: ce_branch, patch_path: ce_patch_full_path, remote: 'canonical-ce')
ensure_ee_repo
Dir.chdir(ee_repo_dir) do
step("In the #{ee_repo_dir} directory")
+ add_remote('canonical-ee', EE_REPO_URL)
+
status = catch(:halt_check) do
ce_branch_compat_check!
delete_ee_branches_locally!
ee_branch_presence_check!
+
+ step("Checking out #{ee_branch_found}", %W[git checkout -b #{ee_branch_found} canonical-ee/#{ee_branch_found}])
+ generate_patch(branch: ee_branch_found, patch_path: ee_patch_full_path, remote: 'canonical-ee')
ee_branch_compat_check!
end
@@ -54,13 +62,20 @@ module Gitlab
private
+ def add_remote(name, url)
+ step(
+ "Adding the #{name} remote (#{url})",
+ %W[git remote add #{name} #{url}]
+ )
+ end
+
def ensure_ee_repo
if Dir.exist?(ee_repo_dir)
step("#{ee_repo_dir} already exists")
else
step(
- "Cloning #{EE_REPO} into #{ee_repo_dir}",
- %W[git clone --branch master --single-branch --depth=200 #{EE_REPO} #{ee_repo_dir}]
+ "Cloning #{EE_REPO_URL} into #{ee_repo_dir}",
+ %W[git clone --branch master --single-branch --depth=200 #{EE_REPO_URL} #{ee_repo_dir}]
)
end
end
@@ -69,14 +84,14 @@ module Gitlab
FileUtils.mkdir_p(patches_dir)
end
- def generate_patch(branch, patch_path)
+ def generate_patch(branch:, patch_path:, remote:)
FileUtils.rm(patch_path, force: true)
- find_merge_base_with_master(branch: branch)
+ find_merge_base_with_master(branch: branch, master_remote: remote)
step(
- "Generating the patch against origin/master in #{patch_path}",
- %w[git diff --binary origin/master...HEAD]
+ "Generating the patch against #{remote}/master in #{patch_path}",
+ %W[git diff --binary #{remote}/master...origin/#{branch}]
) do |output, status|
throw(:halt_check, :ko) unless status.zero?
@@ -94,14 +109,14 @@ module Gitlab
end
def ee_branch_presence_check!
- _, status = step("Fetching origin/#{ee_branch_prefix}", %W[git fetch origin #{ee_branch_prefix}])
+ _, status = step("Fetching origin/#{ee_branch_prefix}", %W[git fetch canonical-ee #{ee_branch_prefix}])
if status.zero?
@ee_branch_found = ee_branch_prefix
return
end
- _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch origin #{ee_branch_suffix}])
+ _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch canonical-ee #{ee_branch_suffix}])
if status.zero?
@ee_branch_found = ee_branch_suffix
@@ -114,10 +129,6 @@ module Gitlab
end
def ee_branch_compat_check!
- step("Checking out origin/#{ee_branch_found}", %W[git checkout -b #{ee_branch_found} FETCH_HEAD])
-
- generate_patch(ee_branch_found, ee_patch_full_path)
-
unless check_patch(ee_patch_full_path).zero?
puts
puts ee_branch_doesnt_apply_cleanly_msg
@@ -131,8 +142,7 @@ module Gitlab
def check_patch(patch_path)
step("Checking out master", %w[git checkout master])
- step("Resetting to latest master", %w[git reset --hard origin/master])
- step("Fetching CE/#{ce_branch}", %W[git fetch #{ce_repo} #{ce_branch}])
+ step("Resetting to latest master", %w[git reset --hard canonical-ee/master])
step(
"Checking if #{patch_path} applies cleanly to EE/master",
# Don't use --check here because it can result in a 0-exit status even
@@ -169,10 +179,10 @@ module Gitlab
command(%W[git branch --delete --force #{ee_branch_suffix}])
end
- def merge_base_found?
+ def merge_base_found?(master_remote:, branch:)
step(
- "Finding merge base with master",
- %w[git merge-base origin/master HEAD]
+ "Finding merge base with #{master_remote}/master",
+ %W[git merge-base #{master_remote}/master origin/#{branch}]
) do |output, status|
if status.zero?
puts "Merge base was found: #{output}"
@@ -181,7 +191,7 @@ module Gitlab
end
end
- def find_merge_base_with_master(branch:)
+ def find_merge_base_with_master(branch:, master_remote:)
# Start with (Math.exp(3).to_i = 20) until (Math.exp(6).to_i = 403)
# In total we go (20 + 54 + 148 + 403 = 625) commits deeper
depth = 20
@@ -190,19 +200,19 @@ module Gitlab
depth += Math.exp(factor).to_i
# Repository is initially cloned with a depth of 20 so we need to fetch
# deeper in the case the branch has more than 20 commits on top of master
- fetch(branch: branch, depth: depth)
- fetch(branch: 'master', depth: depth)
+ fetch(branch: branch, depth: depth, remote: 'origin')
+ fetch(branch: 'master', depth: depth, remote: master_remote)
- merge_base_found?
+ merge_base_found?(master_remote: master_remote, branch: branch)
end
- raise "\n#{branch} is too far behind master, please rebase it!\n" unless success
+ raise "\n#{branch} is too far behind #{master_remote}/master, please rebase it!\n" unless success
end
- def fetch(branch:, depth:)
+ def fetch(branch:, depth:, remote: 'origin')
step(
"Fetching deeper...",
- %W[git fetch --depth=#{depth} --prune origin +refs/heads/#{branch}:refs/remotes/origin/#{branch}]
+ %W[git fetch --depth=#{depth} --prune #{remote} +refs/heads/#{branch}:refs/remotes/#{remote}/#{branch}]
) do |output, status|
raise "Fetch failed: #{output}" unless status.zero?
end
@@ -237,7 +247,7 @@ module Gitlab
end
def patch_url
- "https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/#{ENV['CI_JOB_ID']}/artifacts/raw/ee_compat_check/patches/#{ce_patch_name}"
+ "#{ce_project_url}/-/jobs/#{job_id}/artifacts/raw/ee_compat_check/patches/#{ce_patch_name}"
end
def step(desc, cmd = nil)
@@ -302,9 +312,9 @@ module Gitlab
1. Create a new branch from master and cherry-pick your CE commits
# In the EE repo
- $ git fetch origin
- $ git checkout -b #{ee_branch_prefix} origin/master
- $ git fetch #{ce_repo} #{ce_branch}
+ $ git fetch #{EE_REPO_URL} master
+ $ git checkout -b #{ee_branch_prefix} FETCH_HEAD
+ $ git fetch #{ce_repo_url} #{ce_branch}
$ git cherry-pick SHA # Repeat for all the commits you want to pick
You can squash the `#{ce_branch}` commits into a single "Port of #{ce_branch} to EE" commit.
@@ -312,10 +322,9 @@ module Gitlab
2. Apply your branch's patch to EE
# In the EE repo
- $ git fetch origin master
- $ git checkout -b #{ee_branch_prefix} origin/master
- $ wget #{patch_url}
- $ git apply --3way #{ce_patch_name}
+ $ git fetch #{EE_REPO_URL} master
+ $ git checkout -b #{ee_branch_prefix} FETCH_HEAD
+ $ wget #{patch_url} && git apply --3way #{ce_patch_name}
At this point you might have conflicts such as:
diff --git a/lib/gitlab/email/handler/unsubscribe_handler.rb b/lib/gitlab/email/handler/unsubscribe_handler.rb
index 5894384da5d..ea80e21532e 100644
--- a/lib/gitlab/email/handler/unsubscribe_handler.rb
+++ b/lib/gitlab/email/handler/unsubscribe_handler.rb
@@ -16,6 +16,7 @@ module Gitlab
noteable = sent_notification.noteable
raise NoteableNotFoundError unless noteable
+
noteable.unsubscribe(sent_notification.recipient)
end
diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb
index 7b3483a7f96..582028493e9 100644
--- a/lib/gitlab/encoding_helper.rb
+++ b/lib/gitlab/encoding_helper.rb
@@ -14,9 +14,13 @@ module Gitlab
ENCODING_CONFIDENCE_THRESHOLD = 50
def encode!(message)
- return nil unless message.respond_to? :force_encoding
+ return nil unless message.respond_to?(:force_encoding)
+ return message if message.encoding == Encoding::UTF_8 && message.valid_encoding?
+
+ if message.respond_to?(:frozen?) && message.frozen?
+ message = message.dup
+ end
- # if message is utf-8 encoding, just return it
message.force_encoding("UTF-8")
return message if message.valid_encoding?
@@ -50,6 +54,9 @@ module Gitlab
end
def encode_utf8(message)
+ return nil unless message.is_a?(String)
+ return message if message.encoding == Encoding::UTF_8 && message.valid_encoding?
+
detect = CharlockHolmes::EncodingDetector.detect(message)
if detect && detect[:encoding]
begin
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index a8cb7fc3fe7..0e9ef4f897c 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -6,31 +6,33 @@ module Gitlab
module FileDetector
PATTERNS = {
# Project files
- readme: /\Areadme/i,
- changelog: /\A(changelog|history|changes|news)/i,
- license: /\A(licen[sc]e|copying)(\..+|\z)/i,
- contributing: /\Acontributing/i,
+ readme: /\Areadme[^\/]*\z/i,
+ changelog: /\A(changelog|history|changes|news)[^\/]*\z/i,
+ license: /\A(licen[sc]e|copying)(\.[^\/]+)?\z/i,
+ contributing: /\Acontributing[^\/]*\z/i,
version: 'version',
avatar: /\Alogo\.(png|jpg|gif)\z/,
+ issue_template: /\A\.gitlab\/issue_templates\/[^\/]+\.md\z/,
+ merge_request_template: /\A\.gitlab\/merge_request_templates\/[^\/]+\.md\z/,
# Configuration files
gitignore: '.gitignore',
koding: '.koding.yml',
gitlab_ci: '.gitlab-ci.yml',
- route_map: 'route-map.yml',
+ route_map: '.gitlab/route-map.yml',
# Dependency files
- cartfile: /\ACartfile/,
+ cartfile: /\ACartfile[^\/]*\z/,
composer_json: 'composer.json',
gemfile: /\A(Gemfile|gems\.rb)\z/,
gemfile_lock: 'Gemfile.lock',
- gemspec: /\.gemspec\z/,
+ gemspec: /\A[^\/]*\.gemspec\z/,
godeps_json: 'Godeps.json',
package_json: 'package.json',
podfile: 'Podfile',
- podspec_json: /\.podspec\.json\z/,
- podspec: /\.podspec\z/,
- requirements_txt: /requirements\.txt\z/,
+ podspec_json: /\A[^\/]*\.podspec\.json\z/,
+ podspec: /\A[^\/]*\.podspec\z/,
+ requirements_txt: /\A[^\/]*requirements\.txt\z/,
yarn_lock: 'yarn.lock'
}.freeze
@@ -63,13 +65,11 @@ module Gitlab
# type_of('README.md') # => :readme
# type_of('VERSION') # => :version
def self.type_of(path)
- name = File.basename(path)
-
PATTERNS.each do |type, search|
did_match = if search.is_a?(Regexp)
- name =~ search
+ path =~ search
else
- name.casecmp(search) == 0
+ path.casecmp(search) == 0
end
return type if did_match
diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb
index 2152182b37f..acb000e3e23 100644
--- a/lib/gitlab/fogbugz_import/client.rb
+++ b/lib/gitlab/fogbugz_import/client.rb
@@ -45,6 +45,7 @@ module Gitlab
project_name = repo(project_id).name
res = @api.command(:search, q: "project:'#{project_name}'", cols: 'ixPersonAssignedTo,ixPersonOpenedBy,ixPersonClosedBy,sStatus,sPriority,sCategory,fOpen,sTitle,sLatestTextSummary,dtOpened,dtClosed,dtResolved,dtLastUpdated,events')
return [] unless res['cases']['count'].to_i > 0
+
res['cases']['case']
end
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 3dcee681c72..5e426b13ade 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -18,6 +18,7 @@ module Gitlab
def execute
return true unless repo.valid?
+
client = Gitlab::FogbugzImport::Client.new(token: fb_session[:token], uri: fb_session[:uri])
@cases = client.cases(@repo.id.to_i)
@@ -206,6 +207,7 @@ module Gitlab
def format_content(raw_content)
return raw_content if raw_content.nil?
+
linkify_issues(escape_for_markdown(raw_content))
end
diff --git a/lib/gitlab/gcp/model.rb b/lib/gitlab/gcp/model.rb
deleted file mode 100644
index 195391f0e3c..00000000000
--- a/lib/gitlab/gcp/model.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Gitlab
- module Gcp
- module Model
- def table_name_prefix
- "gcp_"
- end
-
- def model_name
- @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
- end
- end
- end
-end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index c78fe63f9b5..1f31cdbc96d 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -66,6 +66,10 @@ module Gitlab
end
end
end
+
+ def diff_line_code(file_path, new_line_position, old_line_position)
+ "#{Digest::SHA1.hexdigest(file_path)}_#{old_line_position}_#{new_line_position}"
+ end
end
end
end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index a4336facee5..ddd52136bc4 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -12,6 +12,12 @@ module Gitlab
# blob data should use load_all_data!.
MAX_DATA_DISPLAY_SIZE = 10.megabytes
+ # These limits are used as a heuristic to ignore files which can't be LFS
+ # pointers. The format of these is described in
+ # https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md#the-pointer
+ LFS_POINTER_MIN_SIZE = 120.bytes
+ LFS_POINTER_MAX_SIZE = 200.bytes
+
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
class << self
@@ -30,16 +36,7 @@ module Gitlab
if is_enabled
Gitlab::GitalyClient::BlobService.new(repository).get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE)
else
- blob = repository.lookup(sha)
-
- next unless blob.is_a?(Rugged::Blob)
-
- new(
- id: blob.oid,
- size: blob.size,
- data: blob.content(MAX_DATA_DISPLAY_SIZE),
- binary: blob.binary?
- )
+ rugged_raw(repository, sha, limit: MAX_DATA_DISPLAY_SIZE)
end
end
end
@@ -59,10 +56,25 @@ module Gitlab
end
end
+ # Find LFS blobs given an array of sha ids
+ # Returns array of Gitlab::Git::Blob
+ # Does not guarantee blob data will be set
+ def batch_lfs_pointers(repository, blob_ids)
+ blob_ids.lazy
+ .select { |sha| possible_lfs_blob?(repository, sha) }
+ .map { |sha| rugged_raw(repository, sha, limit: LFS_POINTER_MAX_SIZE) }
+ .select(&:lfs_pointer?)
+ .force
+ end
+
def binary?(data)
EncodingHelper.detect_libgit2_binary?(data)
end
+ def size_could_be_lfs?(size)
+ size.between?(LFS_POINTER_MIN_SIZE, LFS_POINTER_MAX_SIZE)
+ end
+
private
# Recursive search of blob id by path
@@ -90,6 +102,7 @@ module Gitlab
if path_arr.size > 1
return nil unless entry[:type] == :tree
+
path_arr.shift
find_entry_by_path(repository, entry[:oid], path_arr.join('/'))
else
@@ -166,6 +179,31 @@ module Gitlab
)
end
end
+ rescue Rugged::ReferenceError
+ nil
+ end
+
+ def rugged_raw(repository, sha, limit:)
+ blob = repository.lookup(sha)
+
+ return unless blob.is_a?(Rugged::Blob)
+
+ new(
+ id: blob.oid,
+ size: blob.size,
+ data: blob.content(limit),
+ binary: blob.binary?
+ )
+ end
+
+ # Efficient lookup to determine if object size
+ # and type make it a possible LFS blob without loading
+ # blob content into memory with repository.lookup(sha)
+ def possible_lfs_blob?(repository, sha)
+ object_header = repository.rugged.read_header(sha)
+
+ object_header[:type] == :blob &&
+ size_could_be_lfs?(object_header[:len])
end
end
@@ -226,7 +264,7 @@ module Gitlab
# size
# see https://github.com/github/git-lfs/blob/v1.1.0/docs/spec.md#the-pointer
def lfs_pointer?
- has_lfs_version_key? && lfs_oid.present? && lfs_size.present?
+ self.class.size_could_be_lfs?(size) && has_lfs_version_key? && lfs_oid.present? && lfs_size.present?
end
def lfs_oid
diff --git a/lib/gitlab/git/branch.rb b/lib/gitlab/git/branch.rb
index c53882787f1..3487e099381 100644
--- a/lib/gitlab/git/branch.rb
+++ b/lib/gitlab/git/branch.rb
@@ -3,6 +3,14 @@
module Gitlab
module Git
class Branch < Ref
+ def self.find(repo, branch_name)
+ if branch_name.is_a?(Gitlab::Git::Branch)
+ branch_name
+ else
+ repo.find_branch(branch_name)
+ end
+ end
+
def initialize(repository, name, target, target_commit)
super(repository, name, target, target_commit)
end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 1957c254c28..d5518814483 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -72,7 +72,8 @@ module Gitlab
decorate(repo, commit) if commit
rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError,
- Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository
+ Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository,
+ Rugged::OdbError, Rugged::TreeError, ArgumentError
nil
end
diff --git a/lib/gitlab/git/conflict/file.rb b/lib/gitlab/git/conflict/file.rb
new file mode 100644
index 00000000000..fc1595f1faf
--- /dev/null
+++ b/lib/gitlab/git/conflict/file.rb
@@ -0,0 +1,86 @@
+module Gitlab
+ module Git
+ module Conflict
+ class File
+ attr_reader :content, :their_path, :our_path, :our_mode, :repository
+
+ def initialize(repository, commit_oid, conflict, content)
+ @repository = repository
+ @commit_oid = commit_oid
+ @their_path = conflict[:theirs][:path]
+ @our_path = conflict[:ours][:path]
+ @our_mode = conflict[:ours][:mode]
+ @content = content
+ end
+
+ def lines
+ return @lines if defined?(@lines)
+
+ begin
+ @type = 'text'
+ @lines = Gitlab::Git::Conflict::Parser.parse(content,
+ our_path: our_path,
+ their_path: their_path)
+ rescue Gitlab::Git::Conflict::Parser::ParserError
+ @type = 'text-editor'
+ @lines = nil
+ end
+ end
+
+ def type
+ lines unless @type
+
+ @type.inquiry
+ end
+
+ def our_blob
+ # REFACTOR NOTE: the source of `commit_oid` used to be
+ # `merge_request.diff_refs.head_sha`. Instead of passing this value
+ # around the new lib structure, I decided to use `@commit_oid` which is
+ # equivalent to `merge_request.source_branch_head.raw.rugged_commit.oid`.
+ # That is what `merge_request.diff_refs.head_sha` is equivalent to when
+ # `merge_request` is not persisted (see `MergeRequest#diff_head_commit`).
+ # I think using the same oid is more consistent anyways, but if Conflicts
+ # start breaking, the change described above is a good place to look at.
+ @our_blob ||= repository.blob_at(@commit_oid, our_path)
+ end
+
+ def line_code(line)
+ Gitlab::Git.diff_line_code(our_path, line[:line_new], line[:line_old])
+ end
+
+ def resolve_lines(resolution)
+ section_id = nil
+
+ lines.map do |line|
+ unless line[:type]
+ section_id = nil
+ next line
+ end
+
+ section_id ||= line_code(line)
+
+ case resolution[section_id]
+ when 'head'
+ next unless line[:type] == 'new'
+ when 'origin'
+ next unless line[:type] == 'old'
+ else
+ raise Gitlab::Git::Conflict::Resolver::ResolutionError, "Missing resolution for section ID: #{section_id}"
+ end
+
+ line
+ end.compact
+ end
+
+ def resolve_content(resolution)
+ if resolution == content
+ raise Gitlab::Git::Conflict::Resolver::ResolutionError, "Resolved content has no changes for file #{our_path}"
+ end
+
+ resolution
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/conflict/parser.rb b/lib/gitlab/git/conflict/parser.rb
new file mode 100644
index 00000000000..3effa9d2d31
--- /dev/null
+++ b/lib/gitlab/git/conflict/parser.rb
@@ -0,0 +1,91 @@
+module Gitlab
+ module Git
+ module Conflict
+ class Parser
+ UnresolvableError = Class.new(StandardError)
+ UnmergeableFile = Class.new(UnresolvableError)
+ UnsupportedEncoding = Class.new(UnresolvableError)
+
+ # Recoverable errors - the conflict can be resolved in an editor, but not with
+ # sections.
+ ParserError = Class.new(StandardError)
+ UnexpectedDelimiter = Class.new(ParserError)
+ MissingEndDelimiter = Class.new(ParserError)
+
+ class << self
+ def parse(text, our_path:, their_path:, parent_file: nil)
+ validate_text!(text)
+
+ line_obj_index = 0
+ line_old = 1
+ line_new = 1
+ type = nil
+ lines = []
+ conflict_start = "<<<<<<< #{our_path}"
+ conflict_middle = '======='
+ conflict_end = ">>>>>>> #{their_path}"
+
+ text.each_line.map do |line|
+ full_line = line.delete("\n")
+
+ if full_line == conflict_start
+ validate_delimiter!(type.nil?)
+
+ type = 'new'
+ elsif full_line == conflict_middle
+ validate_delimiter!(type == 'new')
+
+ type = 'old'
+ elsif full_line == conflict_end
+ validate_delimiter!(type == 'old')
+
+ type = nil
+ elsif line[0] == '\\'
+ type = 'nonewline'
+ lines << {
+ full_line: full_line,
+ type: type,
+ line_obj_index: line_obj_index,
+ line_old: line_old,
+ line_new: line_new
+ }
+ else
+ lines << {
+ full_line: full_line,
+ type: type,
+ line_obj_index: line_obj_index,
+ line_old: line_old,
+ line_new: line_new
+ }
+
+ line_old += 1 if type != 'new'
+ line_new += 1 if type != 'old'
+
+ line_obj_index += 1
+ end
+ end
+
+ raise MissingEndDelimiter unless type.nil?
+
+ lines
+ end
+
+ private
+
+ def validate_text!(text)
+ raise UnmergeableFile if text.blank? # Typically a binary file
+ raise UnmergeableFile if text.length > 200.kilobytes
+
+ text.force_encoding('UTF-8')
+
+ raise UnsupportedEncoding unless text.valid_encoding?
+ end
+
+ def validate_delimiter!(condition)
+ raise UnexpectedDelimiter unless condition
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/conflict/resolver.rb b/lib/gitlab/git/conflict/resolver.rb
new file mode 100644
index 00000000000..df509c5f4ce
--- /dev/null
+++ b/lib/gitlab/git/conflict/resolver.rb
@@ -0,0 +1,91 @@
+module Gitlab
+ module Git
+ module Conflict
+ class Resolver
+ ConflictSideMissing = Class.new(StandardError)
+ ResolutionError = Class.new(StandardError)
+
+ def initialize(repository, our_commit, target_repository, their_commit)
+ @repository = repository
+ @our_commit = our_commit.rugged_commit
+ @target_repository = target_repository
+ @their_commit = their_commit.rugged_commit
+ end
+
+ def conflicts
+ @conflicts ||= begin
+ target_index = @target_repository.rugged.merge_commits(@our_commit, @their_commit)
+
+ # We don't need to do `with_repo_branch_commit` here, because the target
+ # project always fetches source refs when creating merge request diffs.
+ target_index.conflicts.map do |conflict|
+ raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]
+
+ Gitlab::Git::Conflict::File.new(
+ @target_repository,
+ @our_commit.oid,
+ conflict,
+ target_index.merge_file(conflict[:ours][:path])[:data]
+ )
+ end
+ end
+ end
+
+ def resolve_conflicts(user, files, source_branch:, target_branch:, commit_message:)
+ @repository.with_repo_branch_commit(@target_repository, target_branch) do
+ files.each do |file_params|
+ conflict_file = conflict_for_path(file_params[:old_path], file_params[:new_path])
+
+ write_resolved_file_to_index(conflict_file, file_params)
+ end
+
+ unless index.conflicts.empty?
+ missing_files = index.conflicts.map { |file| file[:ours][:path] }
+
+ raise ResolutionError, "Missing resolutions for the following files: #{missing_files.join(', ')}"
+ end
+
+ commit_params = {
+ message: commit_message,
+ parents: [@our_commit, @their_commit].map(&:oid)
+ }
+
+ @repository.commit_index(user, source_branch, index, commit_params)
+ end
+ end
+
+ def conflict_for_path(old_path, new_path)
+ conflicts.find do |conflict|
+ conflict.their_path == old_path && conflict.our_path == new_path
+ end
+ end
+
+ private
+
+ # We can only write when getting the merge index from the source
+ # project, because we will write to that project. We don't use this all
+ # the time because this fetches a ref into the source project, which
+ # isn't needed for reading.
+ def index
+ @index ||= @repository.rugged.merge_commits(@our_commit, @their_commit)
+ end
+
+ def write_resolved_file_to_index(file, params)
+ if params[:sections]
+ resolved_lines = file.resolve_lines(params[:sections])
+ new_file = resolved_lines.map { |line| line[:full_line] }.join("\n")
+
+ new_file << "\n" if file.our_blob.data.ends_with?("\n")
+ elsif params[:content]
+ new_file = file.resolve_content(params[:content])
+ end
+
+ our_path = file.our_path
+
+ index.add(path: our_path, oid: @repository.rugged.write(new_file, :blob), mode: file.our_mode)
+ index.conflict_remove(our_path)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/env.rb b/lib/gitlab/git/env.rb
index f80193ac553..9d0b47a1a6d 100644
--- a/lib/gitlab/git/env.rb
+++ b/lib/gitlab/git/env.rb
@@ -11,9 +11,11 @@ module Gitlab
#
# This class is thread-safe via RequestStore.
class Env
- WHITELISTED_GIT_VARIABLES = %w[
+ WHITELISTED_VARIABLES = %w[
GIT_OBJECT_DIRECTORY
+ GIT_OBJECT_DIRECTORY_RELATIVE
GIT_ALTERNATE_OBJECT_DIRECTORIES
+ GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
].freeze
def self.set(env)
@@ -28,12 +30,23 @@ module Gitlab
RequestStore.fetch(:gitlab_git_env) { {} }
end
+ def self.to_env_hash
+ env = {}
+
+ all.compact.each do |key, value|
+ value = value.join(File::PATH_SEPARATOR) if value.is_a?(Array)
+ env[key.to_s] = value
+ end
+
+ env
+ end
+
def self.[](key)
all[key]
end
def self.whitelist_git_env(env)
- env.select { |key, _| WHITELISTED_GIT_VARIABLES.include?(key.to_s) }.with_indifferent_access
+ env.select { |key, _| WHITELISTED_VARIABLES.include?(key.to_s) }.with_indifferent_access
end
end
end
diff --git a/lib/gitlab/git/hooks_service.rb b/lib/gitlab/git/hooks_service.rb
index c327e9b1616..f302b852b35 100644
--- a/lib/gitlab/git/hooks_service.rb
+++ b/lib/gitlab/git/hooks_service.rb
@@ -8,7 +8,7 @@ module Gitlab
def execute(pusher, repository, oldrev, newrev, ref)
@repository = repository
@gl_id = pusher.gl_id
- @gl_username = pusher.name
+ @gl_username = pusher.username
@oldrev = oldrev
@newrev = newrev
@ref = ref
diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb
new file mode 100644
index 00000000000..732dd5d998a
--- /dev/null
+++ b/lib/gitlab/git/lfs_changes.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Git
+ class LfsChanges
+ def initialize(repository, newrev)
+ @repository = repository
+ @newrev = newrev
+ end
+
+ def new_pointers(object_limit: nil, not_in: nil)
+ @new_pointers ||= begin
+ rev_list.new_objects(not_in: not_in, require_path: true) do |object_ids|
+ object_ids = object_ids.take(object_limit) if object_limit
+
+ Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids)
+ end
+ end
+ end
+
+ def all_pointers
+ rev_list.all_objects(require_path: true) do |object_ids|
+ Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids)
+ end
+ end
+
+ private
+
+ def rev_list
+ ::Gitlab::Git::RevList.new(path_to_repo: @repository.path_to_repo,
+ newrev: @newrev)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb
index d835dcca8ba..e36d5410431 100644
--- a/lib/gitlab/git/operation_service.rb
+++ b/lib/gitlab/git/operation_service.rb
@@ -3,9 +3,17 @@ module Gitlab
class OperationService
include Gitlab::Git::Popen
- WithBranchResult = Struct.new(:newrev, :repo_created, :branch_created) do
+ BranchUpdate = Struct.new(:newrev, :repo_created, :branch_created) do
alias_method :repo_created?, :repo_created
alias_method :branch_created?, :branch_created
+
+ def self.from_gitaly(branch_update)
+ new(
+ branch_update.commit_id,
+ branch_update.repo_created,
+ branch_update.branch_created
+ )
+ end
end
attr_reader :user, :repository
@@ -64,7 +72,7 @@ module Gitlab
# 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,
+ # If `start_repository` 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,
@@ -72,15 +80,13 @@ module Gitlab
start_repository: repository,
&block)
- # Refactoring aid
- unless start_repository.is_a?(Gitlab::Git::Repository)
- raise "expected a Gitlab::Git::Repository, got #{start_repository}"
- end
+ Gitlab::Git.check_namespace!(start_repository)
+ start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)
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}"
+ raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.relative_path}"
end
update_branch_with_hooks(branch_name) do
@@ -112,7 +118,7 @@ module Gitlab
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
update_ref_in_hooks(ref, newrev, oldrev)
- WithBranchResult.new(newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev))
+ BranchUpdate.new(newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev))
end
def find_oldrev_from_branch(newrev, branch)
diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb
index 3d2fc471d28..d41fe78daa1 100644
--- a/lib/gitlab/git/popen.rb
+++ b/lib/gitlab/git/popen.rb
@@ -5,7 +5,9 @@ require 'open3'
module Gitlab
module Git
module Popen
- def popen(cmd, path, vars = {})
+ FAST_GIT_PROCESS_TIMEOUT = 15.seconds
+
+ def popen(cmd, path, vars = {}, lazy_block: nil)
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
@@ -20,13 +22,79 @@ module Gitlab
yield(stdin) if block_given?
stdin.close
- @cmd_output << stdout.read
+ if lazy_block
+ return lazy_block.call(stdout.lazy)
+ else
+ @cmd_output << stdout.read
+ end
+
@cmd_output << stderr.read
@cmd_status = wait_thr.value.exitstatus
end
[@cmd_output, @cmd_status]
end
+
+ def popen_with_timeout(cmd, timeout, path, vars = {})
+ unless cmd.is_a?(Array)
+ raise "System commands must be given as an array of strings"
+ end
+
+ path ||= Dir.pwd
+ vars['PWD'] = path
+
+ unless File.directory?(path)
+ FileUtils.mkdir_p(path)
+ end
+
+ rout, wout = IO.pipe
+ rerr, werr = IO.pipe
+
+ pid = Process.spawn(vars, *cmd, out: wout, err: werr, chdir: path, pgroup: true)
+
+ begin
+ status = process_wait_with_timeout(pid, timeout)
+
+ # close write ends so we could read them
+ wout.close
+ werr.close
+
+ cmd_output = rout.readlines.join
+ cmd_output << rerr.readlines.join # Copying the behaviour of `popen` which merges stderr into output
+
+ [cmd_output, status.exitstatus]
+ rescue Timeout::Error => e
+ kill_process_group_for_pid(pid)
+
+ raise e
+ ensure
+ wout.close unless wout.closed?
+ werr.close unless werr.closed?
+
+ rout.close
+ rerr.close
+ end
+ end
+
+ def process_wait_with_timeout(pid, timeout)
+ deadline = timeout.seconds.from_now
+ wait_time = 0.01
+
+ while deadline > Time.now
+ sleep(wait_time)
+ _, status = Process.wait2(pid, Process::WNOHANG)
+
+ return status unless status.nil?
+ end
+
+ raise Timeout::Error, "Timeout waiting for process ##{pid}"
+ end
+
+ def kill_process_group_for_pid(pid)
+ Process.kill("KILL", -pid)
+ Process.wait(pid)
+ rescue Errno::ESRCH
+ end
end
end
end
diff --git a/lib/gitlab/git/remote_repository.rb b/lib/gitlab/git/remote_repository.rb
new file mode 100644
index 00000000000..3685aa20669
--- /dev/null
+++ b/lib/gitlab/git/remote_repository.rb
@@ -0,0 +1,82 @@
+module Gitlab
+ module Git
+ #
+ # When a Gitaly call involves two repositories instead of one we cannot
+ # assume that both repositories are on the same Gitaly server. In this
+ # case we need to make a distinction between the repository that the
+ # call is being made on (a Repository instance), and the "other"
+ # repository (a RemoteRepository instance). This is the reason why we
+ # have the RemoteRepository class in Gitlab::Git.
+ #
+ # When you make changes, be aware that gitaly-ruby sub-classes this
+ # class.
+ #
+ class RemoteRepository
+ attr_reader :path, :relative_path, :gitaly_repository
+
+ def initialize(repository)
+ @relative_path = repository.relative_path
+ @gitaly_repository = repository.gitaly_repository
+
+ # These instance variables will not be available in gitaly-ruby, where
+ # we have no disk access to this repository.
+ @repository = repository
+ @path = repository.path
+ end
+
+ def empty_repo?
+ # We will override this implementation in gitaly-ruby because we cannot
+ # use '@repository' there.
+ @repository.empty_repo?
+ end
+
+ def commit_id(revision)
+ # We will override this implementation in gitaly-ruby because we cannot
+ # use '@repository' there.
+ @repository.commit(revision)&.sha
+ end
+
+ def branch_exists?(name)
+ # We will override this implementation in gitaly-ruby because we cannot
+ # use '@repository' there.
+ @repository.branch_exists?(name)
+ end
+
+ # Compares self to a Gitlab::Git::Repository. This implementation uses
+ # 'self.gitaly_repository' so that it will also work in the
+ # GitalyRemoteRepository subclass defined in gitaly-ruby.
+ def same_repository?(other_repository)
+ gitaly_repository.storage_name == other_repository.storage &&
+ gitaly_repository.relative_path == other_repository.relative_path
+ end
+
+ def fetch_env
+ gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh'))
+ gitaly_address = gitaly_client.address(storage)
+ gitaly_token = gitaly_client.token(storage)
+
+ request = Gitaly::SSHUploadPackRequest.new(repository: gitaly_repository)
+ env = {
+ 'GITALY_ADDRESS' => gitaly_address,
+ 'GITALY_PAYLOAD' => request.to_json,
+ 'GITALY_WD' => Dir.pwd,
+ 'GIT_SSH_COMMAND' => "#{gitaly_ssh} upload-pack"
+ }
+ env['GITALY_TOKEN'] = gitaly_token if gitaly_token.present?
+
+ env
+ end
+
+ private
+
+ # Must return an object that responds to 'address' and 'storage'.
+ def gitaly_client
+ Gitlab::GitalyClient
+ end
+
+ def storage
+ gitaly_repository.storage_name
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 0f059bef808..fb9c3e92d3f 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -6,12 +6,17 @@ require "rubygems/package"
module Gitlab
module Git
class Repository
+ include Gitlab::Git::RepositoryMirroring
include Gitlab::Git::Popen
ALLOWED_OBJECT_DIRECTORIES_VARIABLES = %w[
GIT_OBJECT_DIRECTORY
GIT_ALTERNATE_OBJECT_DIRECTORIES
].freeze
+ ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES = %w[
+ GIT_OBJECT_DIRECTORY_RELATIVE
+ GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
+ ].freeze
SEARCH_CONTEXT_LINES = 3
NoRepository = Class.new(StandardError)
@@ -53,7 +58,7 @@ module Gitlab
# Rugged repo object
attr_reader :rugged
- attr_reader :storage, :gl_repository, :relative_path, :gitaly_resolver
+ attr_reader :storage, :gl_repository, :relative_path
# This initializer method is only used on the client side (gitlab-ce).
# Gitaly-ruby uses a different initializer.
@@ -61,7 +66,6 @@ module Gitlab
@storage = storage
@relative_path = relative_path
@gl_repository = gl_repository
- @gitaly_resolver = Gitlab::GitalyClient
storage_path = Gitlab.config.repositories.storages[@storage]['path']
@path = File.join(storage_path, @relative_path)
@@ -100,7 +104,7 @@ module Gitlab
end
def exists?
- Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
+ Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
gitaly_repository_client.exists?
else
@@ -162,7 +166,7 @@ module Gitlab
end
def local_branches(sort_by: nil)
- gitaly_migrate(:local_branches) do |is_enabled|
+ gitaly_migrate(:local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_ref_client.local_branches(sort_by: sort_by)
else
@@ -193,7 +197,7 @@ module Gitlab
def has_local_branches?
gitaly_migrate(:has_local_branches) do |is_enabled|
if is_enabled
- gitaly_ref_client.has_local_branches?
+ gitaly_repository_client.has_local_branches?
else
has_local_branches_rugged?
end
@@ -286,13 +290,27 @@ module Gitlab
end
end
+ def batch_existence(object_ids, existing: true)
+ filter_method = existing ? :select : :reject
+
+ object_ids.public_send(filter_method) do |oid| # rubocop:disable GitlabSecurity/PublicSend
+ rugged.exists?(oid)
+ end
+ end
+
# Returns an Array of branch and tag names
def ref_names
branch_names + tag_names
end
def delete_all_refs_except(prefixes)
- delete_refs(*all_ref_names_except(prefixes))
+ gitaly_migrate(:ref_delete_refs) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.delete_refs(except_with_prefixes: prefixes)
+ else
+ delete_refs(*all_ref_names_except(prefixes))
+ end
+ end
end
# Returns an Array of all ref names, except when it's matching pattern
@@ -487,7 +505,7 @@ module Gitlab
# Counts the amount of commits between `from` and `to`.
def count_commits_between(from, to)
- Commit.between(self, from, to).size
+ count_commits(ref: "#{from}..#{to}")
end
# Returns the SHA of the most recent common ancestor of +from+ and +to+
@@ -507,6 +525,10 @@ module Gitlab
gitaly_commit_client.ancestor?(from, to)
end
+ def merged_branch_names(branch_names = [])
+ Set.new(git_merged_branch_names(branch_names))
+ end
+
# Return an array of Diff objects that represent the diff
# between +from+ and +to+. See Diff::filter_diff_options for the allowed
# diff options. The +options+ hash can also include :break_rewrites to
@@ -700,7 +722,17 @@ module Gitlab
tags.find { |tag| tag.name == name }
end
- def merge(user, source_sha, target_branch, message)
+ def merge(user, source_sha, target_branch, message, &block)
+ gitaly_migrate(:operation_user_merge_branch) do |is_enabled|
+ if is_enabled
+ gitaly_operation_client.user_merge_branch(user, source_sha, target_branch, message, &block)
+ else
+ rugged_merge(user, source_sha, target_branch, message, &block)
+ end
+ end
+ end
+
+ def rugged_merge(user, source_sha, target_branch, message)
committer = Gitlab::Git.committer_hash(email: user.email, name: user.name)
OperationService.new(user, self).with_branch(target_branch) do |start_commit|
@@ -731,6 +763,16 @@ module Gitlab
nil
end
+ def ff_merge(user, source_sha, target_branch)
+ gitaly_migrate(:operation_user_ff_branch) do |is_enabled|
+ if is_enabled
+ gitaly_ff_merge(user, source_sha, target_branch)
+ else
+ rugged_ff_merge(user, source_sha, target_branch)
+ end
+ end
+ end
+
def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch(
branch_name,
@@ -862,16 +904,30 @@ module Gitlab
end
end
- # Delete the specified remote from this repository.
- def remote_delete(remote_name)
+ def add_remote(remote_name, url)
+ rugged.remotes.create(remote_name, url)
+ rescue Rugged::ConfigError
+ remote_update(remote_name, url: url)
+ end
+
+ def remove_remote(remote_name)
+ # When a remote is deleted all its remote refs are deleted too, but in
+ # the case of mirrors we map its refs (that would usualy go under
+ # [remote_name]/) to the top level namespace. We clean the mapping so
+ # those don't get deleted.
+ if rugged.config["remote.#{remote_name}.mirror"]
+ rugged.config.delete("remote.#{remote_name}.fetch")
+ end
+
rugged.remotes.delete(remote_name)
- nil
+ true
+ rescue Rugged::ConfigError
+ false
end
- # Add a new remote to this repository.
- def remote_add(remote_name, url)
- rugged.remotes.create(remote_name, url)
- nil
+ # Returns true if a remote exists.
+ def remote_exists?(name)
+ rugged.remotes[name].present?
end
# Update the specified remote using the values in the +options+ hash
@@ -934,6 +990,10 @@ module Gitlab
@attributes.attributes(path)
end
+ def gitattribute(path, name)
+ attributes(path)[name]
+ end
+
def languages(ref = nil)
Gitlab::GitalyClient.migrate(:commit_languages) do |is_enabled|
if is_enabled
@@ -963,23 +1023,22 @@ module Gitlab
def with_repo_branch_commit(start_repository, start_branch_name)
Gitlab::Git.check_namespace!(start_repository)
+ start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)
return yield nil if start_repository.empty_repo?
- if start_repository == self
+ if start_repository.same_repository?(self)
yield commit(start_branch_name)
else
- start_commit = start_repository.commit(start_branch_name)
-
- return yield nil unless start_commit
+ start_commit_id = start_repository.commit_id(start_branch_name)
- sha = start_commit.sha
+ return yield nil unless start_commit_id
- if branch_commit = commit(sha)
+ if branch_commit = commit(start_commit_id)
yield branch_commit
else
with_repo_tmp_commit(
- start_repository, start_branch_name, sha) do |tmp_commit|
+ start_repository, start_branch_name, start_commit_id) do |tmp_commit|
yield tmp_commit
end
end
@@ -987,9 +1046,15 @@ module Gitlab
end
def with_repo_tmp_commit(start_repository, start_branch_name, sha)
+ source_ref = start_branch_name
+
+ unless Gitlab::Git.branch_ref?(source_ref)
+ source_ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_ref}"
+ end
+
tmp_ref = fetch_ref(
start_repository,
- source_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
+ source_ref: source_ref,
target_ref: "refs/tmp/#{SecureRandom.hex}"
)
@@ -998,13 +1063,12 @@ module Gitlab
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)
- true
+ def fetch_source_branch!(source_repository, source_branch, local_ref)
+ Gitlab::GitalyClient.migrate(:fetch_source_branch) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.fetch_source_branch(source_repository, source_branch, local_ref)
else
- false
+ rugged_fetch_source_branch(source_repository, source_branch, local_ref)
end
end
end
@@ -1036,6 +1100,9 @@ module Gitlab
end
def fetch_ref(source_repository, source_ref:, target_ref:)
+ Gitlab::Git.check_namespace!(source_repository)
+ source_repository = RemoteRepository.new(source_repository) unless source_repository.is_a?(RemoteRepository)
+
message, status = GitalyClient.migrate(:fetch_ref) do |is_enabled|
if is_enabled
gitaly_fetch_ref(source_repository, source_ref: source_ref, target_ref: target_ref)
@@ -1058,6 +1125,13 @@ module Gitlab
end
# Refactoring aid; allows us to copy code from app/models/repository.rb
+ def run_git_with_timeout(args, timeout, env: {})
+ circuit_breaker.perform do
+ popen_with_timeout([Gitlab.config.git.bin_path, *args], timeout, path, env)
+ 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
@@ -1082,6 +1156,37 @@ module Gitlab
@has_visible_content = has_local_branches?
end
+ # Like all public `Gitlab::Git::Repository` methods, this method is part
+ # of `Repository`'s interface through `method_missing`.
+ # `Repository` has its own `fetch_remote` which uses `gitlab-shell` and
+ # takes some extra attributes, so we qualify this method name to prevent confusion.
+ def fetch_remote_without_shell(remote = 'origin')
+ run_git(['fetch', remote]).last.zero?
+ end
+
+ def blob_at(sha, path)
+ Gitlab::Git::Blob.find(self, sha, path) unless Gitlab::Git.blank_ref?(sha)
+ end
+
+ # Items should be of format [[commit_id, path], [commit_id1, path1]]
+ def batch_blobs(items, blob_size_limit: nil)
+ Gitlab::Git::Blob.batch(self, items, blob_size_limit: blob_size_limit)
+ end
+
+ def commit_index(user, branch_name, index, options)
+ committer = user_to_committer(user)
+
+ OperationService.new(user, self).with_branch(branch_name) do
+ commit_params = options.merge(
+ tree: index.write_tree(rugged),
+ author: committer,
+ committer: committer
+ )
+
+ create_commit(commit_params)
+ end
+ end
+
def gitaly_repository
Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository)
end
@@ -1110,14 +1215,25 @@ module Gitlab
Gitlab::GitalyClient.migrate(method, status: status, &block)
rescue GRPC::NotFound => e
raise NoRepository.new(e)
- rescue GRPC::BadStatus => e
- raise CommandError.new(e)
rescue GRPC::InvalidArgument => e
raise ArgumentError.new(e)
+ rescue GRPC::BadStatus => e
+ raise CommandError.new(e)
end
private
+ def rugged_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)
+ true
+ else
+ false
+ end
+ end
+ end
+
# Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'.
def branches_filter(filter: nil, sort_by: nil)
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37464
@@ -1135,6 +1251,23 @@ module Gitlab
sort_branches(branches, sort_by)
end
+ # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/695
+ def git_merged_branch_names(branch_names = [])
+ root_sha = find_branch(root_ref).target
+
+ git_arguments =
+ %W[branch --merged #{root_sha}
+ --format=%(refname:short)\ %(objectname)] + branch_names
+
+ lines = run_git(git_arguments).first.lines
+
+ lines.each_with_object([]) do |line, branches|
+ name, sha = line.strip.split(' ', 2)
+
+ branches << name if sha != root_sha
+ end
+ end
+
def log_using_shell?(options)
options[:path].present? ||
options[:disable_walk] ||
@@ -1220,7 +1353,16 @@ module Gitlab
end
def alternate_object_directories
- Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES).compact
+ relative_paths = Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact
+
+ if relative_paths.any?
+ relative_paths.map { |d| File.join(path, d) }
+ else
+ Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES)
+ .flatten
+ .compact
+ .flat_map { |d| d.split(File::PATH_SEPARATOR) }
+ end
end
# Get the content of a blob for a given commit. If the blob is a commit
@@ -1277,6 +1419,7 @@ module Gitlab
end
return nil unless tmp_entry.type == :tree
+
tmp_entry = tmp_entry[dir]
end
end
@@ -1397,6 +1540,7 @@ module Gitlab
# Ref names must start with `refs/`.
def rugged_ref_exists?(ref_name)
raise ArgumentError, 'invalid refname' unless ref_name.start_with?('refs/')
+
rugged.references.exist?(ref_name)
rescue Rugged::ReferenceError
false
@@ -1463,6 +1607,7 @@ module Gitlab
Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
rescue Rugged::ReferenceError => e
raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/
+
raise InvalidRef.new("Invalid reference #{start_point}")
end
@@ -1522,22 +1667,25 @@ module Gitlab
end
def gitaly_fetch_ref(source_repository, source_ref:, target_ref:)
- gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh'))
- gitaly_address = gitaly_resolver.address(source_repository.storage)
- gitaly_token = gitaly_resolver.token(source_repository.storage)
-
- request = Gitaly::SSHUploadPackRequest.new(repository: source_repository.gitaly_repository)
- env = {
- 'GITALY_ADDRESS' => gitaly_address,
- 'GITALY_PAYLOAD' => request.to_json,
- 'GITALY_WD' => Dir.pwd,
- 'GIT_SSH_COMMAND' => "#{gitaly_ssh} upload-pack"
- }
- env['GITALY_TOKEN'] = gitaly_token if gitaly_token.present?
-
args = %W(fetch --no-tags -f ssh://gitaly/internal.git #{source_ref}:#{target_ref})
- run_git(args, env: env)
+ run_git(args, env: source_repository.fetch_env)
+ end
+
+ def gitaly_ff_merge(user, source_sha, target_branch)
+ gitaly_operations_client.user_ff_branch(user, source_sha, target_branch)
+ rescue GRPC::FailedPrecondition => e
+ raise CommitError, e
+ end
+
+ def rugged_ff_merge(user, source_sha, target_branch)
+ OperationService.new(user, self).with_branch(target_branch) do |our_commit|
+ raise ArgumentError, 'Invalid merge target' unless our_commit
+
+ source_sha
+ end
+ rescue Rugged::ReferenceError
+ raise ArgumentError, 'Invalid merge source'
end
end
end
diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb
new file mode 100644
index 00000000000..392bef69e80
--- /dev/null
+++ b/lib/gitlab/git/repository_mirroring.rb
@@ -0,0 +1,104 @@
+module Gitlab
+ module Git
+ module RepositoryMirroring
+ REFMAPS = {
+ # With `:all_refs`, the repository is equivalent to the result of `git clone --mirror`
+ all_refs: '+refs/*:refs/*',
+ heads: '+refs/heads/*:refs/heads/*',
+ tags: '+refs/tags/*:refs/tags/*'
+ }.freeze
+
+ RemoteError = Class.new(StandardError)
+
+ def set_remote_as_mirror(remote_name, refmap: :all_refs)
+ set_remote_refmap(remote_name, refmap)
+
+ rugged.config["remote.#{remote_name}.mirror"] = true
+ rugged.config["remote.#{remote_name}.prune"] = true
+ end
+
+ def set_remote_refmap(remote_name, refmap)
+ Array(refmap).each_with_index do |refspec, i|
+ refspec = REFMAPS[refspec] || refspec
+
+ # We need multiple `fetch` entries, but Rugged only allows replacing a config, not adding to it.
+ # To make sure we start from scratch, we set the first using rugged, and use `git` for any others
+ if i == 0
+ rugged.config["remote.#{remote_name}.fetch"] = refspec
+ else
+ run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}])
+ end
+ end
+ end
+
+ # Like all_refs public `Gitlab::Git::Repository` methods, this method is part
+ # of `Repository`'s interface through `method_missing`.
+ # `Repository` has its own `fetch_as_mirror` which uses `gitlab-shell` and
+ # takes some extra attributes, so we qualify this method name to prevent confusion.
+ def fetch_as_mirror_without_shell(url)
+ remote_name = "tmp-#{SecureRandom.hex}"
+ add_remote(remote_name, url)
+ set_remote_as_mirror(remote_name)
+ fetch_remote_without_shell(remote_name)
+ ensure
+ remove_remote(remote_name) if remote_name
+ end
+
+ def remote_tags(remote)
+ # Each line has this format: "dc872e9fa6963f8f03da6c8f6f264d0845d6b092\trefs/tags/v1.10.0\n"
+ # We want to convert it to: [{ 'v1.10.0' => 'dc872e9fa6963f8f03da6c8f6f264d0845d6b092' }, ...]
+ list_remote_tags(remote).map do |line|
+ target, path = line.strip.split("\t")
+
+ # When the remote repo does not have tags.
+ if target.nil? || path.nil?
+ Rails.logger.info "Empty or invalid list of tags for remote: #{remote}. Output: #{output}"
+ return []
+ end
+
+ name = path.split('/', 3).last
+ # We're only interested in tag references
+ # See: http://stackoverflow.com/questions/15472107/when-listing-git-ls-remote-why-theres-after-the-tag-name
+ next if name =~ /\^\{\}\Z/
+
+ target_commit = Gitlab::Git::Commit.find(self, target)
+ Gitlab::Git::Tag.new(self, name, target, target_commit)
+ end.compact
+ end
+
+ def remote_branches(remote_name)
+ branches = []
+
+ rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref|
+ name = ref.name.sub(/\Arefs\/remotes\/#{remote_name}\//, '')
+
+ begin
+ target_commit = Gitlab::Git::Commit.find(self, ref.target)
+ branches << Gitlab::Git::Branch.new(self, name, ref.target, target_commit)
+ rescue Rugged::ReferenceError
+ # Omit invalid branch
+ end
+ end
+
+ branches
+ end
+
+ private
+
+ def list_remote_tags(remote)
+ tag_list, exit_code, error = nil
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-remote --tags #{remote})
+
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
+ tag_list = stdout.read
+ error = stderr.read
+ exit_code = wait_thr.value.exitstatus
+ end
+
+ raise RemoteError, error unless exit_code.zero?
+
+ tag_list.split("\n")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
index 92a6a672534..4974205b8fd 100644
--- a/lib/gitlab/git/rev_list.rb
+++ b/lib/gitlab/git/rev_list.rb
@@ -13,11 +13,32 @@ module Gitlab
@path_to_repo = path_to_repo
end
- # This method returns an array of new references
+ # This method returns an array of new commit references
def new_refs
execute([*base_args, newrev, '--not', '--all'])
end
+ # Finds newly added objects
+ # Returns an array of shas
+ #
+ # Can skip objects which do not have a path using required_path: true
+ # This skips commit objects and root trees, which might not be needed when
+ # looking for blobs
+ #
+ # When given a block it will yield objects as a lazy enumerator so
+ # the caller can limit work done instead of processing megabytes of data
+ def new_objects(require_path: nil, not_in: nil, &lazy_block)
+ args = [*base_args, newrev, *not_in_refs(not_in), '--objects']
+
+ get_objects(args, require_path: require_path, &lazy_block)
+ end
+
+ def all_objects(require_path: nil, &lazy_block)
+ args = [*base_args, '--all', '--objects']
+
+ get_objects(args, require_path: require_path, &lazy_block)
+ end
+
# This methods returns an array of missed references
#
# Should become obsolete after https://gitlab.com/gitlab-org/gitaly/issues/348.
@@ -27,8 +48,15 @@ module Gitlab
private
+ def not_in_refs(references)
+ return ['--not', '--all'] unless references
+ return [] if references.empty?
+
+ references.prepend('--not')
+ end
+
def execute(args)
- output, status = popen(args, nil, Gitlab::Git::Env.all.stringify_keys)
+ output, status = popen(args, nil, Gitlab::Git::Env.to_env_hash)
unless status.zero?
raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}"
@@ -37,6 +65,10 @@ module Gitlab
output.split("\n")
end
+ def lazy_execute(args, &lazy_block)
+ popen(args, nil, Gitlab::Git::Env.to_env_hash, lazy_block: lazy_block)
+ end
+
def base_args
[
Gitlab.config.git.bin_path,
@@ -44,6 +76,30 @@ module Gitlab
'rev-list'
]
end
+
+ def get_objects(args, require_path: nil)
+ if block_given?
+ lazy_execute(args) do |lazy_output|
+ objects = objects_from_output(lazy_output, require_path: require_path)
+
+ yield(objects)
+ end
+ else
+ object_output = execute(args)
+
+ objects_from_output(object_output, require_path: require_path)
+ end
+ end
+
+ def objects_from_output(object_output, require_path: nil)
+ object_output.map do |output_line|
+ sha, path = output_line.split(' ', 2)
+
+ next if require_path && path.blank?
+
+ sha
+ end.reject(&:nil?)
+ end
end
end
end
diff --git a/lib/gitlab/git/storage.rb b/lib/gitlab/git/storage.rb
index 08e6c29abad..99518c9b1e4 100644
--- a/lib/gitlab/git/storage.rb
+++ b/lib/gitlab/git/storage.rb
@@ -12,6 +12,7 @@ module Gitlab
CircuitOpen = Class.new(Inaccessible)
Misconfiguration = Class.new(Inaccessible)
+ Failing = Class.new(Inaccessible)
REDIS_KEY_PREFIX = 'storage_accessible:'.freeze
diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb
index 1eaa2d83fb6..be7598ef011 100644
--- a/lib/gitlab/git/storage/circuit_breaker.rb
+++ b/lib/gitlab/git/storage/circuit_breaker.rb
@@ -2,15 +2,13 @@ module Gitlab
module Git
module Storage
class CircuitBreaker
+ include CircuitBreakerSettings
+
FailureInfo = Struct.new(:last_failure, :failure_count)
attr_reader :storage,
:hostname,
- :storage_path,
- :failure_count_threshold,
- :failure_wait_time,
- :failure_reset_time,
- :storage_timeout
+ :storage_path
delegate :last_failure, :failure_count, to: :failure_info
@@ -18,7 +16,7 @@ module Gitlab
pattern = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}*"
Gitlab::Git::Storage.redis.with do |redis|
- all_storage_keys = redis.keys(pattern)
+ all_storage_keys = redis.scan_each(match: pattern).to_a
redis.del(*all_storage_keys) unless all_storage_keys.empty?
end
@@ -53,14 +51,10 @@ module Gitlab
config = Gitlab.config.repositories.storages[@storage]
@storage_path = config['path']
- @failure_count_threshold = config['failure_count_threshold']
- @failure_wait_time = config['failure_wait_time']
- @failure_reset_time = config['failure_reset_time']
- @storage_timeout = config['storage_timeout']
end
def perform
- return yield unless Feature.enabled?('git_storage_circuit_breaker')
+ return yield unless enabled?
check_storage_accessible!
@@ -70,10 +64,27 @@ module Gitlab
def circuit_broken?
return false if no_failures?
+ failure_count > failure_count_threshold
+ end
+
+ def backing_off?
+ return false if no_failures?
+
recent_failure = last_failure > failure_wait_time.seconds.ago
- too_many_failures = failure_count > failure_count_threshold
+ too_many_failures = failure_count > backoff_threshold
- recent_failure || too_many_failures
+ recent_failure && too_many_failures
+ end
+
+ private
+
+ # The circuitbreaker can be enabled for the entire fleet using a Feature
+ # flag.
+ #
+ # Enabling it for a single host can be done setting the
+ # `GIT_STORAGE_CIRCUIT_BREAKER` environment variable.
+ def enabled?
+ ENV['GIT_STORAGE_CIRCUIT_BREAKER'].present? || Feature.enabled?('git_storage_circuit_breaker')
end
def failure_info
@@ -89,7 +100,7 @@ module Gitlab
return @storage_available if @storage_available
if @storage_available = Gitlab::Git::Storage::ForkedStorageCheck
- .storage_available?(storage_path, storage_timeout)
+ .storage_available?(storage_path, storage_timeout, access_retries)
track_storage_accessible
else
track_storage_inaccessible
@@ -100,7 +111,11 @@ module Gitlab
def check_storage_accessible!
if circuit_broken?
- raise Gitlab::Git::Storage::CircuitOpen.new("Circuit for #{storage} is broken", failure_wait_time)
+ raise Gitlab::Git::Storage::CircuitOpen.new("Circuit for #{storage} is broken", failure_reset_time)
+ end
+
+ if backing_off?
+ raise Gitlab::Git::Storage::Failing.new("Backing off access to #{storage}", failure_wait_time)
end
unless storage_available?
@@ -137,12 +152,6 @@ module Gitlab
end
end
- def cache_key
- @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}"
- end
-
- private
-
def get_failure_info
last_failure, failure_count = Gitlab::Git::Storage.redis.with do |redis|
redis.hmget(cache_key, :last_failure, :failure_count)
@@ -152,6 +161,10 @@ module Gitlab
FailureInfo.new(last_failure, failure_count.to_i)
end
+
+ def cache_key
+ @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}"
+ end
end
end
end
diff --git a/lib/gitlab/git/storage/circuit_breaker_settings.rb b/lib/gitlab/git/storage/circuit_breaker_settings.rb
new file mode 100644
index 00000000000..257fe8cd8f0
--- /dev/null
+++ b/lib/gitlab/git/storage/circuit_breaker_settings.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Git
+ module Storage
+ module CircuitBreakerSettings
+ def failure_count_threshold
+ application_settings.circuitbreaker_failure_count_threshold
+ end
+
+ def failure_wait_time
+ application_settings.circuitbreaker_failure_wait_time
+ end
+
+ def failure_reset_time
+ application_settings.circuitbreaker_failure_reset_time
+ end
+
+ def storage_timeout
+ application_settings.circuitbreaker_storage_timeout
+ end
+
+ def access_retries
+ application_settings.circuitbreaker_access_retries
+ end
+
+ def backoff_threshold
+ application_settings.circuitbreaker_backoff_threshold
+ end
+
+ private
+
+ def application_settings
+ Gitlab::CurrentSettings.current_application_settings
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/storage/forked_storage_check.rb b/lib/gitlab/git/storage/forked_storage_check.rb
index 91d8241f17b..1307f400700 100644
--- a/lib/gitlab/git/storage/forked_storage_check.rb
+++ b/lib/gitlab/git/storage/forked_storage_check.rb
@@ -4,8 +4,17 @@ module Gitlab
module ForkedStorageCheck
extend self
- def storage_available?(path, timeout_seconds = 5)
- status = timeout_check(path, timeout_seconds)
+ def storage_available?(path, timeout_seconds = 5, retries = 1)
+ partial_timeout = timeout_seconds / retries
+ status = timeout_check(path, partial_timeout)
+
+ # If the status check did not succeed the first time, we retry a few
+ # more times to avoid one-off failures
+ current_attempts = 1
+ while current_attempts < retries && !status.success?
+ status = timeout_check(path, partial_timeout)
+ current_attempts += 1
+ end
status.success?
end
diff --git a/lib/gitlab/git/storage/health.rb b/lib/gitlab/git/storage/health.rb
index 1564e94b7f7..7049772fe3b 100644
--- a/lib/gitlab/git/storage/health.rb
+++ b/lib/gitlab/git/storage/health.rb
@@ -23,26 +23,36 @@ module Gitlab
end
end
- def self.all_keys_for_storages(storage_names, redis)
+ private_class_method def self.all_keys_for_storages(storage_names, redis)
keys_per_storage = {}
redis.pipelined do
storage_names.each do |storage_name|
pattern = pattern_for_storage(storage_name)
+ matched_keys = redis.scan_each(match: pattern)
- keys_per_storage[storage_name] = redis.keys(pattern)
+ keys_per_storage[storage_name] = matched_keys
end
end
- keys_per_storage
+ # We need to make sure each lazy-loaded `Enumerator` for matched keys
+ # is loaded into an array.
+ #
+ # Otherwise it would be loaded in the second `Redis#pipelined` block
+ # within `.load_for_keys`. In this pipelined call, the active
+ # Redis-client changes again, so the values would not be available
+ # until the end of that pipelined-block.
+ keys_per_storage.each do |storage_name, key_future|
+ keys_per_storage[storage_name] = key_future.to_a
+ end
end
- def self.load_for_keys(keys_per_storage, redis)
+ private_class_method def self.load_for_keys(keys_per_storage, redis)
info_for_keys = {}
redis.pipelined do
keys_per_storage.each do |storage_name, keys_future|
- info_for_storage = keys_future.value.map do |key|
+ info_for_storage = keys_future.map do |key|
{ name: key, failure_count: redis.hget(key, :failure_count) }
end
diff --git a/lib/gitlab/git/storage/null_circuit_breaker.rb b/lib/gitlab/git/storage/null_circuit_breaker.rb
index 297c043d054..a12d52d295f 100644
--- a/lib/gitlab/git/storage/null_circuit_breaker.rb
+++ b/lib/gitlab/git/storage/null_circuit_breaker.rb
@@ -2,15 +2,14 @@ module Gitlab
module Git
module Storage
class NullCircuitBreaker
+ include CircuitBreakerSettings
+
# These will have actual values
attr_reader :storage,
:hostname
# These will always have nil values
- attr_reader :storage_path,
- :failure_wait_time,
- :failure_reset_time,
- :storage_timeout
+ attr_reader :storage_path
def initialize(storage, hostname, error: nil)
@storage = storage
@@ -26,8 +25,8 @@ module Gitlab
!!@error
end
- def failure_count_threshold
- 1
+ def backing_off?
+ false
end
def last_failure
@@ -35,7 +34,7 @@ module Gitlab
end
def failure_count
- circuit_broken? ? 1 : 0
+ circuit_broken? ? failure_count_threshold : 0
end
def failure_info
diff --git a/lib/gitlab/git/user.rb b/lib/gitlab/git/user.rb
index da74719ae87..e573cd0e143 100644
--- a/lib/gitlab/git/user.rb
+++ b/lib/gitlab/git/user.rb
@@ -7,9 +7,13 @@ module Gitlab
new(gitlab_user.username, gitlab_user.name, gitlab_user.email, Gitlab::GlId.gl_id(gitlab_user))
end
- # TODO support the username field in Gitaly https://gitlab.com/gitlab-org/gitaly/issues/628
def self.from_gitaly(gitaly_user)
- new('', gitaly_user.name, gitaly_user.email, gitaly_user.gl_id)
+ new(
+ gitaly_user.gl_username,
+ Gitlab::EncodingHelper.encode!(gitaly_user.name),
+ Gitlab::EncodingHelper.encode!(gitaly_user.email),
+ gitaly_user.gl_id
+ )
end
def initialize(username, name, email, gl_id)
@@ -22,6 +26,10 @@ module Gitlab
def ==(other)
[username, name, email, gl_id] == [other.username, other.name, other.email, other.gl_id]
end
+
+ def to_gitaly
+ Gitaly::User.new(gl_username: username, gl_id: gl_id, name: name.b, email: email.b)
+ end
end
end
end
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index d651c931a38..d4a53d32c28 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -8,6 +8,9 @@ module Gitlab
{ name: name, email: email, message: message }
end
end
+ PageBlob = Struct.new(:name)
+
+ attr_reader :repository
def self.default_ref
'master'
@@ -23,6 +26,155 @@ module Gitlab
end
def write_page(name, format, content, commit_details)
+ @repository.gitaly_migrate(:wiki_write_page) do |is_enabled|
+ if is_enabled
+ gitaly_write_page(name, format, content, commit_details)
+ gollum_wiki.clear_cache
+ else
+ gollum_write_page(name, format, content, commit_details)
+ end
+ end
+ end
+
+ def delete_page(page_path, commit_details)
+ @repository.gitaly_migrate(:wiki_delete_page) do |is_enabled|
+ if is_enabled
+ gitaly_delete_page(page_path, commit_details)
+ gollum_wiki.clear_cache
+ else
+ gollum_delete_page(page_path, commit_details)
+ end
+ end
+ end
+
+ def update_page(page_path, title, format, content, commit_details)
+ @repository.gitaly_migrate(:wiki_update_page) do |is_enabled|
+ if is_enabled
+ gitaly_update_page(page_path, title, format, content, commit_details)
+ gollum_wiki.clear_cache
+ else
+ gollum_update_page(page_path, title, format, content, commit_details)
+ end
+ end
+ end
+
+ def pages(limit: nil)
+ @repository.gitaly_migrate(:wiki_get_all_pages, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
+ if is_enabled
+ gitaly_get_all_pages
+ else
+ gollum_get_all_pages(limit: limit)
+ end
+ end
+ end
+
+ def page(title:, version: nil, dir: nil)
+ @repository.gitaly_migrate(:wiki_find_page) do |is_enabled|
+ if is_enabled
+ gitaly_find_page(title: title, version: version, dir: dir)
+ else
+ gollum_find_page(title: title, version: version, dir: dir)
+ end
+ end
+ end
+
+ def file(name, version)
+ @repository.gitaly_migrate(:wiki_find_file) do |is_enabled|
+ if is_enabled
+ gitaly_find_file(name, version)
+ else
+ gollum_find_file(name, version)
+ end
+ end
+ end
+
+ # options:
+ # :page - The Integer page number.
+ # :per_page - The number of items per page.
+ # :limit - Total number of items to return.
+ def page_versions(page_path, options = {})
+ current_page = gollum_page_by_path(page_path)
+
+ commits_from_page(current_page, options).map do |gitlab_git_commit|
+ gollum_page = gollum_wiki.page(current_page.title, gitlab_git_commit.id)
+ Gitlab::Git::WikiPageVersion.new(gitlab_git_commit, gollum_page&.format)
+ end
+ end
+
+ def count_page_versions(page_path)
+ @repository.count_commits(ref: 'HEAD', path: page_path)
+ end
+
+ def preview_slug(title, format)
+ # Adapted from gollum gem (Gollum::Wiki#preview_page) to avoid
+ # using Rugged through a Gollum::Wiki instance
+ page_class = Gollum::Page
+ page = page_class.new(nil)
+ ext = page_class.format_to_ext(format.to_sym)
+ name = page_class.cname(title) + '.' + ext
+ blob = PageBlob.new(name)
+ page.populate(blob)
+ page.url_path
+ end
+
+ private
+
+ # options:
+ # :page - The Integer page number.
+ # :per_page - The number of items per page.
+ # :limit - Total number of items to return.
+ def commits_from_page(gollum_page, options = {})
+ unless options[:limit]
+ options[:offset] = ([1, options.delete(:page).to_i].max - 1) * Gollum::Page.per_page
+ options[:limit] = (options.delete(:per_page) || Gollum::Page.per_page).to_i
+ end
+
+ @repository.log(ref: gollum_page.last_version.id,
+ path: gollum_page.path,
+ limit: options[:limit],
+ offset: options[:offset])
+ end
+
+ def gollum_wiki
+ @gollum_wiki ||= Gollum::Wiki.new(@repository.path)
+ end
+
+ def gollum_page_by_path(page_path)
+ page_name = Gollum::Page.canonicalize_filename(page_path)
+ page_dir = File.split(page_path).first
+
+ gollum_wiki.paged(page_name, page_dir)
+ end
+
+ def new_page(gollum_page)
+ Gitlab::Git::WikiPage.new(gollum_page, new_version(gollum_page, gollum_page.version.id))
+ end
+
+ def new_version(gollum_page, commit_id)
+ Gitlab::Git::WikiPageVersion.new(version(commit_id), gollum_page&.format)
+ end
+
+ def version(commit_id)
+ commit_find_proc = -> { Gitlab::Git::Commit.find(@repository, commit_id) }
+
+ if RequestStore.active?
+ RequestStore.fetch([:wiki_version_commit, commit_id]) { commit_find_proc.call }
+ else
+ commit_find_proc.call
+ end
+ end
+
+ def assert_type!(object, klass)
+ unless object.is_a?(klass)
+ raise ArgumentError, "expected a #{klass}, got #{object.inspect}"
+ end
+ end
+
+ def gitaly_wiki_client
+ @gitaly_wiki_client ||= Gitlab::GitalyClient::WikiService.new(@repository)
+ end
+
+ def gollum_write_page(name, format, content, commit_details)
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
@@ -33,14 +185,14 @@ module Gitlab
raise Gitlab::Git::Wiki::DuplicatePageError, e.message
end
- def delete_page(page_path, commit_details)
+ def gollum_delete_page(page_path, commit_details)
assert_type!(commit_details, CommitDetails)
gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h)
nil
end
- def update_page(page_path, title, format, content, commit_details)
+ def gollum_update_page(page_path, title, format, content, commit_details)
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
@@ -48,11 +200,7 @@ module Gitlab
nil
end
- def pages
- gollum_wiki.pages.map { |gollum_page| new_page(gollum_page) }
- end
-
- def page(title:, version: nil, dir: nil)
+ def gollum_find_page(title:, version: nil, dir: nil)
if version
version = Gitlab::Git::Commit.find(@repository, version).id
end
@@ -63,7 +211,7 @@ module Gitlab
new_page(gollum_page)
end
- def file(name, version)
+ def gollum_find_file(name, version)
version ||= self.class.default_ref
gollum_file = gollum_wiki.file(name, version)
return unless gollum_file
@@ -71,43 +219,39 @@ module Gitlab
Gitlab::Git::WikiFile.new(gollum_file)
end
- def page_versions(page_path)
- current_page = gollum_page_by_path(page_path)
- current_page.versions.map do |gollum_git_commit|
- gollum_page = gollum_wiki.page(current_page.title, gollum_git_commit.id)
- new_version(gollum_page, gollum_git_commit.id)
- end
+ def gollum_get_all_pages(limit: nil)
+ gollum_wiki.pages(limit: limit).map { |gollum_page| new_page(gollum_page) }
end
- def preview_slug(title, format)
- gollum_wiki.preview_page(title, '', format).url_path
+ def gitaly_write_page(name, format, content, commit_details)
+ gitaly_wiki_client.write_page(name, format, content, commit_details)
end
- private
+ def gitaly_update_page(page_path, title, format, content, commit_details)
+ gitaly_wiki_client.update_page(page_path, title, format, content, commit_details)
+ end
- def gollum_wiki
- @gollum_wiki ||= Gollum::Wiki.new(@repository.path)
+ def gitaly_delete_page(page_path, commit_details)
+ gitaly_wiki_client.delete_page(page_path, commit_details)
end
- def gollum_page_by_path(page_path)
- page_name = Gollum::Page.canonicalize_filename(page_path)
- page_dir = File.split(page_path).first
+ def gitaly_find_page(title:, version: nil, dir: nil)
+ wiki_page, version = gitaly_wiki_client.find_page(title: title, version: version, dir: dir)
+ return unless wiki_page
- gollum_wiki.paged(page_name, page_dir)
+ Gitlab::Git::WikiPage.new(wiki_page, version)
end
- def new_page(gollum_page)
- Gitlab::Git::WikiPage.new(gollum_page, new_version(gollum_page, gollum_page.version.id))
- end
+ def gitaly_find_file(name, version)
+ wiki_file = gitaly_wiki_client.find_file(name, version)
+ return unless wiki_file
- def new_version(gollum_page, commit_id)
- commit = Gitlab::Git::Commit.find(@repository, commit_id)
- Gitlab::Git::WikiPageVersion.new(commit, gollum_page&.format)
+ Gitlab::Git::WikiFile.new(wiki_file)
end
- def assert_type!(object, klass)
- unless object.is_a?(klass)
- raise ArgumentError, "expected a #{klass}, got #{object.inspect}"
+ def gitaly_get_all_pages
+ gitaly_wiki_client.get_all_pages.map do |wiki_page, version|
+ Gitlab::Git::WikiPage.new(wiki_page, version)
end
end
end
diff --git a/lib/gitlab/git/wiki_file.rb b/lib/gitlab/git/wiki_file.rb
index 527f2a44dea..84335aca4bc 100644
--- a/lib/gitlab/git/wiki_file.rb
+++ b/lib/gitlab/git/wiki_file.rb
@@ -1,7 +1,7 @@
module Gitlab
module Git
class WikiFile
- attr_reader :mime_type, :raw_data, :name
+ attr_reader :mime_type, :raw_data, :name, :path
# This class is meant to be serializable so that it can be constructed
# by Gitaly and sent over the network to GitLab.
@@ -13,6 +13,7 @@ module Gitlab
@mime_type = gollum_file.mime_type
@raw_data = gollum_file.raw_data
@name = gollum_file.name
+ @path = gollum_file.path
end
end
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 42b59c106e2..8998c4b1a83 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -215,10 +215,6 @@ module Gitlab
).exec
end
- def matching_merge_request?(newrev, branch_name)
- Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
- end
-
def deploy_key
actor if deploy_key?
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index cf36106e23d..f27cd800bdd 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -31,7 +31,38 @@ module Gitlab
CLIENT_NAME = (Sidekiq.server? ? 'gitlab-sidekiq' : 'gitlab-web').freeze
MUTEX = Mutex.new
- private_constant :MUTEX
+ METRICS_MUTEX = Mutex.new
+ private_constant :MUTEX, :METRICS_MUTEX
+
+ class << self
+ attr_accessor :query_time
+ end
+
+ self.query_time = 0
+
+ def self.migrate_histogram
+ @migrate_histogram ||=
+ METRICS_MUTEX.synchronize do
+ # If a thread was blocked on the mutex, the value was set already
+ return @migrate_histogram if @migrate_histogram
+
+ Gitlab::Metrics.histogram(:gitaly_migrate_call_duration_seconds,
+ "Gitaly migration call execution timings",
+ gitaly_enabled: nil, feature: nil)
+ end
+ end
+
+ def self.gitaly_call_histogram
+ @gitaly_call_histogram ||=
+ METRICS_MUTEX.synchronize do
+ # If a thread was blocked on the mutex, the value was set already
+ return @gitaly_call_histogram if @gitaly_call_histogram
+
+ Gitlab::Metrics.histogram(:gitaly_controller_action_duration_seconds,
+ "Gitaly endpoint histogram by controller and action combination",
+ Gitlab::Metrics::Transaction::BASE_LABELS.merge(gitaly_service: nil, rpc: nil))
+ end
+ end
def self.stub(name, storage)
MUTEX.synchronize do
@@ -68,17 +99,48 @@ module Gitlab
address
end
+ def self.address_metadata(storage)
+ Base64.strict_encode64(JSON.dump({ storage => { 'address' => address(storage), 'token' => token(storage) } }))
+ end
+
# All Gitaly RPC call sites should use GitalyClient.call. This method
# makes sure that per-request authentication headers are set.
- def self.call(storage, service, rpc, request)
+ #
+ # This method optionally takes a block which receives the keyword
+ # arguments hash 'kwargs' that will be passed to gRPC. This allows the
+ # caller to modify or augment the keyword arguments. The block must
+ # return a hash.
+ #
+ # For example:
+ #
+ # GitalyClient.call(storage, service, rpc, request) do |kwargs|
+ # kwargs.merge(deadline: Time.now + 10)
+ # end
+ #
+ def self.call(storage, service, rpc, request, remote_storage: nil, timeout: nil)
+ start = Gitlab::Metrics::System.monotonic_time
enforce_gitaly_request_limits(:call)
- metadata = request_metadata(storage)
- metadata = yield(metadata) if block_given?
- stub(service, storage).__send__(rpc, request, metadata) # rubocop:disable GitlabSecurity/PublicSend
+ kwargs = request_kwargs(storage, timeout, remote_storage: remote_storage)
+ kwargs = yield(kwargs) if block_given?
+
+ stub(service, storage).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend
+ ensure
+ duration = Gitlab::Metrics::System.monotonic_time - start
+
+ # Keep track, seperately, for the performance bar
+ self.query_time += duration
+ gitaly_call_histogram.observe(
+ current_transaction_labels.merge(gitaly_service: service.to_s, rpc: rpc.to_s),
+ duration)
end
- def self.request_metadata(storage)
+ def self.current_transaction_labels
+ Gitlab::Metrics::Transaction.current&.labels || {}
+ end
+ private_class_method :current_transaction_labels
+
+ def self.request_kwargs(storage, timeout, remote_storage: nil)
encoded_token = Base64.strict_encode64(token(storage).to_s)
metadata = {
'authorization' => "Bearer #{encoded_token}",
@@ -88,8 +150,24 @@ module Gitlab
feature_stack = Thread.current[:gitaly_feature_stack]
feature = feature_stack && feature_stack[0]
metadata['call_site'] = feature.to_s if feature
+ metadata['gitaly-servers'] = address_metadata(remote_storage) if remote_storage
+
+ result = { metadata: metadata }
+
+ # nil timeout indicates that we should use the default
+ timeout = default_timeout if timeout.nil?
- { metadata: metadata }
+ return result unless timeout > 0
+
+ # Do not use `Time.now` for deadline calculation, since it
+ # will be affected by Timecop in some tests, but grpc's c-core
+ # uses system time instead of timecop's time, so tests will fail
+ # `Time.at(Process.clock_gettime(Process::CLOCK_REALTIME))` will
+ # circumvent timecop
+ deadline = Time.at(Process.clock_gettime(Process::CLOCK_REALTIME)) + timeout
+ result[:deadline] = deadline
+
+ result
end
def self.token(storage)
@@ -150,8 +228,11 @@ module Gitlab
feature_stack = Thread.current[:gitaly_feature_stack] ||= []
feature_stack.unshift(feature)
begin
+ start = Gitlab::Metrics::System.monotonic_time
yield is_enabled
ensure
+ total_time = Gitlab::Metrics::System.monotonic_time - start
+ migrate_histogram.observe({ gitaly_enabled: is_enabled, feature: feature }, total_time)
feature_stack.shift
Thread.current[:gitaly_feature_stack] = nil if feature_stack.empty?
end
@@ -259,6 +340,26 @@ module Gitlab
Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| self.encode(s) } )
end
+ # The default timeout on all Gitaly calls
+ def self.default_timeout
+ return 0 if Sidekiq.server?
+
+ timeout(:gitaly_timeout_default)
+ end
+
+ def self.fast_timeout
+ timeout(:gitaly_timeout_fast)
+ end
+
+ def self.medium_timeout
+ timeout(:gitaly_timeout_medium)
+ end
+
+ def self.timeout(timeout_name)
+ Gitlab::CurrentSettings.current_application_settings[timeout_name]
+ end
+ private_class_method :timeout
+
# Count a stack. Used for n+1 detection
def self.count_stack
return unless RequestStore.active?
diff --git a/lib/gitlab/gitaly_client/attributes_bag.rb b/lib/gitlab/gitaly_client/attributes_bag.rb
new file mode 100644
index 00000000000..198a1de91c7
--- /dev/null
+++ b/lib/gitlab/gitaly_client/attributes_bag.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module GitalyClient
+ # This module expects an `ATTRS` const to be defined on the subclass
+ # See GitalyClient::WikiFile for an example
+ module AttributesBag
+ extend ActiveSupport::Concern
+
+ included do
+ attr_accessor(*const_get(:ATTRS))
+ end
+
+ def initialize(params)
+ params = params.with_indifferent_access
+
+ attributes.each do |attr|
+ instance_variable_set("@#{attr}", params[attr])
+ end
+ end
+
+ def ==(other)
+ attributes.all? do |field|
+ instance_variable_get("@#{field}") == other.instance_variable_get("@#{field}")
+ end
+ end
+
+ def attributes
+ self.class.const_get(:ATTRS)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index a2b50f2507e..34807d280e5 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -16,9 +16,9 @@ module Gitlab
revision: GitalyClient.encode(revision)
)
- response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request)
+ response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request, timeout: GitalyClient.medium_timeout)
response.flat_map do |msg|
- msg.paths.map { |d| d.dup.force_encoding(Encoding::UTF_8) }
+ msg.paths.map { |d| EncodingHelper.encode!(d.dup) }
end
end
@@ -29,7 +29,7 @@ module Gitlab
child_id: child_id
)
- GitalyClient.call(@repository.storage, :commit_service, :commit_is_ancestor, request).value
+ GitalyClient.call(@repository.storage, :commit_service, :commit_is_ancestor, request, timeout: GitalyClient.fast_timeout).value
end
def diff(from, to, options = {})
@@ -77,7 +77,7 @@ module Gitlab
limit: limit.to_i
)
- response = GitalyClient.call(@repository.storage, :commit_service, :tree_entry, request)
+ response = GitalyClient.call(@repository.storage, :commit_service, :tree_entry, request, timeout: GitalyClient.medium_timeout)
entry = nil
data = ''
@@ -102,7 +102,7 @@ module Gitlab
path: path.present? ? GitalyClient.encode(path) : '.'
)
- response = GitalyClient.call(@repository.storage, :commit_service, :get_tree_entries, request)
+ response = GitalyClient.call(@repository.storage, :commit_service, :get_tree_entries, request, timeout: GitalyClient.medium_timeout)
response.flat_map do |message|
message.entries.map do |gitaly_tree_entry|
@@ -129,7 +129,7 @@ module Gitlab
request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present?
request.path = options[:path] if options[:path].present?
- GitalyClient.call(@repository.storage, :commit_service, :count_commits, request).count
+ GitalyClient.call(@repository.storage, :commit_service, :count_commits, request, timeout: GitalyClient.medium_timeout).count
end
def last_commit_for_path(revision, path)
@@ -139,7 +139,7 @@ module Gitlab
path: GitalyClient.encode(path.to_s)
)
- gitaly_commit = GitalyClient.call(@repository.storage, :commit_service, :last_commit_for_path, request).commit
+ gitaly_commit = GitalyClient.call(@repository.storage, :commit_service, :last_commit_for_path, request, timeout: GitalyClient.fast_timeout).commit
return unless gitaly_commit
Gitlab::Git::Commit.new(@repository, gitaly_commit)
@@ -152,7 +152,7 @@ module Gitlab
to: to
)
- response = GitalyClient.call(@repository.storage, :commit_service, :commits_between, request)
+ response = GitalyClient.call(@repository.storage, :commit_service, :commits_between, request, timeout: GitalyClient.medium_timeout)
consume_commits_response(response)
end
@@ -165,7 +165,7 @@ module Gitlab
)
request.order = opts[:order].upcase if opts[:order].present?
- response = GitalyClient.call(@repository.storage, :commit_service, :find_all_commits, request)
+ response = GitalyClient.call(@repository.storage, :commit_service, :find_all_commits, request, timeout: GitalyClient.medium_timeout)
consume_commits_response(response)
end
@@ -179,7 +179,7 @@ module Gitlab
offset: offset.to_i
)
- response = GitalyClient.call(@repository.storage, :commit_service, :commits_by_message, request)
+ response = GitalyClient.call(@repository.storage, :commit_service, :commits_by_message, request, timeout: GitalyClient.medium_timeout)
consume_commits_response(response)
end
@@ -197,7 +197,7 @@ module Gitlab
path: GitalyClient.encode(path)
)
- response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request)
+ response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request, timeout: GitalyClient.medium_timeout)
response.reduce("") { |memo, msg| memo << msg.data }
end
@@ -207,7 +207,7 @@ module Gitlab
revision: GitalyClient.encode(revision)
)
- response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request)
+ response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request, timeout: GitalyClient.medium_timeout)
response.commit
end
@@ -217,7 +217,7 @@ module Gitlab
repository: @gitaly_repo,
revision: GitalyClient.encode(revision)
)
- response = GitalyClient.call(@repository.storage, :diff_service, :commit_patch, request)
+ response = GitalyClient.call(@repository.storage, :diff_service, :commit_patch, request, timeout: GitalyClient.medium_timeout)
response.sum(&:data)
end
@@ -227,7 +227,7 @@ module Gitlab
repository: @gitaly_repo,
revision: GitalyClient.encode(revision)
)
- GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request)
+ GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request, timeout: GitalyClient.medium_timeout)
end
def find_commits(options)
@@ -245,7 +245,7 @@ module Gitlab
request.paths = GitalyClient.encode_repeated(Array(options[:path])) if options[:path].present?
- response = GitalyClient.call(@repository.storage, :commit_service, :find_commits, request)
+ response = GitalyClient.call(@repository.storage, :commit_service, :find_commits, request, timeout: GitalyClient.medium_timeout)
consume_commits_response(response)
end
@@ -259,7 +259,7 @@ module Gitlab
request_params.merge!(Gitlab::Git::DiffCollection.collection_limits(options).to_h)
request = Gitaly::CommitDiffRequest.new(request_params)
- response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request)
+ response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request, timeout: GitalyClient.medium_timeout)
GitalyClient::DiffStitcher.new(response)
end
diff --git a/lib/gitlab/gitaly_client/diff.rb b/lib/gitlab/gitaly_client/diff.rb
index 54df6304865..d98a0ce988f 100644
--- a/lib/gitlab/gitaly_client/diff.rb
+++ b/lib/gitlab/gitaly_client/diff.rb
@@ -1,21 +1,9 @@
module Gitlab
module GitalyClient
class Diff
- FIELDS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed).freeze
+ ATTRS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed).freeze
- attr_accessor(*FIELDS)
-
- def initialize(params)
- params.each do |key, val|
- public_send(:"#{key}=", val) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
-
- def ==(other)
- FIELDS.all? do |field|
- public_send(field) == other.public_send(field) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
+ include AttributesBag
end
end
end
diff --git a/lib/gitlab/gitaly_client/diff_stitcher.rb b/lib/gitlab/gitaly_client/diff_stitcher.rb
index 65d81dc5d46..da243ee2d1a 100644
--- a/lib/gitlab/gitaly_client/diff_stitcher.rb
+++ b/lib/gitlab/gitaly_client/diff_stitcher.rb
@@ -12,7 +12,7 @@ module Gitlab
@rpc_response.each do |diff_msg|
if current_diff.nil?
- diff_params = diff_msg.to_h.slice(*GitalyClient::Diff::FIELDS)
+ diff_params = diff_msg.to_h.slice(*GitalyClient::Diff::ATTRS)
# gRPC uses frozen strings by default, and we need to have an unfrozen string as it
# gets processed further down the line. So we unfreeze the first chunk of the patch
# in case it's the only chunk we receive for this diff.
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 81ddaf13e10..526d44a8b77 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -10,7 +10,7 @@ module Gitlab
request = Gitaly::UserDeleteTagRequest.new(
repository: @gitaly_repo,
tag_name: GitalyClient.encode(tag_name),
- user: Util.gitaly_user(user)
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly
)
response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_tag, request)
@@ -23,7 +23,7 @@ module Gitlab
def add_tag(tag_name, user, target, message)
request = Gitaly::UserCreateTagRequest.new(
repository: @gitaly_repo,
- user: Util.gitaly_user(user),
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
tag_name: GitalyClient.encode(tag_name),
target_revision: GitalyClient.encode(target),
message: GitalyClient.encode(message.to_s)
@@ -45,7 +45,7 @@ module Gitlab
request = Gitaly::UserCreateBranchRequest.new(
repository: @gitaly_repo,
branch_name: GitalyClient.encode(branch_name),
- user: Util.gitaly_user(user),
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
start_point: GitalyClient.encode(start_point)
)
response = GitalyClient.call(@repository.storage, :operation_service,
@@ -65,7 +65,7 @@ module Gitlab
request = Gitaly::UserDeleteBranchRequest.new(
repository: @gitaly_repo,
branch_name: GitalyClient.encode(branch_name),
- user: Util.gitaly_user(user)
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly
)
response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_branch, request)
@@ -74,6 +74,54 @@ module Gitlab
raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error
end
end
+
+ def user_merge_branch(user, source_sha, target_branch, message)
+ request_enum = QueueEnumerator.new
+ response_enum = GitalyClient.call(
+ @repository.storage,
+ :operation_service,
+ :user_merge_branch,
+ request_enum.each
+ )
+
+ request_enum.push(
+ Gitaly::UserMergeBranchRequest.new(
+ repository: @gitaly_repo,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ commit_id: source_sha,
+ branch: GitalyClient.encode(target_branch),
+ message: GitalyClient.encode(message)
+ )
+ )
+
+ yield response_enum.next.commit_id
+
+ request_enum.push(Gitaly::UserMergeBranchRequest.new(apply: true))
+
+ branch_update = response_enum.next.branch_update
+ raise Gitlab::Git::CommitError.new('failed to apply merge to branch') unless branch_update.commit_id.present?
+
+ Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update)
+ ensure
+ request_enum.close
+ end
+
+ def user_ff_branch(user, source_sha, target_branch)
+ request = Gitaly::UserFFBranchRequest.new(
+ repository: @gitaly_repo,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ commit_id: source_sha,
+ branch: GitalyClient.encode(target_branch)
+ )
+
+ branch_update = GitalyClient.call(
+ @repository.storage,
+ :operation_service,
+ :user_ff_branch,
+ request
+ ).branch_update
+ Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update)
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/queue_enumerator.rb b/lib/gitlab/gitaly_client/queue_enumerator.rb
new file mode 100644
index 00000000000..b8018029552
--- /dev/null
+++ b/lib/gitlab/gitaly_client/queue_enumerator.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module GitalyClient
+ class QueueEnumerator
+ def initialize
+ @queue = Queue.new
+ end
+
+ def push(elem)
+ @queue << elem
+ end
+
+ def close
+ push(:close)
+ end
+
+ def each
+ return enum_for(:each) unless block_given?
+
+ loop do
+ elem = @queue.pop
+ break if elem == :close
+
+ yield elem
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 8214b7d63fa..066e4e183c0 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -46,7 +46,8 @@ module Gitlab
commit_id: commit_id,
prefix: ref_prefix
)
- encode!(GitalyClient.call(@storage, :ref_service, :find_ref_name, request).name.dup)
+ response = GitalyClient.call(@storage, :ref_service, :find_ref_name, request, timeout: GitalyClient.medium_timeout)
+ encode!(response.name.dup)
end
def count_tag_names
@@ -57,14 +58,6 @@ module Gitlab
branch_names.count
end
- # TODO implement a more efficient RPC for this https://gitlab.com/gitlab-org/gitaly/issues/616
- def has_local_branches?
- request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo)
- response = GitalyClient.call(@storage, :ref_service, :find_all_branch_names, request).first
-
- response&.names.present?
- end
-
def local_branches(sort_by: nil)
request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo)
request.sort_by = sort_by_param(sort_by) if sort_by
@@ -134,6 +127,15 @@ module Gitlab
GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request)
end
+ def delete_refs(except_with_prefixes:)
+ request = Gitaly::DeleteRefsRequest.new(
+ repository: @gitaly_repo,
+ except_with_prefix: except_with_prefixes
+ )
+
+ GitalyClient.call(@repository.storage, :ref_service, :delete_refs, request)
+ end
+
private
def consume_refs_response(response)
@@ -145,6 +147,7 @@ module Gitlab
enum_value = Gitaly::FindLocalBranchesRequest::SortBy.resolve(sort_by.upcase.to_sym)
raise ArgumentError, "Invalid sort_by key `#{sort_by}`" unless enum_value
+
enum_value
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index fdf912214e0..b9e606592d7 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -10,7 +10,9 @@ module Gitlab
def exists?
request = Gitaly::RepositoryExistsRequest.new(repository: @gitaly_repo)
- GitalyClient.call(@storage, :repository_service, :repository_exists, request).exists
+ response = GitalyClient.call(@storage, :repository_service, :repository_exists, request, timeout: GitalyClient.fast_timeout)
+
+ response.exists
end
def garbage_collect(create_bitmap)
@@ -30,7 +32,8 @@ module Gitlab
def repository_size
request = Gitaly::RepositorySizeRequest.new(repository: @gitaly_repo)
- GitalyClient.call(@storage, :repository_service, :repository_size, request).size
+ response = GitalyClient.call(@storage, :repository_service, :repository_size, request)
+ response.size
end
def apply_gitattributes(revision)
@@ -58,6 +61,32 @@ module Gitlab
request = Gitaly::CreateRepositoryRequest.new(repository: @gitaly_repo)
GitalyClient.call(@storage, :repository_service, :create_repository, request)
end
+
+ def has_local_branches?
+ request = Gitaly::HasLocalBranchesRequest.new(repository: @gitaly_repo)
+ response = GitalyClient.call(@storage, :repository_service, :has_local_branches, request, timeout: GitalyClient.fast_timeout)
+
+ response.value
+ end
+
+ def fetch_source_branch(source_repository, source_branch, local_ref)
+ request = Gitaly::FetchSourceBranchRequest.new(
+ repository: @gitaly_repo,
+ source_repository: source_repository.gitaly_repository,
+ source_branch: source_branch.b,
+ target_ref: local_ref.b
+ )
+
+ response = GitalyClient.call(
+ @storage,
+ :repository_service,
+ :fetch_source_branch,
+ request,
+ remote_storage: source_repository.storage
+ )
+
+ response.result
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb
index da43c616b94..b1a033280b4 100644
--- a/lib/gitlab/gitaly_client/util.rb
+++ b/lib/gitlab/gitaly_client/util.rb
@@ -3,22 +3,18 @@ module Gitlab
module Util
class << self
def repository(repository_storage, relative_path, gl_repository)
+ git_object_directory = Gitlab::Git::Env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence ||
+ Gitlab::Git::Env['GIT_OBJECT_DIRECTORY'].presence
+ git_alternate_object_directories =
+ Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE']).presence ||
+ Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES']).flat_map { |d| d.split(File::PATH_SEPARATOR) }
+
Gitaly::Repository.new(
storage_name: repository_storage,
relative_path: relative_path,
gl_repository: gl_repository,
- git_object_directory: Gitlab::Git::Env['GIT_OBJECT_DIRECTORY'].to_s,
- git_alternate_object_directories: Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES'])
- )
- end
-
- def gitaly_user(gitlab_user)
- return unless gitlab_user
-
- Gitaly::User.new(
- gl_id: Gitlab::GlId.gl_id(gitlab_user),
- name: GitalyClient.encode(gitlab_user.name),
- email: GitalyClient.encode(gitlab_user.email)
+ git_object_directory: git_object_directory.to_s,
+ git_alternate_object_directories: git_alternate_object_directories
)
end
diff --git a/lib/gitlab/gitaly_client/wiki_file.rb b/lib/gitlab/gitaly_client/wiki_file.rb
new file mode 100644
index 00000000000..47c60c92484
--- /dev/null
+++ b/lib/gitlab/gitaly_client/wiki_file.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module GitalyClient
+ class WikiFile
+ ATTRS = %i(name mime_type path raw_data).freeze
+
+ include AttributesBag
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/wiki_page.rb b/lib/gitlab/gitaly_client/wiki_page.rb
new file mode 100644
index 00000000000..7339468e911
--- /dev/null
+++ b/lib/gitlab/gitaly_client/wiki_page.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module GitalyClient
+ class WikiPage
+ ATTRS = %i(title format url_path path name historical raw_data).freeze
+
+ include AttributesBag
+
+ def initialize(params)
+ super
+
+ # All gRPC strings in a response are frozen, so we get an unfrozen
+ # version here so appending to `raw_data` doesn't blow up.
+ @raw_data = @raw_data.dup
+ end
+
+ def historical?
+ @historical
+ end
+
+ def format
+ @format.to_sym
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
new file mode 100644
index 00000000000..c8f065f5881
--- /dev/null
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -0,0 +1,170 @@
+require 'stringio'
+
+module Gitlab
+ module GitalyClient
+ class WikiService
+ MAX_MSG_SIZE = 128.kilobytes.freeze
+
+ def initialize(repository)
+ @gitaly_repo = repository.gitaly_repository
+ @repository = repository
+ end
+
+ def write_page(name, format, content, commit_details)
+ request = Gitaly::WikiWritePageRequest.new(
+ repository: @gitaly_repo,
+ name: GitalyClient.encode(name),
+ format: format.to_s,
+ commit_details: gitaly_commit_details(commit_details)
+ )
+
+ strio = StringIO.new(content)
+
+ enum = Enumerator.new do |y|
+ until strio.eof?
+ chunk = strio.read(MAX_MSG_SIZE)
+ request.content = GitalyClient.encode(chunk)
+
+ y.yield request
+
+ request = Gitaly::WikiWritePageRequest.new
+ end
+ end
+
+ response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_write_page, enum)
+ if error = response.duplicate_error.presence
+ raise Gitlab::Git::Wiki::DuplicatePageError, error
+ end
+ end
+
+ def update_page(page_path, title, format, content, commit_details)
+ request = Gitaly::WikiUpdatePageRequest.new(
+ repository: @gitaly_repo,
+ page_path: GitalyClient.encode(page_path),
+ title: GitalyClient.encode(title),
+ format: format.to_s,
+ commit_details: gitaly_commit_details(commit_details)
+ )
+
+ strio = StringIO.new(content)
+
+ enum = Enumerator.new do |y|
+ until strio.eof?
+ chunk = strio.read(MAX_MSG_SIZE)
+ request.content = GitalyClient.encode(chunk)
+
+ y.yield request
+
+ request = Gitaly::WikiUpdatePageRequest.new
+ end
+ end
+
+ GitalyClient.call(@repository.storage, :wiki_service, :wiki_update_page, enum)
+ end
+
+ def delete_page(page_path, commit_details)
+ request = Gitaly::WikiDeletePageRequest.new(
+ repository: @gitaly_repo,
+ page_path: GitalyClient.encode(page_path),
+ commit_details: gitaly_commit_details(commit_details)
+ )
+
+ GitalyClient.call(@repository.storage, :wiki_service, :wiki_delete_page, request)
+ end
+
+ def find_page(title:, version: nil, dir: nil)
+ request = Gitaly::WikiFindPageRequest.new(
+ repository: @gitaly_repo,
+ title: GitalyClient.encode(title),
+ revision: GitalyClient.encode(version),
+ directory: GitalyClient.encode(dir)
+ )
+
+ response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request)
+
+ wiki_page_from_iterator(response)
+ end
+
+ def get_all_pages
+ request = Gitaly::WikiGetAllPagesRequest.new(repository: @gitaly_repo)
+ response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_all_pages, request)
+ pages = []
+
+ loop do
+ page, version = wiki_page_from_iterator(response) { |message| message.end_of_page }
+
+ break unless page && version
+
+ pages << [page, version]
+ end
+
+ pages
+ end
+
+ def find_file(name, revision)
+ request = Gitaly::WikiFindFileRequest.new(
+ repository: @gitaly_repo,
+ name: GitalyClient.encode(name),
+ revision: GitalyClient.encode(revision)
+ )
+
+ response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_file, request)
+ wiki_file = nil
+
+ response.each do |message|
+ next unless message.name.present?
+
+ if wiki_file
+ wiki_file.raw_data << message.raw_data
+ else
+ wiki_file = GitalyClient::WikiFile.new(message.to_h)
+ # All gRPC strings in a response are frozen, so we get
+ # an unfrozen version here so appending in the else clause below doesn't blow up.
+ wiki_file.raw_data = wiki_file.raw_data.dup
+ end
+ end
+
+ wiki_file
+ end
+
+ private
+
+ # If a block is given and the yielded value is true, iteration will be
+ # stopped early at that point; else the iterator is consumed entirely.
+ # The iterator is traversed with `next` to allow resuming the iteration.
+ def wiki_page_from_iterator(iterator)
+ wiki_page = version = nil
+
+ while message = iterator.next
+ break if block_given? && yield(message)
+
+ page = message.page
+ next unless page
+
+ if wiki_page
+ wiki_page.raw_data << page.raw_data
+ else
+ wiki_page = GitalyClient::WikiPage.new(page.to_h)
+
+ version = Gitlab::Git::WikiPageVersion.new(
+ Gitlab::Git::Commit.decorate(@repository, page.version.commit),
+ page.version.format
+ )
+ end
+ end
+
+ [wiki_page, version]
+ rescue StopIteration
+ [wiki_page, version]
+ end
+
+ def gitaly_commit_details(commit_details)
+ Gitaly::WikiCommitDetails.new(
+ name: GitalyClient.encode(commit_details.name),
+ email: GitalyClient.encode(commit_details.email),
+ message: GitalyClient.encode(commit_details.message)
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import.rb b/lib/gitlab/github_import.rb
new file mode 100644
index 00000000000..65b5e30c70f
--- /dev/null
+++ b/lib/gitlab/github_import.rb
@@ -0,0 +1,38 @@
+module Gitlab
+ module GithubImport
+ def self.refmap
+ [:heads, :tags, '+refs/pull/*/head:refs/merge-requests/*/head']
+ end
+
+ def self.new_client_for(project, token: nil, parallel: true)
+ token_to_use = token || project.import_data&.credentials&.fetch(:user)
+
+ Client.new(token_to_use, parallel: parallel)
+ end
+
+ # Inserts a raw row and returns the ID of the inserted row.
+ #
+ # attributes - The attributes/columns to set.
+ # relation - An ActiveRecord::Relation to use for finding the ID of the row
+ # when using MySQL.
+ def self.insert_and_return_id(attributes, relation)
+ # We use bulk_insert here so we can bypass any queries executed by
+ # callbacks or validation rules, as doing this wouldn't scale when
+ # importing very large projects.
+ result = Gitlab::Database
+ .bulk_insert(relation.table_name, [attributes], return_ids: true)
+
+ # MySQL doesn't support returning the IDs of a bulk insert in a way that
+ # is not a pain, so in this case we'll issue an extra query instead.
+ result.first ||
+ relation.where(iid: attributes[:iid]).limit(1).pluck(:id).first
+ end
+
+ # Returns the ID of the ghost user.
+ def self.ghost_user_id
+ key = 'github-import/ghost-user-id'
+
+ Caching.read_integer(key) || Caching.write(key, User.select(:id).ghost.id)
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
deleted file mode 100644
index f330041cc00..00000000000
--- a/lib/gitlab/github_import/base_formatter.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module Gitlab
- module GithubImport
- class BaseFormatter
- attr_reader :client, :formatter, :project, :raw_data
-
- def initialize(project, raw_data, client = nil)
- @project = project
- @raw_data = raw_data
- @client = client
- @formatter = Gitlab::ImportFormatter.new
- end
-
- def create!
- association = project.public_send(project_association) # rubocop:disable GitlabSecurity/PublicSend
-
- association.find_or_create_by!(find_condition) do |record|
- record.attributes = attributes
- end
- end
-
- def url
- raw_data.url || ''
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb
deleted file mode 100644
index 8aa885fb811..00000000000
--- a/lib/gitlab/github_import/branch_formatter.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-module Gitlab
- module GithubImport
- class BranchFormatter < BaseFormatter
- delegate :repo, :sha, :ref, to: :raw_data
-
- def exists?
- branch_exists? && commit_exists?
- end
-
- def valid?
- sha.present? && ref.present?
- end
-
- def user
- raw_data.user&.login || 'unknown'
- end
-
- def short_sha
- Commit.truncate_sha(sha)
- end
-
- private
-
- def branch_exists?
- project.repository.branch_exists?(ref)
- end
-
- def commit_exists?
- project.repository.branch_names_contains(sha).include?(ref)
- end
-
- def short_id
- sha.to_s[0..7]
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/bulk_importing.rb b/lib/gitlab/github_import/bulk_importing.rb
new file mode 100644
index 00000000000..147597289cf
--- /dev/null
+++ b/lib/gitlab/github_import/bulk_importing.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module BulkImporting
+ # Builds and returns an Array of objects to bulk insert into the
+ # database.
+ #
+ # enum - An Enumerable that returns the objects to turn into database
+ # rows.
+ def build_database_rows(enum)
+ enum.each_with_object([]) do |(object, _), rows|
+ rows << build(object) unless already_imported?(object)
+ end
+ end
+
+ # Bulk inserts the given rows into the database.
+ def bulk_insert(model, rows, batch_size: 100)
+ rows.each_slice(batch_size) do |slice|
+ Gitlab::Database.bulk_insert(model.table_name, slice)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/caching.rb b/lib/gitlab/github_import/caching.rb
new file mode 100644
index 00000000000..b08f133794f
--- /dev/null
+++ b/lib/gitlab/github_import/caching.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Caching
+ # The default timeout of the cache keys.
+ TIMEOUT = 24.hours.to_i
+
+ WRITE_IF_GREATER_SCRIPT = <<-EOF.strip_heredoc.freeze
+ local key, value, ttl = KEYS[1], tonumber(ARGV[1]), ARGV[2]
+ local existing = tonumber(redis.call("get", key))
+
+ if existing == nil or value > existing then
+ redis.call("set", key, value)
+ redis.call("expire", key, ttl)
+ return true
+ else
+ return false
+ end
+ EOF
+
+ # Reads a cache key.
+ #
+ # If the key exists and has a non-empty value its TTL is refreshed
+ # automatically.
+ #
+ # raw_key - The cache key to read.
+ # timeout - The new timeout of the key if the key is to be refreshed.
+ def self.read(raw_key, timeout: TIMEOUT)
+ key = cache_key_for(raw_key)
+ value = Redis::Cache.with { |redis| redis.get(key) }
+
+ if value.present?
+ # We refresh the expiration time so frequently used keys stick
+ # around, removing the need for querying the database as much as
+ # possible.
+ #
+ # A key may be empty when we looked up a GitHub user (for example) but
+ # did not find a matching GitLab user. In that case we _don't_ want to
+ # refresh the TTL so we automatically pick up the right data when said
+ # user were to register themselves on the GitLab instance.
+ Redis::Cache.with { |redis| redis.expire(key, timeout) }
+ end
+
+ value
+ end
+
+ # Reads an integer from the cache, or returns nil if no value was found.
+ #
+ # See Caching.read for more information.
+ def self.read_integer(raw_key, timeout: TIMEOUT)
+ value = read(raw_key, timeout: timeout)
+
+ value.to_i if value.present?
+ end
+
+ # Sets a cache key to the given value.
+ #
+ # key - The cache key to write.
+ # value - The value to set.
+ # timeout - The time after which the cache key should expire.
+ def self.write(raw_key, value, timeout: TIMEOUT)
+ key = cache_key_for(raw_key)
+
+ Redis::Cache.with do |redis|
+ redis.set(key, value, ex: timeout)
+ end
+
+ value
+ end
+
+ # Adds a value to a set.
+ #
+ # raw_key - The key of the set to add the value to.
+ # value - The value to add to the set.
+ # timeout - The new timeout of the key.
+ def self.set_add(raw_key, value, timeout: TIMEOUT)
+ key = cache_key_for(raw_key)
+
+ Redis::Cache.with do |redis|
+ redis.multi do |m|
+ m.sadd(key, value)
+ m.expire(key, timeout)
+ end
+ end
+ end
+
+ # Returns true if the given value is present in the set.
+ #
+ # raw_key - The key of the set to check.
+ # value - The value to check for.
+ def self.set_includes?(raw_key, value)
+ key = cache_key_for(raw_key)
+
+ Redis::Cache.with do |redis|
+ redis.sismember(key, value)
+ end
+ end
+
+ # Sets multiple keys to a given value.
+ #
+ # mapping - A Hash mapping the cache keys to their values.
+ # timeout - The time after which the cache key should expire.
+ def self.write_multiple(mapping, timeout: TIMEOUT)
+ Redis::Cache.with do |redis|
+ redis.multi do |multi|
+ mapping.each do |raw_key, value|
+ multi.set(cache_key_for(raw_key), value, ex: timeout)
+ end
+ end
+ end
+ end
+
+ # Sets the expiration time of a key.
+ #
+ # raw_key - The key for which to change the timeout.
+ # timeout - The new timeout.
+ def self.expire(raw_key, timeout)
+ key = cache_key_for(raw_key)
+
+ Redis::Cache.with do |redis|
+ redis.expire(key, timeout)
+ end
+ end
+
+ # Sets a key to the given integer but only if the existing value is
+ # smaller than the given value.
+ #
+ # This method uses a Lua script to ensure the read and write are atomic.
+ #
+ # raw_key - The key to set.
+ # value - The new value for the key.
+ # timeout - The key timeout in seconds.
+ #
+ # Returns true when the key was overwritten, false otherwise.
+ def self.write_if_greater(raw_key, value, timeout: TIMEOUT)
+ key = cache_key_for(raw_key)
+ val = Redis::Cache.with do |redis|
+ redis
+ .eval(WRITE_IF_GREATER_SCRIPT, keys: [key], argv: [value, timeout])
+ end
+
+ val ? true : false
+ end
+
+ def self.cache_key_for(raw_key)
+ "#{Redis::Cache::CACHE_NAMESPACE}:#{raw_key}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 0550f9695bd..5da9befa08e 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -1,147 +1,216 @@
+# frozen_string_literal: true
+
module Gitlab
module GithubImport
+ # HTTP client for interacting with the GitHub API.
+ #
+ # This class is basically a fancy wrapped around Octokit while adding some
+ # functionality to deal with rate limiting and parallel imports. Usage is
+ # mostly the same as Octokit, for example:
+ #
+ # client = GithubImport::Client.new('hunter2')
+ #
+ # client.labels.each do |label|
+ # puts label.name
+ # end
class Client
- GITHUB_SAFE_REMAINING_REQUESTS = 100
- GITHUB_SAFE_SLEEP_TIME = 500
+ attr_reader :octokit
+
+ # A single page of data and the corresponding page number.
+ Page = Struct.new(:objects, :number)
+
+ # The minimum number of requests we want to keep available.
+ #
+ # We don't use a value of 0 as multiple threads may be using the same
+ # token in parallel. This could result in all of them hitting the GitHub
+ # rate limit at once. The threshold is put in place to not hit the limit
+ # in most cases.
+ RATE_LIMIT_THRESHOLD = 50
+
+ # token - The GitHub API token to use.
+ #
+ # per_page - The number of objects that should be displayed per page.
+ #
+ # parallel - When set to true hitting the rate limit will result in a
+ # dedicated error being raised. When set to `false` we will
+ # instead just `sleep()` until the rate limit is reset. Setting
+ # this value to `true` for parallel importing is crucial as
+ # otherwise hitting the rate limit will result in a thread
+ # being blocked in a `sleep()` call for up to an hour.
+ def initialize(token, per_page: 100, parallel: true)
+ @octokit = Octokit::Client.new(
+ access_token: token,
+ per_page: per_page,
+ api_endpoint: api_endpoint
+ )
- attr_reader :access_token, :host, :api_version
+ @octokit.connection_options[:ssl] = { verify: verify_ssl }
- def initialize(access_token, host: nil, api_version: 'v3')
- @access_token = access_token
- @host = host.to_s.sub(%r{/+\z}, '')
- @api_version = api_version
- @users = {}
+ @parallel = parallel
+ end
- if access_token
- ::Octokit.auto_paginate = false
- end
+ def parallel?
+ @parallel
end
- def api
- @api ||= ::Octokit::Client.new(
- access_token: access_token,
- api_endpoint: api_endpoint,
- # If there is no config, we're connecting to github.com and we
- # should verify ssl.
- connection_options: {
- ssl: { verify: config ? config['verify_ssl'] : true }
- }
- )
+ # Returns the details of a GitHub user.
+ #
+ # username - The username of the user.
+ def user(username)
+ with_rate_limit { octokit.user(username) }
end
- def client
- unless config
- raise Projects::ImportService::Error,
- 'OAuth configuration for GitHub missing.'
- end
+ # Returns the details of a GitHub repository.
+ #
+ # name - The path (in the form `owner/repository`) of the repository.
+ def repository(name)
+ with_rate_limit { octokit.repo(name) }
+ end
- @client ||= ::OAuth2::Client.new(
- config.app_id,
- config.app_secret,
- github_options.merge(ssl: { verify: config['verify_ssl'] })
- )
+ def labels(*args)
+ each_object(:labels, *args)
end
- def authorize_url(redirect_uri)
- client.auth_code.authorize_url({
- redirect_uri: redirect_uri,
- scope: "repo, user, user:email"
- })
+ def milestones(*args)
+ each_object(:milestones, *args)
end
- def get_token(code)
- client.auth_code.get_token(code).token
+ def releases(*args)
+ each_object(:releases, *args)
end
- def method_missing(method, *args, &block)
- if api.respond_to?(method)
- request(method, *args, &block)
- else
- super(method, *args, &block)
+ # Fetches data from the GitHub API and yields a Page object for every page
+ # of data, without loading all of them into memory.
+ #
+ # method - The Octokit method to use for getting the data.
+ # args - Arguments to pass to the Octokit method.
+ #
+ # rubocop: disable GitlabSecurity/PublicSend
+ def each_page(method, *args, &block)
+ return to_enum(__method__, method, *args) unless block_given?
+
+ page =
+ if args.last.is_a?(Hash) && args.last[:page]
+ args.last[:page]
+ else
+ 1
+ end
+
+ collection = with_rate_limit { octokit.public_send(method, *args) }
+ next_url = octokit.last_response.rels[:next]
+
+ yield Page.new(collection, page)
+
+ while next_url
+ response = with_rate_limit { next_url.get }
+ next_url = response.rels[:next]
+
+ yield Page.new(response.data, page += 1)
end
end
- def respond_to?(method)
- api.respond_to?(method) || super
+ # Iterates over all of the objects for the given method (e.g. `:labels`).
+ #
+ # method - The method to send to Octokit for querying data.
+ # args - Any arguments to pass to the Octokit method.
+ def each_object(method, *args, &block)
+ return to_enum(__method__, method, *args) unless block_given?
+
+ each_page(method, *args) do |page|
+ page.objects.each do |object|
+ yield object
+ end
+ end
end
- def user(login)
- return nil unless login.present?
- return @users[login] if @users.key?(login)
+ # Yields the supplied block, responding to any rate limit errors.
+ #
+ # The exact strategy used for handling rate limiting errors depends on
+ # whether we are running in parallel mode or not. For more information see
+ # `#rate_or_wait_for_rate_limit`.
+ def with_rate_limit
+ return yield unless rate_limiting_enabled?
- @users[login] = api.user(login)
- end
+ request_count_counter.increment
- private
+ raise_or_wait_for_rate_limit unless requests_remaining?
- def api_endpoint
- if host.present? && api_version.present?
- "#{host}/api/#{api_version}"
- else
- github_options[:site]
+ begin
+ yield
+ rescue Octokit::TooManyRequests
+ raise_or_wait_for_rate_limit
+
+ # This retry will only happen when running in sequential mode as we'll
+ # raise an error in parallel mode.
+ retry
end
end
- def config
- Gitlab.config.omniauth.providers.find { |provider| provider.name == "github" }
+ # Returns `true` if we're still allowed to perform API calls.
+ def requests_remaining?
+ remaining_requests > RATE_LIMIT_THRESHOLD
+ end
+
+ def remaining_requests
+ octokit.rate_limit.remaining
end
- def github_options
- if config
- config["args"]["client_options"].deep_symbolize_keys
+ def raise_or_wait_for_rate_limit
+ rate_limit_counter.increment
+
+ if parallel?
+ raise RateLimitError
else
- OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys
+ sleep(rate_limit_resets_in)
end
end
- def rate_limit
- api.rate_limit!
- # GitHub Rate Limit API returns 404 when the rate limit is
- # disabled. In this case we just want to return gracefully
- # instead of spitting out an error.
- rescue Octokit::NotFound
- nil
+ def rate_limit_resets_in
+ # We add a few seconds to the rate limit so we don't _immediately_
+ # resume when the rate limit resets as this may result in us performing
+ # a request before GitHub has a chance to reset the limit.
+ octokit.rate_limit.resets_in + 5
end
- def has_rate_limit?
- return @has_rate_limit if defined?(@has_rate_limit)
-
- @has_rate_limit = rate_limit.present?
+ def rate_limiting_enabled?
+ @rate_limiting_enabled ||= api_endpoint.include?('.github.com')
end
- def rate_limit_exceed?
- has_rate_limit? && rate_limit.remaining <= GITHUB_SAFE_REMAINING_REQUESTS
+ def api_endpoint
+ custom_api_endpoint || default_api_endpoint
end
- def rate_limit_sleep_time
- rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME
+ def custom_api_endpoint
+ github_omniauth_provider.dig('args', 'client_options', 'site')
end
- def request(method, *args, &block)
- sleep rate_limit_sleep_time if rate_limit_exceed?
+ def default_api_endpoint
+ OmniAuth::Strategies::GitHub.default_options[:client_options][:site]
+ end
- data = api.__send__(method, *args) # rubocop:disable GitlabSecurity/PublicSend
- return data unless data.is_a?(Array)
+ def verify_ssl
+ github_omniauth_provider.fetch('verify_ssl', true)
+ end
- last_response = api.last_response
+ def github_omniauth_provider
+ @github_omniauth_provider ||=
+ Gitlab.config.omniauth.providers
+ .find { |provider| provider.name == 'github' }
+ .to_h
+ end
- if block_given?
- yield data
- # api.last_response could change while we're yielding (e.g. fetching labels for each PR)
- # so we cache our own last response
- each_response_page(last_response, &block)
- else
- each_response_page(last_response) { |page| data.concat(page) }
- data
- end
+ def rate_limit_counter
+ @rate_limit_counter ||= Gitlab::Metrics.counter(
+ :github_importer_rate_limit_hits,
+ 'The number of times we hit the GitHub rate limit when importing projects'
+ )
end
- def each_response_page(last_response)
- while last_response.rels[:next]
- sleep rate_limit_sleep_time if rate_limit_exceed?
- last_response = last_response.rels[:next].get
- yield last_response.data if last_response.data.is_a?(Array)
- end
+ def request_count_counter
+ @request_counter ||= Gitlab::Metrics.counter(
+ :github_importer_request_count,
+ 'The number of GitHub API calls performed when importing projects'
+ )
end
end
end
diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb
deleted file mode 100644
index e21922070c1..00000000000
--- a/lib/gitlab/github_import/comment_formatter.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-module Gitlab
- module GithubImport
- class CommentFormatter < BaseFormatter
- attr_writer :author_id
-
- def attributes
- {
- project: project,
- note: note,
- commit_id: raw_data.commit_id,
- line_code: line_code,
- author_id: author_id,
- type: type,
- created_at: raw_data.created_at,
- updated_at: raw_data.updated_at
- }
- end
-
- private
-
- def author
- @author ||= UserFormatter.new(client, raw_data.user)
- end
-
- def author_id
- author.gitlab_id || project.creator_id
- end
-
- def body
- raw_data.body || ""
- end
-
- def line_code
- return unless on_diff?
-
- parsed_lines = Gitlab::Diff::Parser.new.parse(diff_hunk.lines)
- generate_line_code(parsed_lines.to_a.last)
- end
-
- def generate_line_code(line)
- Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
- end
-
- def on_diff?
- diff_hunk.present?
- end
-
- def diff_hunk
- raw_data.diff_hunk
- end
-
- def file_path
- raw_data.path
- end
-
- def note
- if author.gitlab_id
- body
- else
- formatter.author_line(author.login) + body
- end
- end
-
- def type
- 'LegacyDiffNote' if on_diff?
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
deleted file mode 100644
index b8c07460ebb..00000000000
--- a/lib/gitlab/github_import/importer.rb
+++ /dev/null
@@ -1,329 +0,0 @@
-module Gitlab
- module GithubImport
- class Importer
- include Gitlab::ShellAdapter
-
- attr_reader :errors, :project, :repo, :repo_url
-
- def initialize(project)
- @project = project
- @repo = project.import_source
- @repo_url = project.import_url
- @errors = []
- @labels = {}
- end
-
- def client
- return @client if defined?(@client)
- unless credentials
- raise Projects::ImportService::Error,
- "Unable to find project import data credentials for project ID: #{@project.id}"
- end
-
- opts = {}
- # Gitea plan to be GitHub compliant
- if project.gitea_import?
- uri = URI.parse(project.import_url)
- host = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}".sub(%r{/?[\w-]+/[\w-]+\.git\z}, '')
- opts = {
- host: host,
- api_version: 'v1'
- }
- end
-
- @client = Client.new(credentials[:user], opts)
- end
-
- def execute
- # The ordering of importing is important here due to the way GitHub structures their data
- # 1. Labels are required by other items while not having a dependency on anything else
- # so need to be first
- # 2. Pull requests must come before issues. Every pull request is also an issue but not
- # all issues are pull requests. Only the issue entity has labels defined in GitHub. GitLab
- # doesn't structure data like this so we need to make sure that we've created the MRs
- # before we attempt to add the labels defined in the GitHub issue for the related, already
- # imported, pull request
- import_labels
- import_milestones
- import_pull_requests
- import_issues
- import_comments(:issues)
- import_comments(:pull_requests)
- import_wiki
-
- # Gitea doesn't have a Release API yet
- # See https://github.com/go-gitea/gitea/issues/330
- unless project.gitea_import?
- import_releases
- end
-
- handle_errors
-
- true
- end
-
- private
-
- def credentials
- return @credentials if defined?(@credentials)
-
- @credentials = project.import_data ? project.import_data.credentials : nil
- end
-
- def handle_errors
- return unless errors.any?
-
- project.update_column(:import_error, {
- message: 'The remote data could not be fully imported.',
- errors: errors
- }.to_json)
- end
-
- def import_labels
- fetch_resources(:labels, repo, per_page: 100) do |labels|
- labels.each do |raw|
- begin
- gh_label = LabelFormatter.new(project, raw)
- gh_label.create!
- rescue => e
- errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(gh_label.url), errors: e.message }
- end
- end
- end
-
- cache_labels!
- end
-
- def import_milestones
- fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones|
- milestones.each do |raw|
- begin
- gh_milestone = MilestoneFormatter.new(project, raw)
- gh_milestone.create!
- rescue => e
- errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(gh_milestone.url), errors: e.message }
- end
- end
- end
- end
-
- def import_issues
- fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
- issues.each do |raw|
- gh_issue = IssueFormatter.new(project, raw, client)
-
- begin
- issuable =
- if gh_issue.pull_request?
- MergeRequest.find_by(target_project_id: project.id, iid: gh_issue.number)
- else
- gh_issue.create!
- end
-
- apply_labels(issuable, raw)
- rescue => e
- errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(gh_issue.url), errors: e.message }
- end
- end
- end
- end
-
- def import_pull_requests
- fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
- pull_requests.each do |raw|
- gh_pull_request = PullRequestFormatter.new(project, raw, client)
-
- next unless gh_pull_request.valid?
-
- begin
- restore_source_branch(gh_pull_request) unless gh_pull_request.source_branch_exists?
- restore_target_branch(gh_pull_request) unless gh_pull_request.target_branch_exists?
-
- merge_request = gh_pull_request.create!
-
- # Gitea doesn't return PR in the Issue API endpoint, so labels must be assigned at this stage
- if project.gitea_import?
- apply_labels(merge_request, raw)
- end
- rescue => e
- errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(gh_pull_request.url), errors: e.message }
- ensure
- clean_up_restored_branches(gh_pull_request)
- end
- end
- end
-
- project.repository.after_remove_branch
- end
-
- def restore_source_branch(pull_request)
- project.repository.create_branch(pull_request.source_branch_name, pull_request.source_branch_sha)
- end
-
- def restore_target_branch(pull_request)
- project.repository.create_branch(pull_request.target_branch_name, pull_request.target_branch_sha)
- end
-
- def remove_branch(name)
- project.repository.delete_branch(name)
- rescue Gitlab::Git::Repository::DeleteBranchFailed
- errors << { type: :remove_branch, name: name }
- end
-
- def clean_up_restored_branches(pull_request)
- return if pull_request.opened?
-
- remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists?
- remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists?
- end
-
- def apply_labels(issuable, raw)
- return unless raw.labels.count > 0
-
- label_ids = raw.labels
- .map { |attrs| @labels[attrs.name] }
- .compact
-
- issuable.update_attribute(:label_ids, label_ids)
- end
-
- def import_comments(issuable_type)
- resource_type = "#{issuable_type}_comments".to_sym
-
- # Two notes here:
- # 1. We don't have a distinctive attribute for comments (unlike issues iid), so we fetch the last inserted note,
- # compare it against every comment in the current imported page until we find match, and that's where start importing
- # 2. GH returns comments for _both_ issues and PRs through issues_comments API, while pull_requests_comments returns
- # only comments on diffs, so select last note not based on noteable_type but on line_code
- line_code_is = issuable_type == :pull_requests ? 'NOT NULL' : 'NULL'
- last_note = project.notes.where("line_code IS #{line_code_is}").last
-
- fetch_resources(resource_type, repo, per_page: 100) do |comments|
- if last_note
- discard_inserted_comments(comments, last_note)
- last_note = nil
- end
-
- create_comments(comments)
- end
- end
-
- def create_comments(comments)
- ActiveRecord::Base.no_touching do
- comments.each do |raw|
- begin
- comment = CommentFormatter.new(project, raw, client)
-
- # GH does not return info about comment's parent, so we guess it by checking its URL!
- *_, parent, iid = URI(raw.html_url).path.split('/')
-
- issuable = if parent == 'issues'
- Issue.find_by(project_id: project.id, iid: iid)
- else
- MergeRequest.find_by(target_project_id: project.id, iid: iid)
- end
-
- next unless issuable
-
- issuable.notes.create!(comment.attributes)
- rescue => e
- errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
- end
- end
- end
- end
-
- def discard_inserted_comments(comments, last_note)
- last_note_attrs = nil
-
- cut_off_index = comments.find_index do |raw|
- comment = CommentFormatter.new(project, raw)
- comment_attrs = comment.attributes
- last_note_attrs ||= last_note.slice(*comment_attrs.keys)
-
- comment_attrs.with_indifferent_access == last_note_attrs
- end
-
- # No matching resource in the collection, which means we got halted right on the end of the last page, so all good
- return unless cut_off_index
-
- # Otherwise, remove the resources we've already inserted
- comments.shift(cut_off_index + 1)
- end
-
- def import_wiki
- unless project.wiki.repository_exists?
- wiki = WikiFormatter.new(project)
- gitlab_shell.import_repository(project.repository_storage_path, wiki.disk_path, wiki.import_url)
- end
- rescue Gitlab::Shell::Error => e
- # GitHub error message when the wiki repo has not been created,
- # this means that repo has wiki enabled, but have no pages. So,
- # we can skip the import.
- if e.message !~ /repository not exported/
- errors << { type: :wiki, errors: e.message }
- end
- end
-
- def import_releases
- fetch_resources(:releases, repo, per_page: 100) do |releases|
- releases.each do |raw|
- begin
- gh_release = ReleaseFormatter.new(project, raw)
- gh_release.create! if gh_release.valid?
- rescue => e
- errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(gh_release.url), errors: e.message }
- end
- end
- end
- end
-
- def cache_labels!
- project.labels.select(:id, :title).find_each do |label|
- @labels[label.title] = label.id
- end
- end
-
- def fetch_resources(resource_type, *opts)
- return if imported?(resource_type)
-
- opts.last[:page] = current_page(resource_type)
-
- client.public_send(resource_type, *opts) do |resources| # rubocop:disable GitlabSecurity/PublicSend
- yield resources
- increment_page(resource_type)
- end
-
- imported!(resource_type)
- end
-
- def imported?(resource_type)
- Rails.cache.read("#{cache_key_prefix}:#{resource_type}:imported")
- end
-
- def imported!(resource_type)
- Rails.cache.write("#{cache_key_prefix}:#{resource_type}:imported", true, ex: 1.day)
- end
-
- def increment_page(resource_type)
- key = "#{cache_key_prefix}:#{resource_type}:current-page"
-
- # Rails.cache.increment calls INCRBY directly on the value stored under the key, which is
- # a serialized ActiveSupport::Cache::Entry, so it will return an error by Redis, hence this ugly work-around
- page = Rails.cache.read(key)
- page += 1
- Rails.cache.write(key, page)
-
- page
- end
-
- def current_page(resource_type)
- Rails.cache.fetch("#{cache_key_prefix}:#{resource_type}:current-page", ex: 1.day) { 1 }
- end
-
- def cache_key_prefix
- @cache_key_prefix ||= "github-import:#{project.id}"
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/importer/diff_note_importer.rb b/lib/gitlab/github_import/importer/diff_note_importer.rb
new file mode 100644
index 00000000000..8274f37d358
--- /dev/null
+++ b/lib/gitlab/github_import/importer/diff_note_importer.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class DiffNoteImporter
+ attr_reader :note, :project, :client, :user_finder
+
+ # note - An instance of `Gitlab::GithubImport::Representation::DiffNote`.
+ # project - An instance of `Project`.
+ # client - An instance of `Gitlab::GithubImport::Client`.
+ def initialize(note, project, client)
+ @note = note
+ @project = project
+ @client = client
+ @user_finder = UserFinder.new(project, client)
+ end
+
+ def execute
+ return unless (mr_id = find_merge_request_id)
+
+ author_id, author_found = user_finder.author_id_for(note)
+
+ note_body =
+ MarkdownText.format(note.note, note.author, author_found)
+
+ attributes = {
+ noteable_type: 'MergeRequest',
+ noteable_id: mr_id,
+ project_id: project.id,
+ author_id: author_id,
+ note: note_body,
+ system: false,
+ commit_id: note.commit_id,
+ line_code: note.line_code,
+ type: 'LegacyDiffNote',
+ created_at: note.created_at,
+ updated_at: note.updated_at,
+ st_diff: note.diff_hash.to_yaml
+ }
+
+ # It's possible that during an import we'll insert tens of thousands
+ # of diff notes. If we were to use the Note/LegacyDiffNote model here
+ # we'd also have to run additional queries for both validations and
+ # callbacks, putting a lot of pressure on the database.
+ #
+ # To work around this we're using bulk_insert with a single row. This
+ # allows us to efficiently insert data (even if it's just 1 row)
+ # without having to use all sorts of hacks to disable callbacks.
+ Gitlab::Database.bulk_insert(LegacyDiffNote.table_name, [attributes])
+ rescue ActiveRecord::InvalidForeignKey
+ # It's possible the project and the issue have been deleted since
+ # scheduling this job. In this case we'll just skip creating the note.
+ end
+
+ # Returns the ID of the merge request this note belongs to.
+ def find_merge_request_id
+ GithubImport::IssuableFinder.new(project, note).database_id
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/diff_notes_importer.rb b/lib/gitlab/github_import/importer/diff_notes_importer.rb
new file mode 100644
index 00000000000..966f12c5c2f
--- /dev/null
+++ b/lib/gitlab/github_import/importer/diff_notes_importer.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class DiffNotesImporter
+ include ParallelScheduling
+
+ def representation_class
+ Representation::DiffNote
+ end
+
+ def importer_class
+ DiffNoteImporter
+ end
+
+ def sidekiq_worker_class
+ ImportDiffNoteWorker
+ end
+
+ def collection_method
+ :pull_requests_comments
+ end
+
+ def id_for_already_imported_cache(note)
+ note.id
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/issue_and_label_links_importer.rb b/lib/gitlab/github_import/importer/issue_and_label_links_importer.rb
new file mode 100644
index 00000000000..bad064b76c8
--- /dev/null
+++ b/lib/gitlab/github_import/importer/issue_and_label_links_importer.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class IssueAndLabelLinksImporter
+ attr_reader :issue, :project, :client
+
+ # issue - An instance of `Gitlab::GithubImport::Representation::Issue`.
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(issue, project, client)
+ @issue = issue
+ @project = project
+ @client = client
+ end
+
+ def execute
+ IssueImporter.import_if_issue(issue, project, client)
+ LabelLinksImporter.new(issue, project, client).execute
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb
new file mode 100644
index 00000000000..31fefebf787
--- /dev/null
+++ b/lib/gitlab/github_import/importer/issue_importer.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class IssueImporter
+ attr_reader :project, :issue, :client, :user_finder, :milestone_finder,
+ :issuable_finder
+
+ # Imports an issue if it's a regular issue and not a pull request.
+ def self.import_if_issue(issue, project, client)
+ new(issue, project, client).execute unless issue.pull_request?
+ end
+
+ # issue - An instance of `Gitlab::GithubImport::Representation::Issue`.
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(issue, project, client)
+ @issue = issue
+ @project = project
+ @client = client
+ @user_finder = UserFinder.new(project, client)
+ @milestone_finder = MilestoneFinder.new(project)
+ @issuable_finder = GithubImport::IssuableFinder.new(project, issue)
+ end
+
+ def execute
+ Issue.transaction do
+ if (issue_id = create_issue)
+ create_assignees(issue_id)
+ issuable_finder.cache_database_id(issue_id)
+ end
+ end
+ end
+
+ # Creates a new GitLab issue for the current GitHub issue.
+ #
+ # Returns the ID of the created issue as an Integer. If the issue
+ # couldn't be created this method will return `nil` instead.
+ def create_issue
+ author_id, author_found = user_finder.author_id_for(issue)
+
+ description =
+ MarkdownText.format(issue.description, issue.author, author_found)
+
+ attributes = {
+ iid: issue.iid,
+ title: issue.truncated_title,
+ author_id: author_id,
+ project_id: project.id,
+ description: description,
+ milestone_id: milestone_finder.id_for(issue),
+ state: issue.state,
+ created_at: issue.created_at,
+ updated_at: issue.updated_at
+ }
+
+ GithubImport.insert_and_return_id(attributes, project.issues)
+ rescue ActiveRecord::InvalidForeignKey
+ # It's possible the project has been deleted since scheduling this
+ # job. In this case we'll just skip creating the issue.
+ end
+
+ # Stores all issue assignees in the database.
+ #
+ # issue_id - The ID of the created issue.
+ def create_assignees(issue_id)
+ assignees = []
+
+ issue.assignees.each do |assignee|
+ if (user_id = user_finder.user_id_for(assignee))
+ assignees << { issue_id: issue_id, user_id: user_id }
+ end
+ end
+
+ Gitlab::Database.bulk_insert(IssueAssignee.table_name, assignees)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/issues_importer.rb b/lib/gitlab/github_import/importer/issues_importer.rb
new file mode 100644
index 00000000000..ac6d0666b3a
--- /dev/null
+++ b/lib/gitlab/github_import/importer/issues_importer.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class IssuesImporter
+ include ParallelScheduling
+
+ def importer_class
+ IssueAndLabelLinksImporter
+ end
+
+ def representation_class
+ Representation::Issue
+ end
+
+ def sidekiq_worker_class
+ ImportIssueWorker
+ end
+
+ def collection_method
+ :issues
+ end
+
+ def id_for_already_imported_cache(issue)
+ issue.number
+ end
+
+ def collection_options
+ { state: 'all', sort: 'created', direction: 'asc' }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/label_links_importer.rb b/lib/gitlab/github_import/importer/label_links_importer.rb
new file mode 100644
index 00000000000..2001b7e3482
--- /dev/null
+++ b/lib/gitlab/github_import/importer/label_links_importer.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class LabelLinksImporter
+ attr_reader :issue, :project, :client, :label_finder
+
+ # issue - An instance of `Gitlab::GithubImport::Representation::Issue`
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(issue, project, client)
+ @issue = issue
+ @project = project
+ @client = client
+ @label_finder = LabelFinder.new(project)
+ end
+
+ def execute
+ create_labels
+ end
+
+ def create_labels
+ time = Time.zone.now
+ rows = []
+ target_id = find_target_id
+
+ issue.label_names.each do |label_name|
+ # Although unlikely it's technically possible for an issue to be
+ # given a label that was created and assigned after we imported all
+ # the project's labels.
+ next unless (label_id = label_finder.id_for(label_name))
+
+ rows << {
+ label_id: label_id,
+ target_id: target_id,
+ target_type: issue.issuable_type,
+ created_at: time,
+ updated_at: time
+ }
+ end
+
+ Gitlab::Database.bulk_insert(LabelLink.table_name, rows)
+ end
+
+ def find_target_id
+ GithubImport::IssuableFinder.new(project, issue).database_id
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/labels_importer.rb b/lib/gitlab/github_import/importer/labels_importer.rb
new file mode 100644
index 00000000000..a73033d35ba
--- /dev/null
+++ b/lib/gitlab/github_import/importer/labels_importer.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class LabelsImporter
+ include BulkImporting
+
+ attr_reader :project, :client, :existing_labels
+
+ # project - An instance of `Project`.
+ # client - An instance of `Gitlab::GithubImport::Client`.
+ def initialize(project, client)
+ @project = project
+ @client = client
+ @existing_labels = project.labels.pluck(:title).to_set
+ end
+
+ def execute
+ bulk_insert(Label, build_labels)
+ build_labels_cache
+ end
+
+ def build_labels
+ build_database_rows(each_label)
+ end
+
+ def already_imported?(label)
+ existing_labels.include?(label.name)
+ end
+
+ def build_labels_cache
+ LabelFinder.new(project).build_cache
+ end
+
+ def build(label)
+ time = Time.zone.now
+
+ {
+ title: label.name,
+ color: '#' + label.color,
+ project_id: project.id,
+ type: 'ProjectLabel',
+ created_at: time,
+ updated_at: time
+ }
+ end
+
+ def each_label
+ client.labels(project.import_source)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/milestones_importer.rb b/lib/gitlab/github_import/importer/milestones_importer.rb
new file mode 100644
index 00000000000..c53480e828a
--- /dev/null
+++ b/lib/gitlab/github_import/importer/milestones_importer.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class MilestonesImporter
+ include BulkImporting
+
+ attr_reader :project, :client, :existing_milestones
+
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(project, client)
+ @project = project
+ @client = client
+ @existing_milestones = project.milestones.pluck(:iid).to_set
+ end
+
+ def execute
+ bulk_insert(Milestone, build_milestones)
+ build_milestones_cache
+ end
+
+ def build_milestones
+ build_database_rows(each_milestone)
+ end
+
+ def already_imported?(milestone)
+ existing_milestones.include?(milestone.number)
+ end
+
+ def build_milestones_cache
+ MilestoneFinder.new(project).build_cache
+ end
+
+ def build(milestone)
+ {
+ iid: milestone.number,
+ title: milestone.title,
+ description: milestone.description,
+ project_id: project.id,
+ state: state_for(milestone),
+ created_at: milestone.created_at,
+ updated_at: milestone.updated_at
+ }
+ end
+
+ def state_for(milestone)
+ milestone.state == 'open' ? :active : :closed
+ end
+
+ def each_milestone
+ client.milestones(project.import_source, state: 'all')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/note_importer.rb b/lib/gitlab/github_import/importer/note_importer.rb
new file mode 100644
index 00000000000..c890f2df360
--- /dev/null
+++ b/lib/gitlab/github_import/importer/note_importer.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class NoteImporter
+ attr_reader :note, :project, :client, :user_finder
+
+ # note - An instance of `Gitlab::GithubImport::Representation::Note`.
+ # project - An instance of `Project`.
+ # client - An instance of `Gitlab::GithubImport::Client`.
+ def initialize(note, project, client)
+ @note = note
+ @project = project
+ @client = client
+ @user_finder = UserFinder.new(project, client)
+ end
+
+ def execute
+ return unless (noteable_id = find_noteable_id)
+
+ author_id, author_found = user_finder.author_id_for(note)
+
+ note_body =
+ MarkdownText.format(note.note, note.author, author_found)
+
+ attributes = {
+ noteable_type: note.noteable_type,
+ noteable_id: noteable_id,
+ project_id: project.id,
+ author_id: author_id,
+ note: note_body,
+ system: false,
+ created_at: note.created_at,
+ updated_at: note.updated_at
+ }
+
+ # We're using bulk_insert here so we can bypass any validations and
+ # callbacks. Running these would result in a lot of unnecessary SQL
+ # queries being executed when importing large projects.
+ Gitlab::Database.bulk_insert(Note.table_name, [attributes])
+ rescue ActiveRecord::InvalidForeignKey
+ # It's possible the project and the issue have been deleted since
+ # scheduling this job. In this case we'll just skip creating the note.
+ end
+
+ # Returns the ID of the issue or merge request to create the note for.
+ def find_noteable_id
+ GithubImport::IssuableFinder.new(project, note).database_id
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/notes_importer.rb b/lib/gitlab/github_import/importer/notes_importer.rb
new file mode 100644
index 00000000000..5aec760ea5f
--- /dev/null
+++ b/lib/gitlab/github_import/importer/notes_importer.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class NotesImporter
+ include ParallelScheduling
+
+ def importer_class
+ NoteImporter
+ end
+
+ def representation_class
+ Representation::Note
+ end
+
+ def sidekiq_worker_class
+ ImportNoteWorker
+ end
+
+ def collection_method
+ :issues_comments
+ end
+
+ def id_for_already_imported_cache(note)
+ note.id
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/pull_request_importer.rb b/lib/gitlab/github_import/importer/pull_request_importer.rb
new file mode 100644
index 00000000000..49d859f9624
--- /dev/null
+++ b/lib/gitlab/github_import/importer/pull_request_importer.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class PullRequestImporter
+ attr_reader :pull_request, :project, :client, :user_finder,
+ :milestone_finder, :issuable_finder
+
+ # pull_request - An instance of
+ # `Gitlab::GithubImport::Representation::PullRequest`.
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(pull_request, project, client)
+ @pull_request = pull_request
+ @project = project
+ @client = client
+ @user_finder = UserFinder.new(project, client)
+ @milestone_finder = MilestoneFinder.new(project)
+ @issuable_finder =
+ GithubImport::IssuableFinder.new(project, pull_request)
+ end
+
+ def execute
+ if (mr_id = create_merge_request)
+ issuable_finder.cache_database_id(mr_id)
+ end
+ end
+
+ # Creates the merge request and returns its ID.
+ #
+ # This method will return `nil` if the merge request could not be
+ # created.
+ def create_merge_request
+ author_id, author_found = user_finder.author_id_for(pull_request)
+
+ description = MarkdownText
+ .format(pull_request.description, pull_request.author, author_found)
+
+ # This work must be wrapped in a transaction as otherwise we can leave
+ # behind incomplete data in the event of an error. This can then lead
+ # to duplicate key errors when jobs are retried.
+ MergeRequest.transaction do
+ attributes = {
+ iid: pull_request.iid,
+ title: pull_request.truncated_title,
+ description: description,
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: pull_request.formatted_source_branch,
+ target_branch: pull_request.target_branch,
+ state: pull_request.state,
+ milestone_id: milestone_finder.id_for(pull_request),
+ author_id: author_id,
+ assignee_id: user_finder.assignee_id_for(pull_request),
+ created_at: pull_request.created_at,
+ updated_at: pull_request.updated_at
+ }
+
+ # When creating merge requests there are a lot of hooks that may
+ # run, for many different reasons. Many of these hooks (e.g. the
+ # ones used for rendering Markdown) are completely unnecessary and
+ # may even lead to transaction timeouts.
+ #
+ # To ensure importing pull requests has a minimal impact and can
+ # complete in a reasonable time we bypass all the hooks by inserting
+ # the row and then retrieving it. We then only perform the
+ # additional work that is strictly necessary.
+ merge_request_id = GithubImport
+ .insert_and_return_id(attributes, project.merge_requests)
+
+ merge_request = project.merge_requests.find(merge_request_id)
+
+ # These fields are set so we can create the correct merge request
+ # diffs.
+ merge_request.source_branch_sha = pull_request.source_branch_sha
+ merge_request.target_branch_sha = pull_request.target_branch_sha
+
+ merge_request.keep_around_commit
+ merge_request.merge_request_diffs.create
+
+ merge_request.id
+ end
+ rescue ActiveRecord::InvalidForeignKey
+ # It's possible the project has been deleted since scheduling this
+ # job. In this case we'll just skip creating the merge request.
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/pull_requests_importer.rb b/lib/gitlab/github_import/importer/pull_requests_importer.rb
new file mode 100644
index 00000000000..5437e32e9f1
--- /dev/null
+++ b/lib/gitlab/github_import/importer/pull_requests_importer.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class PullRequestsImporter
+ include ParallelScheduling
+
+ def importer_class
+ PullRequestImporter
+ end
+
+ def representation_class
+ Representation::PullRequest
+ end
+
+ def sidekiq_worker_class
+ ImportPullRequestWorker
+ end
+
+ def id_for_already_imported_cache(pr)
+ pr.number
+ end
+
+ def each_object_to_import
+ super do |pr|
+ update_repository if update_repository?(pr)
+ yield pr
+ end
+ end
+
+ def update_repository
+ # We set this column _before_ fetching the repository, and this is
+ # deliberate. If we were to update this column after the fetch we may
+ # miss out on changes pushed during the fetch or between the fetch and
+ # updating the timestamp.
+ project.update_column(:last_repository_updated_at, Time.zone.now)
+
+ project.repository.fetch_remote('github', forced: false)
+
+ pname = project.path_with_namespace
+
+ Rails.logger
+ .info("GitHub importer finished updating repository for #{pname}")
+
+ repository_updates_counter.increment(project: pname)
+ end
+
+ def update_repository?(pr)
+ last_update = project.last_repository_updated_at || project.created_at
+
+ return false if pr.updated_at < last_update
+
+ # PRs may be updated without there actually being new commits, thus we
+ # check to make sure we only re-fetch if truly necessary.
+ !(commit_exists?(pr.head.sha) && commit_exists?(pr.base.sha))
+ end
+
+ def commit_exists?(sha)
+ project.repository.lookup(sha)
+ true
+ rescue Rugged::Error
+ false
+ end
+
+ def collection_method
+ :pull_requests
+ end
+
+ def collection_options
+ { state: 'all', sort: 'created', direction: 'asc' }
+ end
+
+ def repository_updates_counter
+ @repository_updates_counter ||= Gitlab::Metrics.counter(
+ :github_importer_repository_updates,
+ 'The number of times repositories have to be updated again'
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/releases_importer.rb b/lib/gitlab/github_import/importer/releases_importer.rb
new file mode 100644
index 00000000000..100f459fdcc
--- /dev/null
+++ b/lib/gitlab/github_import/importer/releases_importer.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class ReleasesImporter
+ include BulkImporting
+
+ attr_reader :project, :client, :existing_tags
+
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(project, client)
+ @project = project
+ @client = client
+ @existing_tags = project.releases.pluck(:tag).to_set
+ end
+
+ def execute
+ bulk_insert(Release, build_releases)
+ end
+
+ def build_releases
+ build_database_rows(each_release)
+ end
+
+ def already_imported?(release)
+ existing_tags.include?(release.tag_name)
+ end
+
+ def build(release)
+ {
+ tag: release.tag_name,
+ description: description_for(release),
+ created_at: release.created_at,
+ updated_at: release.updated_at,
+ project_id: project.id
+ }
+ end
+
+ def each_release
+ client.releases(project.import_source)
+ end
+
+ def description_for(release)
+ if release.body.present?
+ release.body
+ else
+ "Release for tag #{release.tag_name}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
new file mode 100644
index 00000000000..9cf2e7fd871
--- /dev/null
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Importer
+ class RepositoryImporter
+ include Gitlab::ShellAdapter
+
+ attr_reader :project, :client
+
+ def initialize(project, client)
+ @project = project
+ @client = client
+ end
+
+ # Returns true if we should import the wiki for the project.
+ def import_wiki?
+ client.repository(project.import_source)&.has_wiki &&
+ !project.wiki_repository_exists?
+ end
+
+ # Imports the repository data.
+ #
+ # This method will return true if the data was imported successfully or
+ # the repository had already been imported before.
+ def execute
+ imported =
+ # It's possible a repository has already been imported when running
+ # this code, e.g. because we had to retry this job after
+ # `import_wiki?` raised a rate limit error. In this case we'll skip
+ # re-importing the main repository.
+ if project.repository.empty_repo?
+ import_repository
+ else
+ true
+ end
+
+ update_clone_time if imported
+
+ imported = import_wiki_repository if import_wiki? && imported
+
+ imported
+ end
+
+ def import_repository
+ project.ensure_repository
+
+ refmap = Gitlab::GithubImport.refmap
+ project.repository.fetch_as_mirror(project.import_url, refmap: refmap, forced: true, remote_name: 'github')
+
+ true
+ rescue Gitlab::Git::Repository::NoRepository, Gitlab::Shell::Error => e
+ fail_import("Failed to import the repository: #{e.message}")
+ end
+
+ def import_wiki_repository
+ wiki_path = "#{project.disk_path}.wiki"
+ wiki_url = project.import_url.sub(/\.git\z/, '.wiki.git')
+ storage_path = project.repository_storage_path
+
+ gitlab_shell.import_repository(storage_path, wiki_path, wiki_url)
+
+ true
+ rescue Gitlab::Shell::Error => e
+ if e.message !~ /repository not exported/
+ fail_import("Failed to import the wiki: #{e.message}")
+ else
+ true
+ end
+ end
+
+ def update_clone_time
+ project.update_column(:last_repository_updated_at, Time.zone.now)
+ end
+
+ def fail_import(message)
+ project.mark_import_as_failed(message)
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/issuable_finder.rb b/lib/gitlab/github_import/issuable_finder.rb
new file mode 100644
index 00000000000..211915f1d87
--- /dev/null
+++ b/lib/gitlab/github_import/issuable_finder.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # IssuableFinder can be used for caching and retrieving database IDs for
+ # issuable objects such as issues and pull requests. By caching these IDs we
+ # remove the need for running a lot of database queries when importing
+ # GitHub projects.
+ class IssuableFinder
+ attr_reader :project, :object
+
+ # The base cache key to use for storing/retrieving issuable IDs.
+ CACHE_KEY = 'github-import/issuable-finder/%{project}/%{type}/%{iid}'.freeze
+
+ # project - An instance of `Project`.
+ # object - The object to look up or set a database ID for.
+ def initialize(project, object)
+ @project = project
+ @object = object
+ end
+
+ # Returns the database ID for the object.
+ #
+ # This method will return `nil` if no ID could be found.
+ def database_id
+ val = Caching.read(cache_key)
+
+ val.to_i if val.present?
+ end
+
+ # Associates the given database ID with the current object.
+ #
+ # database_id - The ID of the corresponding database row.
+ def cache_database_id(database_id)
+ Caching.write(cache_key, database_id)
+ end
+
+ private
+
+ def cache_key
+ CACHE_KEY % {
+ project: project.id,
+ type: cache_key_type,
+ iid: cache_key_iid
+ }
+ end
+
+ # Returns the identifier to use for cache keys.
+ #
+ # For issues and pull requests this will be "Issue" or "MergeRequest"
+ # respectively. For diff notes this will return "MergeRequest", for
+ # regular notes it will either return "Issue" or "MergeRequest" depending
+ # on what type of object the note belongs to.
+ def cache_key_type
+ if object.respond_to?(:issuable_type)
+ object.issuable_type
+ elsif object.respond_to?(:noteable_type)
+ object.noteable_type
+ else
+ raise(
+ TypeError,
+ "Instances of #{object.class} are not supported"
+ )
+ end
+ end
+
+ def cache_key_iid
+ if object.respond_to?(:noteable_id)
+ object.noteable_id
+ elsif object.respond_to?(:iid)
+ object.iid
+ else
+ raise(
+ TypeError,
+ "Instances of #{object.class} are not supported"
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/issuable_formatter.rb b/lib/gitlab/github_import/issuable_formatter.rb
deleted file mode 100644
index 27b171d6ddb..00000000000
--- a/lib/gitlab/github_import/issuable_formatter.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-module Gitlab
- module GithubImport
- class IssuableFormatter < BaseFormatter
- attr_writer :assignee_id, :author_id
-
- def project_association
- raise NotImplementedError
- end
-
- delegate :number, to: :raw_data
-
- def find_condition
- { iid: number }
- end
-
- private
-
- def state
- raw_data.state == 'closed' ? 'closed' : 'opened'
- end
-
- def assigned?
- raw_data.assignee.present?
- end
-
- def author
- @author ||= UserFormatter.new(client, raw_data.user)
- end
-
- def author_id
- @author_id ||= author.gitlab_id || project.creator_id
- end
-
- def assignee
- if assigned?
- @assignee ||= UserFormatter.new(client, raw_data.assignee)
- end
- end
-
- def assignee_id
- return @assignee_id if defined?(@assignee_id)
-
- @assignee_id = assignee.try(:gitlab_id)
- end
-
- def body
- raw_data.body || ""
- end
-
- def description
- if author.gitlab_id
- body
- else
- formatter.author_line(author.login) + body
- end
- end
-
- def milestone
- if raw_data.milestone.present?
- milestone = MilestoneFormatter.new(project, raw_data.milestone)
- project.milestones.find_by(milestone.find_condition)
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
deleted file mode 100644
index 977cd0423ba..00000000000
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-module Gitlab
- module GithubImport
- class IssueFormatter < IssuableFormatter
- def attributes
- {
- iid: number,
- project: project,
- milestone: milestone,
- title: raw_data.title,
- description: description,
- state: state,
- author_id: author_id,
- assignee_ids: Array(assignee_id),
- created_at: raw_data.created_at,
- updated_at: raw_data.updated_at
- }
- end
-
- def has_comments?
- raw_data.comments > 0
- end
-
- def project_association
- :issues
- end
-
- def pull_request?
- raw_data.pull_request.present?
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/label_finder.rb b/lib/gitlab/github_import/label_finder.rb
new file mode 100644
index 00000000000..9be071141db
--- /dev/null
+++ b/lib/gitlab/github_import/label_finder.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class LabelFinder
+ attr_reader :project
+
+ # The base cache key to use for storing/retrieving label IDs.
+ CACHE_KEY = 'github-import/label-finder/%{project}/%{name}'.freeze
+
+ # project - An instance of `Project`.
+ def initialize(project)
+ @project = project
+ end
+
+ # Returns the label ID for the given name.
+ def id_for(name)
+ Caching.read_integer(cache_key_for(name))
+ end
+
+ def build_cache
+ mapping = @project
+ .labels
+ .pluck(:id, :name)
+ .each_with_object({}) do |(id, name), hash|
+ hash[cache_key_for(name)] = id
+ end
+
+ Caching.write_multiple(mapping)
+ end
+
+ def cache_key_for(name)
+ CACHE_KEY % { project: project.id, name: name }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb
deleted file mode 100644
index 211ccdc51bb..00000000000
--- a/lib/gitlab/github_import/label_formatter.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-module Gitlab
- module GithubImport
- class LabelFormatter < BaseFormatter
- def attributes
- {
- project: project,
- title: title,
- color: color
- }
- end
-
- def project_association
- :labels
- end
-
- def create!
- params = attributes.except(:project)
- service = ::Labels::FindOrCreateService.new(nil, project, params)
- label = service.execute(skip_authorization: true)
-
- raise ActiveRecord::RecordInvalid.new(label) unless label.persisted?
-
- label
- end
-
- private
-
- def color
- "##{raw_data.color}"
- end
-
- def title
- raw_data.name
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/markdown_text.rb b/lib/gitlab/github_import/markdown_text.rb
new file mode 100644
index 00000000000..b25c4f7becf
--- /dev/null
+++ b/lib/gitlab/github_import/markdown_text.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class MarkdownText
+ attr_reader :text, :author, :exists
+
+ def self.format(*args)
+ new(*args).to_s
+ end
+
+ # text - The Markdown text as a String.
+ # author - An instance of `Gitlab::GithubImport::Representation::User`
+ # exists - Boolean that indicates the user exists in the GitLab database.
+ def initialize(text, author, exists = false)
+ @text = text
+ @author = author
+ @exists = exists
+ end
+
+ def to_s
+ if exists
+ text
+ else
+ "*Created by: #{author.login}*\n\n#{text}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/milestone_finder.rb b/lib/gitlab/github_import/milestone_finder.rb
new file mode 100644
index 00000000000..208d15dc144
--- /dev/null
+++ b/lib/gitlab/github_import/milestone_finder.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ class MilestoneFinder
+ attr_reader :project
+
+ # The base cache key to use for storing/retrieving milestone IDs.
+ CACHE_KEY = 'github-import/milestone-finder/%{project}/%{iid}'.freeze
+
+ # project - An instance of `Project`
+ def initialize(project)
+ @project = project
+ end
+
+ # issuable - An instance of `Gitlab::GithubImport::Representation::Issue`
+ # or `Gitlab::GithubImport::Representation::PullRequest`.
+ def id_for(issuable)
+ return unless issuable.milestone_number
+
+ Caching.read_integer(cache_key_for(issuable.milestone_number))
+ end
+
+ def build_cache
+ mapping = @project
+ .milestones
+ .pluck(:id, :iid)
+ .each_with_object({}) do |(id, iid), hash|
+ hash[cache_key_for(iid)] = id
+ end
+
+ Caching.write_multiple(mapping)
+ end
+
+ def cache_key_for(iid)
+ CACHE_KEY % { project: project.id, iid: iid }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb
deleted file mode 100644
index dd782eff059..00000000000
--- a/lib/gitlab/github_import/milestone_formatter.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-module Gitlab
- module GithubImport
- class MilestoneFormatter < BaseFormatter
- def attributes
- {
- iid: number,
- project: project,
- title: raw_data.title,
- description: raw_data.description,
- due_date: raw_data.due_on,
- state: state,
- created_at: raw_data.created_at,
- updated_at: raw_data.updated_at
- }
- end
-
- def project_association
- :milestones
- end
-
- def find_condition
- { iid: number }
- end
-
- def number
- if project.gitea_import?
- raw_data.id
- else
- raw_data.number
- end
- end
-
- private
-
- def state
- raw_data.state == 'closed' ? 'closed' : 'active'
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/page_counter.rb b/lib/gitlab/github_import/page_counter.rb
new file mode 100644
index 00000000000..c3db2d0b469
--- /dev/null
+++ b/lib/gitlab/github_import/page_counter.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # PageCounter can be used to keep track of the last imported page of a
+ # collection, allowing workers to resume where they left off in the event of
+ # an error.
+ class PageCounter
+ attr_reader :cache_key
+
+ # The base cache key to use for storing the last page number.
+ CACHE_KEY = 'github-importer/page-counter/%{project}/%{collection}'.freeze
+
+ def initialize(project, collection)
+ @cache_key = CACHE_KEY % { project: project.id, collection: collection }
+ end
+
+ # Sets the page number to the given value.
+ #
+ # Returns true if the page number was overwritten, false otherwise.
+ def set(page)
+ Caching.write_if_greater(cache_key, page)
+ end
+
+ # Returns the current value from the cache.
+ def current
+ Caching.read_integer(cache_key) || 1
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/parallel_importer.rb b/lib/gitlab/github_import/parallel_importer.rb
new file mode 100644
index 00000000000..6da11e6ef08
--- /dev/null
+++ b/lib/gitlab/github_import/parallel_importer.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # The ParallelImporter schedules the importing of a GitHub project using
+ # Sidekiq.
+ class ParallelImporter
+ attr_reader :project
+
+ def self.async?
+ true
+ end
+
+ def self.imports_repository?
+ true
+ end
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ jid = generate_jid
+
+ # The original import JID is the JID of the RepositoryImportWorker job,
+ # which will be removed once that job completes. Reusing that JID could
+ # result in StuckImportJobsWorker marking the job as stuck before we get
+ # to running Stage::ImportRepositoryWorker.
+ #
+ # We work around this by setting the JID to a custom generated one, then
+ # refreshing it in the various stages whenever necessary.
+ Gitlab::SidekiqStatus
+ .set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+
+ project.update_column(:import_jid, jid)
+
+ Stage::ImportRepositoryWorker
+ .perform_async(project.id)
+
+ true
+ end
+
+ def generate_jid
+ "github-importer/#{project.id}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/parallel_scheduling.rb b/lib/gitlab/github_import/parallel_scheduling.rb
new file mode 100644
index 00000000000..d4d1357f5a3
--- /dev/null
+++ b/lib/gitlab/github_import/parallel_scheduling.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module ParallelScheduling
+ attr_reader :project, :client, :page_counter, :already_imported_cache_key
+
+ # The base cache key to use for tracking already imported objects.
+ ALREADY_IMPORTED_CACHE_KEY =
+ 'github-importer/already-imported/%{project}/%{collection}'.freeze
+
+ # project - An instance of `Project`.
+ # client - An instance of `Gitlab::GithubImport::Client`.
+ # parallel - When set to true the objects will be imported in parallel.
+ def initialize(project, client, parallel: true)
+ @project = project
+ @client = client
+ @parallel = parallel
+ @page_counter = PageCounter.new(project, collection_method)
+ @already_imported_cache_key = ALREADY_IMPORTED_CACHE_KEY %
+ { project: project.id, collection: collection_method }
+ end
+
+ def parallel?
+ @parallel
+ end
+
+ def execute
+ retval =
+ if parallel?
+ parallel_import
+ else
+ sequential_import
+ end
+
+ # Once we have completed all work we can remove our "already exists"
+ # cache so we don't put too much pressure on Redis.
+ #
+ # We don't immediately remove it since it's technically possible for
+ # other instances of this job to still run, instead we set the
+ # expiration time to a lower value. This prevents the other jobs from
+ # still scheduling duplicates while. Since all work has already been
+ # completed those jobs will just cycle through any remaining pages while
+ # not scheduling anything.
+ Caching.expire(already_imported_cache_key, 15.minutes.to_i)
+
+ retval
+ end
+
+ # Imports all the objects in sequence in the current thread.
+ def sequential_import
+ each_object_to_import do |object|
+ repr = representation_class.from_api_response(object)
+
+ importer_class.new(repr, project, client).execute
+ end
+ end
+
+ # Imports all objects in parallel by scheduling a Sidekiq job for every
+ # individual object.
+ def parallel_import
+ waiter = JobWaiter.new
+
+ each_object_to_import do |object|
+ repr = representation_class.from_api_response(object)
+
+ sidekiq_worker_class
+ .perform_async(project.id, repr.to_hash, waiter.key)
+
+ waiter.jobs_remaining += 1
+ end
+
+ waiter
+ end
+
+ # The method that will be called for traversing through all the objects to
+ # import, yielding them to the supplied block.
+ def each_object_to_import
+ repo = project.import_source
+
+ # We inject the page number here to make sure that all importers always
+ # start where they left off. Simply starting over wouldn't work for
+ # repositories with a lot of data (e.g. tens of thousands of comments).
+ options = collection_options.merge(page: page_counter.current)
+
+ client.each_page(collection_method, repo, options) do |page|
+ # Technically it's possible that the same work is performed multiple
+ # times, as Sidekiq doesn't guarantee there will ever only be one
+ # instance of a job. In such a scenario it's possible for one job to
+ # have a lower page number (e.g. 5) compared to another (e.g. 10). In
+ # this case we skip over all the objects until we have caught up,
+ # reducing the number of duplicate jobs scheduled by the provided
+ # block.
+ next unless page_counter.set(page.number)
+
+ page.objects.each do |object|
+ next if already_imported?(object)
+
+ yield object
+
+ # We mark the object as imported immediately so we don't end up
+ # scheduling it multiple times.
+ mark_as_imported(object)
+ end
+ end
+ end
+
+ # Returns true if the given object has already been imported, false
+ # otherwise.
+ #
+ # object - The object to check.
+ def already_imported?(object)
+ id = id_for_already_imported_cache(object)
+
+ Caching.set_includes?(already_imported_cache_key, id)
+ end
+
+ # Marks the given object as "already imported".
+ def mark_as_imported(object)
+ id = id_for_already_imported_cache(object)
+
+ Caching.set_add(already_imported_cache_key, id)
+ end
+
+ # Returns the ID to use for the cache used for checking if an object has
+ # already been imported or not.
+ #
+ # object - The object we may want to import.
+ def id_for_already_imported_cache(object)
+ raise NotImplementedError
+ end
+
+ # The class used for converting API responses to Hashes when performing
+ # the import.
+ def representation_class
+ raise NotImplementedError
+ end
+
+ # The class to use for importing objects when importing them sequentially.
+ def importer_class
+ raise NotImplementedError
+ end
+
+ # The Sidekiq worker class used for scheduling the importing of objects in
+ # parallel.
+ def sidekiq_worker_class
+ raise NotImplementedError
+ end
+
+ # The name of the method to call to retrieve the data to import.
+ def collection_method
+ raise NotImplementedError
+ end
+
+ # Any options to be passed to the method used for retrieving the data to
+ # import.
+ def collection_options
+ {}
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
deleted file mode 100644
index a55adc9b1c8..00000000000
--- a/lib/gitlab/github_import/project_creator.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-module Gitlab
- module GithubImport
- class ProjectCreator
- include Gitlab::CurrentSettings
-
- attr_reader :repo, :name, :namespace, :current_user, :session_data, :type
-
- def initialize(repo, name, namespace, current_user, session_data, type: 'github')
- @repo = repo
- @name = name
- @namespace = namespace
- @current_user = current_user
- @session_data = session_data
- @type = type
- end
-
- def execute
- ::Projects::CreateService.new(
- current_user,
- name: name,
- path: name,
- description: repo.description,
- namespace_id: namespace.id,
- visibility_level: visibility_level,
- import_type: type,
- import_source: repo.full_name,
- import_url: import_url,
- skip_wiki: skip_wiki
- ).execute
- end
-
- private
-
- def import_url
- repo.clone_url.sub('://', "://#{session_data[:github_access_token]}@")
- end
-
- def visibility_level
- repo.private ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility
- end
-
- #
- # If the GitHub project repository has wiki, we should not create the
- # default wiki. Otherwise the GitHub importer will fail because the wiki
- # repository already exist.
- #
- def skip_wiki
- repo.has_wiki?
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
deleted file mode 100644
index 150afa31432..00000000000
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-module Gitlab
- module GithubImport
- class PullRequestFormatter < IssuableFormatter
- delegate :user, :project, :ref, :repo, :sha, to: :source_branch, prefix: true
- delegate :user, :exists?, :project, :ref, :repo, :sha, :short_sha, to: :target_branch, prefix: true
-
- def attributes
- {
- iid: number,
- title: raw_data.title,
- description: description,
- source_project: source_branch_project,
- source_branch: source_branch_name,
- source_branch_sha: source_branch_sha,
- target_project: target_branch_project,
- target_branch: target_branch_name,
- target_branch_sha: target_branch_sha,
- state: state,
- milestone: milestone,
- author_id: author_id,
- assignee_id: assignee_id,
- created_at: raw_data.created_at,
- updated_at: raw_data.updated_at,
- imported: true
- }
- end
-
- def project_association
- :merge_requests
- end
-
- def valid?
- source_branch.valid? && target_branch.valid?
- end
-
- def source_branch
- @source_branch ||= BranchFormatter.new(project, raw_data.head)
- end
-
- def source_branch_name
- @source_branch_name ||=
- if cross_project? || !source_branch_exists?
- source_branch_name_prefixed
- else
- source_branch_ref
- end
- end
-
- def source_branch_name_prefixed
- "gh-#{target_branch_short_sha}/#{number}/#{source_branch_user}/#{source_branch_ref}"
- end
-
- def source_branch_exists?
- !cross_project? && source_branch.exists?
- end
-
- def target_branch
- @target_branch ||= BranchFormatter.new(project, raw_data.base)
- end
-
- def target_branch_name
- @target_branch_name ||= target_branch_exists? ? target_branch_ref : target_branch_name_prefixed
- end
-
- def target_branch_name_prefixed
- "gl-#{target_branch_short_sha}/#{number}/#{target_branch_user}/#{target_branch_ref}"
- end
-
- def cross_project?
- return true if source_branch_repo.nil?
-
- source_branch_repo.id != target_branch_repo.id
- end
-
- def opened?
- state == 'opened'
- end
-
- private
-
- def state
- if raw_data.state == 'closed' && raw_data.merged_at.present?
- 'merged'
- else
- super
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/rate_limit_error.rb b/lib/gitlab/github_import/rate_limit_error.rb
new file mode 100644
index 00000000000..cc2de909c29
--- /dev/null
+++ b/lib/gitlab/github_import/rate_limit_error.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # Error that will be raised when we're about to reach (or have reached) the
+ # GitHub API's rate limit.
+ RateLimitError = Class.new(StandardError)
+ end
+end
diff --git a/lib/gitlab/github_import/release_formatter.rb b/lib/gitlab/github_import/release_formatter.rb
deleted file mode 100644
index 1ad702a6058..00000000000
--- a/lib/gitlab/github_import/release_formatter.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Gitlab
- module GithubImport
- class ReleaseFormatter < BaseFormatter
- def attributes
- {
- project: project,
- tag: raw_data.tag_name,
- description: raw_data.body,
- created_at: raw_data.created_at,
- updated_at: raw_data.created_at
- }
- end
-
- def project_association
- :releases
- end
-
- def find_condition
- { tag: raw_data.tag_name }
- end
-
- def valid?
- !raw_data.draft
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/representation.rb b/lib/gitlab/github_import/representation.rb
new file mode 100644
index 00000000000..639477ef2a2
--- /dev/null
+++ b/lib/gitlab/github_import/representation.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ TIMESTAMP_KEYS = %i[created_at updated_at merged_at].freeze
+
+ # Converts a Hash with String based keys to one that can be used by the
+ # various Representation classes.
+ #
+ # Example:
+ #
+ # Representation.symbolize_hash('number' => 10) # => { number: 10 }
+ def self.symbolize_hash(raw_hash = nil)
+ hash = raw_hash.deep_symbolize_keys
+
+ TIMESTAMP_KEYS.each do |key|
+ hash[key] = Time.parse(hash[key]) if hash[key].is_a?(String)
+ end
+
+ hash
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/diff_note.rb b/lib/gitlab/github_import/representation/diff_note.rb
new file mode 100644
index 00000000000..bb7439a0641
--- /dev/null
+++ b/lib/gitlab/github_import/representation/diff_note.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ class DiffNote
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :noteable_type, :noteable_id, :commit_id, :file_path,
+ :diff_hunk, :author, :note, :created_at, :updated_at,
+ :github_id
+
+ NOTEABLE_ID_REGEX = /\/pull\/(?<iid>\d+)/i
+
+ # Builds a diff note from a GitHub API response.
+ #
+ # note - An instance of `Sawyer::Resource` containing the note details.
+ def self.from_api_response(note)
+ matches = note.html_url.match(NOTEABLE_ID_REGEX)
+
+ unless matches
+ raise(
+ ArgumentError,
+ "The note URL #{note.html_url.inspect} is not supported"
+ )
+ end
+
+ user = Representation::User.from_api_response(note.user) if note.user
+ hash = {
+ noteable_type: 'MergeRequest',
+ noteable_id: matches[:iid].to_i,
+ file_path: note.path,
+ commit_id: note.commit_id,
+ diff_hunk: note.diff_hunk,
+ author: user,
+ note: note.body,
+ created_at: note.created_at,
+ updated_at: note.updated_at,
+ github_id: note.id
+ }
+
+ new(hash)
+ end
+
+ # Builds a new note using a Hash that was built from a JSON payload.
+ def self.from_json_hash(raw_hash)
+ hash = Representation.symbolize_hash(raw_hash)
+ hash[:author] &&= Representation::User.from_json_hash(hash[:author])
+
+ new(hash)
+ end
+
+ # attributes - A Hash containing the raw note details. The keys of this
+ # Hash must be Symbols.
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def line_code
+ diff_line = Gitlab::Diff::Parser.new.parse(diff_hunk.lines).to_a.last
+
+ Gitlab::Git
+ .diff_line_code(file_path, diff_line.new_pos, diff_line.old_pos)
+ end
+
+ # Returns a Hash that can be used to populate `notes.st_diff`, removing
+ # the need for requesting Git data for every diff note.
+ def diff_hash
+ {
+ diff: diff_hunk,
+ new_path: file_path,
+ old_path: file_path,
+
+ # These fields are not displayed for LegacyDiffNote notes, so it
+ # doesn't really matter what we set them to.
+ a_mode: '100644',
+ b_mode: '100644',
+ new_file: false
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/expose_attribute.rb b/lib/gitlab/github_import/representation/expose_attribute.rb
new file mode 100644
index 00000000000..c3405759631
--- /dev/null
+++ b/lib/gitlab/github_import/representation/expose_attribute.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ module ExposeAttribute
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Defines getter methods for the given attribute names.
+ #
+ # Example:
+ #
+ # expose_attribute :iid, :title
+ def expose_attribute(*names)
+ names.each do |name|
+ name = name.to_sym
+
+ define_method(name) { attributes[name] }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/issue.rb b/lib/gitlab/github_import/representation/issue.rb
new file mode 100644
index 00000000000..f3071b3e2b3
--- /dev/null
+++ b/lib/gitlab/github_import/representation/issue.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ class Issue
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :iid, :title, :description, :milestone_number,
+ :created_at, :updated_at, :state, :assignees,
+ :label_names, :author
+
+ # Builds an issue from a GitHub API response.
+ #
+ # issue - An instance of `Sawyer::Resource` containing the issue
+ # details.
+ def self.from_api_response(issue)
+ user =
+ if issue.user
+ Representation::User.from_api_response(issue.user)
+ end
+
+ hash = {
+ iid: issue.number,
+ title: issue.title,
+ description: issue.body,
+ milestone_number: issue.milestone&.number,
+ state: issue.state == 'open' ? :opened : :closed,
+ assignees: issue.assignees.map do |u|
+ Representation::User.from_api_response(u)
+ end,
+ label_names: issue.labels.map(&:name),
+ author: user,
+ created_at: issue.created_at,
+ updated_at: issue.updated_at,
+ pull_request: issue.pull_request ? true : false
+ }
+
+ new(hash)
+ end
+
+ # Builds a new issue using a Hash that was built from a JSON payload.
+ def self.from_json_hash(raw_hash)
+ hash = Representation.symbolize_hash(raw_hash)
+
+ hash[:state] = hash[:state].to_sym
+ hash[:assignees].map! { |u| Representation::User.from_json_hash(u) }
+ hash[:author] &&= Representation::User.from_json_hash(hash[:author])
+
+ new(hash)
+ end
+
+ # attributes - A hash containing the raw issue details. The keys of this
+ # Hash (and any nested hashes) must be symbols.
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def truncated_title
+ title.truncate(255)
+ end
+
+ def labels?
+ label_names && label_names.any?
+ end
+
+ def pull_request?
+ attributes[:pull_request]
+ end
+
+ def issuable_type
+ pull_request? ? 'MergeRequest' : 'Issue'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/note.rb b/lib/gitlab/github_import/representation/note.rb
new file mode 100644
index 00000000000..a68bc4c002f
--- /dev/null
+++ b/lib/gitlab/github_import/representation/note.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ class Note
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :noteable_id, :noteable_type, :author, :note,
+ :created_at, :updated_at, :github_id
+
+ NOTEABLE_TYPE_REGEX = /\/(?<type>(pull|issues))\/(?<iid>\d+)/i
+
+ # Builds a note from a GitHub API response.
+ #
+ # note - An instance of `Sawyer::Resource` containing the note details.
+ def self.from_api_response(note)
+ matches = note.html_url.match(NOTEABLE_TYPE_REGEX)
+
+ if !matches || !matches[:type]
+ raise(
+ ArgumentError,
+ "The note URL #{note.html_url.inspect} is not supported"
+ )
+ end
+
+ noteable_type =
+ if matches[:type] == 'pull'
+ 'MergeRequest'
+ else
+ 'Issue'
+ end
+
+ user = Representation::User.from_api_response(note.user) if note.user
+ hash = {
+ noteable_type: noteable_type,
+ noteable_id: matches[:iid].to_i,
+ author: user,
+ note: note.body,
+ created_at: note.created_at,
+ updated_at: note.updated_at,
+ github_id: note.id
+ }
+
+ new(hash)
+ end
+
+ # Builds a new note using a Hash that was built from a JSON payload.
+ def self.from_json_hash(raw_hash)
+ hash = Representation.symbolize_hash(raw_hash)
+
+ hash[:author] &&= Representation::User.from_json_hash(hash[:author])
+
+ new(hash)
+ end
+
+ # attributes - A Hash containing the raw note details. The keys of this
+ # Hash must be Symbols.
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ alias_method :issuable_type, :noteable_type
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/pull_request.rb b/lib/gitlab/github_import/representation/pull_request.rb
new file mode 100644
index 00000000000..593b491a837
--- /dev/null
+++ b/lib/gitlab/github_import/representation/pull_request.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ class PullRequest
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :iid, :title, :description, :source_branch,
+ :source_branch_sha, :target_branch, :target_branch_sha,
+ :milestone_number, :author, :assignee, :created_at,
+ :updated_at, :merged_at, :source_repository_id,
+ :target_repository_id, :source_repository_owner
+
+ # Builds a PR from a GitHub API response.
+ #
+ # issue - An instance of `Sawyer::Resource` containing the PR details.
+ def self.from_api_response(pr)
+ assignee =
+ if pr.assignee
+ Representation::User.from_api_response(pr.assignee)
+ end
+
+ user = Representation::User.from_api_response(pr.user) if pr.user
+ hash = {
+ iid: pr.number,
+ title: pr.title,
+ description: pr.body,
+ source_branch: pr.head.ref,
+ target_branch: pr.base.ref,
+ source_branch_sha: pr.head.sha,
+ target_branch_sha: pr.base.sha,
+ source_repository_id: pr.head&.repo&.id,
+ target_repository_id: pr.base&.repo&.id,
+ source_repository_owner: pr.head&.user&.login,
+ state: pr.state == 'open' ? :opened : :closed,
+ milestone_number: pr.milestone&.number,
+ author: user,
+ assignee: assignee,
+ created_at: pr.created_at,
+ updated_at: pr.updated_at,
+ merged_at: pr.merged_at
+ }
+
+ new(hash)
+ end
+
+ # Builds a new PR using a Hash that was built from a JSON payload.
+ def self.from_json_hash(raw_hash)
+ hash = Representation.symbolize_hash(raw_hash)
+
+ hash[:state] = hash[:state].to_sym
+ hash[:author] &&= Representation::User.from_json_hash(hash[:author])
+
+ # Assignees are optional so we only convert it from a Hash if one was
+ # set.
+ hash[:assignee] &&= Representation::User
+ .from_json_hash(hash[:assignee])
+
+ new(hash)
+ end
+
+ # attributes - A Hash containing the raw PR details. The keys of this
+ # Hash (and any nested hashes) must be symbols.
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def truncated_title
+ title.truncate(255)
+ end
+
+ # Returns a formatted source branch.
+ #
+ # For cross-project pull requests the branch name will be in the format
+ # `owner-name:branch-name`.
+ def formatted_source_branch
+ if cross_project? && source_repository_owner
+ "#{source_repository_owner}:#{source_branch}"
+ elsif source_branch == target_branch
+ # Sometimes the source and target branch are the same, but GitLab
+ # doesn't support this. This can happen when both the user and
+ # source repository have been deleted, and the PR was submitted from
+ # the fork's master branch.
+ "#{source_branch}-#{iid}"
+ else
+ source_branch
+ end
+ end
+
+ def state
+ if merged_at
+ :merged
+ else
+ attributes[:state]
+ end
+ end
+
+ def cross_project?
+ return true unless source_repository_id
+
+ source_repository_id != target_repository_id
+ end
+
+ def issuable_type
+ 'MergeRequest'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/to_hash.rb b/lib/gitlab/github_import/representation/to_hash.rb
new file mode 100644
index 00000000000..4a0f36ab8f0
--- /dev/null
+++ b/lib/gitlab/github_import/representation/to_hash.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ module ToHash
+ # Converts the current representation to a Hash. The keys of this Hash
+ # will be Symbols.
+ def to_hash
+ hash = {}
+
+ attributes.each do |key, value|
+ hash[key] = convert_value_for_to_hash(value)
+ end
+
+ hash
+ end
+
+ def convert_value_for_to_hash(value)
+ if value.is_a?(Array)
+ value.map { |v| convert_value_for_to_hash(v) }
+ elsif value.respond_to?(:to_hash)
+ value.to_hash
+ else
+ value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/representation/user.rb b/lib/gitlab/github_import/representation/user.rb
new file mode 100644
index 00000000000..e00dcfca33d
--- /dev/null
+++ b/lib/gitlab/github_import/representation/user.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Representation
+ class User
+ include ToHash
+ include ExposeAttribute
+
+ attr_reader :attributes
+
+ expose_attribute :id, :login
+
+ # Builds a user from a GitHub API response.
+ #
+ # user - An instance of `Sawyer::Resource` containing the user details.
+ def self.from_api_response(user)
+ new(id: user.id, login: user.login)
+ end
+
+ # Builds a user using a Hash that was built from a JSON payload.
+ def self.from_json_hash(raw_hash)
+ new(Representation.symbolize_hash(raw_hash))
+ end
+
+ # attributes - A Hash containing the user details. The keys of this
+ # Hash (and any nested hashes) must be symbols.
+ def initialize(attributes)
+ @attributes = attributes
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/sequential_importer.rb b/lib/gitlab/github_import/sequential_importer.rb
new file mode 100644
index 00000000000..4f7324536a0
--- /dev/null
+++ b/lib/gitlab/github_import/sequential_importer.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # The SequentialImporter imports a GitHub project in a single thread,
+ # without using Sidekiq. This makes it useful for testing purposes as well
+ # as Rake tasks, but it should be avoided for anything else in favour of the
+ # parallel importer.
+ class SequentialImporter
+ attr_reader :project, :client
+
+ SEQUENTIAL_IMPORTERS = [
+ Importer::LabelsImporter,
+ Importer::MilestonesImporter,
+ Importer::ReleasesImporter
+ ].freeze
+
+ PARALLEL_IMPORTERS = [
+ Importer::PullRequestsImporter,
+ Importer::IssuesImporter,
+ Importer::DiffNotesImporter,
+ Importer::NotesImporter
+ ].freeze
+
+ # project - The project to import the data into.
+ # token - The token to use for the GitHub API.
+ def initialize(project, token: nil)
+ @project = project
+ @client = GithubImport
+ .new_client_for(project, token: token, parallel: false)
+ end
+
+ def execute
+ Importer::RepositoryImporter.new(project, client).execute
+
+ SEQUENTIAL_IMPORTERS.each do |klass|
+ klass.new(project, client).execute
+ end
+
+ PARALLEL_IMPORTERS.each do |klass|
+ klass.new(project, client, parallel: false).execute
+ end
+
+ project.repository.after_import
+
+ true
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/user_finder.rb b/lib/gitlab/github_import/user_finder.rb
new file mode 100644
index 00000000000..be1259662a7
--- /dev/null
+++ b/lib/gitlab/github_import/user_finder.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ # Class that can be used for finding a GitLab user ID based on a GitHub user
+ # ID or username.
+ #
+ # Any found user IDs are cached in Redis to reduce the number of SQL queries
+ # executed over time. Valid keys are refreshed upon access so frequently
+ # used keys stick around.
+ #
+ # Lookups are cached even if no ID was found to remove the need for querying
+ # the database when most queries are not going to return results anyway.
+ class UserFinder
+ attr_reader :project, :client
+
+ # The base cache key to use for caching user IDs for a given GitHub user
+ # ID.
+ ID_CACHE_KEY = 'github-import/user-finder/user-id/%s'.freeze
+
+ # The base cache key to use for caching user IDs for a given GitHub email
+ # address.
+ ID_FOR_EMAIL_CACHE_KEY =
+ 'github-import/user-finder/id-for-email/%s'.freeze
+
+ # The base cache key to use for caching the Email addresses of GitHub
+ # usernames.
+ EMAIL_FOR_USERNAME_CACHE_KEY =
+ 'github-import/user-finder/email-for-username/%s'.freeze
+
+ # project - An instance of `Project`
+ # client - An instance of `Gitlab::GithubImport::Client`
+ def initialize(project, client)
+ @project = project
+ @client = client
+ end
+
+ # Returns the GitLab user ID of an object's author.
+ #
+ # If the object has no author ID we'll use the ID of the GitLab ghost
+ # user.
+ def author_id_for(object)
+ id =
+ if object&.author
+ user_id_for(object.author)
+ else
+ GithubImport.ghost_user_id
+ end
+
+ if id
+ [id, true]
+ else
+ [project.creator_id, false]
+ end
+ end
+
+ # Returns the GitLab user ID of an issuable's assignee.
+ def assignee_id_for(issuable)
+ user_id_for(issuable.assignee) if issuable.assignee
+ end
+
+ # Returns the GitLab user ID for a GitHub user.
+ #
+ # user - An instance of `Gitlab::GithubImport::Representation::User`.
+ def user_id_for(user)
+ find(user.id, user.login)
+ end
+
+ # Returns the GitLab ID for the given GitHub ID or username.
+ #
+ # id - The ID of the GitHub user.
+ # username - The username of the GitHub user.
+ def find(id, username)
+ email = email_for_github_username(username)
+ cached, found_id = find_from_cache(id, email)
+
+ return found_id if found_id
+
+ # We only want to query the database if necessary. If previous lookups
+ # didn't yield a user ID we won't query the database again until the
+ # keys expire.
+ find_id_from_database(id, email) unless cached
+ end
+
+ # Finds a user ID from the cache for a given GitHub ID or Email.
+ def find_from_cache(id, email = nil)
+ id_exists, id_for_github_id = cached_id_for_github_id(id)
+
+ return [id_exists, id_for_github_id] if id_for_github_id
+
+ # Just in case no Email address could be retrieved (for whatever reason)
+ return [false] unless email
+
+ cached_id_for_github_email(email)
+ end
+
+ # Finds a GitLab user ID from the database for a given GitHub user ID or
+ # Email.
+ def find_id_from_database(id, email)
+ id_for_github_id(id) || id_for_github_email(email)
+ end
+
+ def email_for_github_username(username)
+ cache_key = EMAIL_FOR_USERNAME_CACHE_KEY % username
+ email = Caching.read(cache_key)
+
+ unless email
+ user = client.user(username)
+ email = Caching.write(cache_key, user.email) if user
+ end
+
+ email
+ end
+
+ def cached_id_for_github_id(id)
+ read_id_from_cache(ID_CACHE_KEY % id)
+ end
+
+ def cached_id_for_github_email(email)
+ read_id_from_cache(ID_FOR_EMAIL_CACHE_KEY % email)
+ end
+
+ # Queries and caches the GitLab user ID for a GitHub user ID, if one was
+ # found.
+ def id_for_github_id(id)
+ gitlab_id = query_id_for_github_id(id) || nil
+
+ Caching.write(ID_CACHE_KEY % id, gitlab_id)
+ end
+
+ # Queries and caches the GitLab user ID for a GitHub email, if one was
+ # found.
+ def id_for_github_email(email)
+ gitlab_id = query_id_for_github_email(email) || nil
+
+ Caching.write(ID_FOR_EMAIL_CACHE_KEY % email, gitlab_id)
+ end
+
+ def query_id_for_github_id(id)
+ User.for_github_id(id).pluck(:id).first
+ end
+
+ def query_id_for_github_email(email)
+ User.by_any_email(email).pluck(:id).first
+ end
+
+ # Reads an ID from the cache.
+ #
+ # The return value is an Array with two values:
+ #
+ # 1. A boolean indicating if the key was present or not.
+ # 2. The ID as an Integer, or nil in case no ID could be found.
+ def read_id_from_cache(key)
+ value = Caching.read(key)
+ exists = !value.nil?
+ number = value.to_i
+
+ # The cache key may be empty to indicate a previously looked up user for
+ # which we couldn't find an ID.
+ [exists, number.positive? ? number : nil]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/user_formatter.rb b/lib/gitlab/github_import/user_formatter.rb
deleted file mode 100644
index 04c2964da20..00000000000
--- a/lib/gitlab/github_import/user_formatter.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-module Gitlab
- module GithubImport
- class UserFormatter
- attr_reader :client, :raw
-
- delegate :id, :login, to: :raw, allow_nil: true
-
- def initialize(client, raw)
- @client = client
- @raw = raw
- end
-
- def gitlab_id
- return @gitlab_id if defined?(@gitlab_id)
-
- @gitlab_id = find_by_external_uid || find_by_email
- end
-
- private
-
- def email
- @email ||= client.user(raw.login).try(:email)
- end
-
- def find_by_email
- return nil unless email
-
- User.find_by_any_email(email)
- .try(:id)
- end
-
- def find_by_external_uid
- return nil unless id
-
- identities = ::Identity.arel_table
-
- User.select(:id)
- .joins(:identities).where(identities[:provider].eq(:github)
- .and(identities[:extern_uid].eq(id)))
- .first
- .try(:id)
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/wiki_formatter.rb b/lib/gitlab/github_import/wiki_formatter.rb
deleted file mode 100644
index 0396122eeb9..00000000000
--- a/lib/gitlab/github_import/wiki_formatter.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module Gitlab
- module GithubImport
- class WikiFormatter
- attr_reader :project
-
- def initialize(project)
- @project = project
- end
-
- def disk_path
- "#{project.disk_path}.wiki"
- end
-
- def import_url
- project.import_url.sub(/\.git\z/, ".wiki.git")
- end
- end
- end
-end
diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb
index f1007daab5d..075b3982608 100644
--- a/lib/gitlab/gitlab_import/client.rb
+++ b/lib/gitlab/gitlab_import/client.rb
@@ -65,6 +65,7 @@ module Gitlab
y << item
end
break if items.empty? || items.size < per_page
+
page += 1
end
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 3a666c2268b..dfcdfc307b6 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -20,7 +20,7 @@ module Gitlab
gon.gitlab_url = Gitlab.config.gitlab.url
gon.revision = Gitlab::REVISION
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
- gon.sprite_icons = ActionController::Base.helpers.asset_path('icons.svg')
+ gon.sprite_icons = IconsHelper.sprite_icon_path
if current_user
gon.current_user_id = current_user.id
diff --git a/lib/gitlab/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb
index 635f52131f9..42ded7c286f 100644
--- a/lib/gitlab/group_hierarchy.rb
+++ b/lib/gitlab/group_hierarchy.rb
@@ -17,12 +17,32 @@ module Gitlab
@model = ancestors_base.model
end
+ # Returns the set of descendants of a given relation, but excluding the given
+ # relation
+ def descendants
+ base_and_descendants.where.not(id: descendants_base.select(:id))
+ end
+
+ # Returns the set of ancestors of a given relation, but excluding the given
+ # relation
+ #
+ # Passing an `upto` will stop the recursion once the specified parent_id is
+ # reached. So all ancestors *lower* than the specified ancestor will be
+ # included.
+ def ancestors(upto: nil)
+ base_and_ancestors(upto: upto).where.not(id: ancestors_base.select(:id))
+ end
+
# Returns a relation that includes the ancestors_base set of groups
# and all their ancestors (recursively).
- def base_and_ancestors
+ #
+ # Passing an `upto` will stop the recursion once the specified parent_id is
+ # reached. So all ancestors *lower* than the specified acestor will be
+ # included.
+ def base_and_ancestors(upto: nil)
return ancestors_base unless Group.supports_nested_groups?
- read_only(base_and_ancestors_cte.apply_to(model.all))
+ read_only(base_and_ancestors_cte(upto).apply_to(model.all))
end
# Returns a relation that includes the descendants_base set of groups
@@ -78,17 +98,19 @@ module Gitlab
private
- def base_and_ancestors_cte
+ def base_and_ancestors_cte(stop_id = nil)
cte = SQL::RecursiveCTE.new(:base_and_ancestors)
cte << ancestors_base.except(:order)
# Recursively get all the ancestors of the base set.
- cte << model
+ parent_query = model
.from([groups_table, cte.table])
.where(groups_table[:id].eq(cte.table[:parent_id]))
.except(:order)
+ parent_query = parent_query.where(cte.table[:parent_id].not_eq(stop_id)) if stop_id
+ cte << parent_query
cte
end
diff --git a/lib/gitlab/hook_data/issuable_builder.rb b/lib/gitlab/hook_data/issuable_builder.rb
new file mode 100644
index 00000000000..4febb0ab430
--- /dev/null
+++ b/lib/gitlab/hook_data/issuable_builder.rb
@@ -0,0 +1,56 @@
+module Gitlab
+ module HookData
+ class IssuableBuilder
+ CHANGES_KEYS = %i[previous current].freeze
+
+ attr_accessor :issuable
+
+ def initialize(issuable)
+ @issuable = issuable
+ end
+
+ def build(user: nil, changes: {})
+ hook_data = {
+ object_kind: issuable.class.name.underscore,
+ user: user.hook_attrs,
+ project: issuable.project.hook_attrs,
+ object_attributes: issuable.hook_attrs,
+ labels: issuable.labels.map(&:hook_attrs),
+ changes: final_changes(changes.slice(*safe_keys)),
+ # DEPRECATED
+ repository: issuable.project.hook_attrs.slice(:name, :url, :description, :homepage)
+ }
+
+ if issuable.is_a?(Issue)
+ hook_data[:assignees] = issuable.assignees.map(&:hook_attrs) if issuable.assignees.any?
+ else
+ hook_data[:assignee] = issuable.assignee.hook_attrs if issuable.assignee
+ end
+
+ hook_data
+ end
+
+ def safe_keys
+ issuable_builder::SAFE_HOOK_ATTRIBUTES + issuable_builder::SAFE_HOOK_RELATIONS
+ end
+
+ private
+
+ def issuable_builder
+ case issuable
+ when Issue
+ Gitlab::HookData::IssueBuilder
+ when MergeRequest
+ Gitlab::HookData::MergeRequestBuilder
+ end
+ end
+
+ def final_changes(changes_hash)
+ changes_hash.reduce({}) do |hash, (key, changes_array)|
+ hash[key] = Hash[CHANGES_KEYS.zip(changes_array)]
+ hash
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb
new file mode 100644
index 00000000000..e29dd0d5b0e
--- /dev/null
+++ b/lib/gitlab/hook_data/issue_builder.rb
@@ -0,0 +1,55 @@
+module Gitlab
+ module HookData
+ class IssueBuilder
+ SAFE_HOOK_ATTRIBUTES = %i[
+ assignee_id
+ author_id
+ closed_at
+ confidential
+ created_at
+ deleted_at
+ description
+ due_date
+ id
+ iid
+ last_edited_at
+ last_edited_by_id
+ milestone_id
+ moved_to_id
+ project_id
+ relative_position
+ state
+ time_estimate
+ title
+ updated_at
+ updated_by_id
+ ].freeze
+
+ SAFE_HOOK_RELATIONS = %i[
+ assignees
+ labels
+ total_time_spent
+ ].freeze
+
+ attr_accessor :issue
+
+ def initialize(issue)
+ @issue = issue
+ end
+
+ def build
+ attrs = {
+ url: Gitlab::UrlBuilder.build(issue),
+ total_time_spent: issue.total_time_spent,
+ human_total_time_spent: issue.human_total_time_spent,
+ human_time_estimate: issue.human_time_estimate,
+ assignee_ids: issue.assignee_ids,
+ assignee_id: issue.assignee_ids.first # This key is deprecated
+ }
+
+ issue.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES)
+ .merge!(attrs)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb
new file mode 100644
index 00000000000..ae9b68eb648
--- /dev/null
+++ b/lib/gitlab/hook_data/merge_request_builder.rb
@@ -0,0 +1,62 @@
+module Gitlab
+ module HookData
+ class MergeRequestBuilder
+ SAFE_HOOK_ATTRIBUTES = %i[
+ assignee_id
+ author_id
+ created_at
+ deleted_at
+ description
+ head_pipeline_id
+ id
+ iid
+ last_edited_at
+ last_edited_by_id
+ merge_commit_sha
+ merge_error
+ merge_params
+ merge_status
+ merge_user_id
+ merge_when_pipeline_succeeds
+ milestone_id
+ source_branch
+ source_project_id
+ state
+ target_branch
+ target_project_id
+ time_estimate
+ title
+ updated_at
+ updated_by_id
+ ].freeze
+
+ SAFE_HOOK_RELATIONS = %i[
+ assignee
+ labels
+ total_time_spent
+ ].freeze
+
+ attr_accessor :merge_request
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ end
+
+ def build
+ attrs = {
+ url: Gitlab::UrlBuilder.build(merge_request),
+ source: merge_request.source_project.try(:hook_attrs),
+ target: merge_request.target_project.hook_attrs,
+ last_commit: merge_request.diff_head_commit&.hook_attrs,
+ work_in_progress: merge_request.work_in_progress?,
+ total_time_spent: merge_request.total_time_spent,
+ human_total_time_spent: merge_request.human_total_time_spent,
+ human_time_estimate: merge_request.human_time_estimate
+ }
+
+ merge_request.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES)
+ .merge!(attrs)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index 50ee879129c..2066005dddc 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.2.0'.freeze
+ VERSION = '0.2.1'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index dec8b4c5acd..f2b193c79cb 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -19,6 +19,7 @@ project_tree:
- milestone:
- events:
- :push_event_payload
+ - :issue_assignees
- snippets:
- :award_emoji
- notes:
@@ -53,7 +54,6 @@ project_tree:
- :auto_devops
- :triggers
- :pipeline_schedules
- - :cluster
- :services
- :hooks
- protected_branches:
@@ -62,6 +62,7 @@ project_tree:
- protected_tags:
- :create_access_levels
- :project_feature
+ - :custom_attributes
# Only include the following attributes for the models specified.
included_attributes:
@@ -113,6 +114,7 @@ excluded_attributes:
- :milestone_id
- :ref_fetched
- :merge_jid
+ - :latest_merge_request_diff_id
award_emoji:
- :awardable_id
statuses:
@@ -131,8 +133,6 @@ methods:
- :type
services:
- :type
- merge_request_diff:
- - :utf8_st_diffs
merge_request_diff_files:
- :utf8_diff
merge_requests:
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
index fbdd74788bc..c14646b0611 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -1,6 +1,10 @@
module Gitlab
module ImportExport
class Importer
+ def self.imports_repository?
+ true
+ end
+
def initialize(project)
@archive_file = project.import_source
@current_user = project.creator
diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb
index 81a213e8321..f3d7407383c 100644
--- a/lib/gitlab/import_export/merge_request_parser.rb
+++ b/lib/gitlab/import_export/merge_request_parser.rb
@@ -1,7 +1,7 @@
module Gitlab
module ImportExport
class MergeRequestParser
- FORKED_PROJECT_ID = -1
+ FORKED_PROJECT_ID = nil
def initialize(project, diff_head_sha, merge_request, relation_hash)
@project = project
@@ -26,7 +26,7 @@ module Gitlab
end
def fetch_ref
- @project.repository.fetch_ref(@project.repository.path, @diff_head_sha, @merge_request.source_branch)
+ @project.repository.fetch_ref(@project.repository, source_ref: @diff_head_sha, target_ref: @merge_request.source_branch)
end
def branch_exists?(branch_name)
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 3bc095a99a9..c518943be59 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -2,7 +2,7 @@ module Gitlab
module ImportExport
class ProjectTreeRestorer
# Relations which cannot have both group_id and project_id at the same time
- RESTRICT_PROJECT_AND_GROUP = %i(milestones).freeze
+ RESTRICT_PROJECT_AND_GROUP = %i(milestone milestones).freeze
def initialize(user:, shared:, project:)
@path = File.join(shared.export_path, 'project.json')
@@ -60,6 +60,8 @@ module Gitlab
end
end
+ @project.merge_requests.set_latest_merge_request_diff_ids!
+
@saved
end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index a76cf1addc0..d7d1b05e8b9 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -8,8 +8,6 @@ module Gitlab
triggers: 'Ci::Trigger',
pipeline_schedules: 'Ci::PipelineSchedule',
builds: 'Ci::Build',
- cluster: 'Gcp::Cluster',
- clusters: 'Gcp::Cluster',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
@@ -17,7 +15,8 @@ module Gitlab
labels: :project_labels,
priorities: :label_priorities,
auto_devops: :project_auto_devops,
- label: :project_label }.freeze
+ label: :project_label,
+ custom_attributes: 'ProjectCustomAttribute' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id].freeze
@@ -37,7 +36,7 @@ module Gitlab
def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:)
@relation_name = OVERRIDES[relation_sym] || relation_sym
- @relation_hash = relation_hash.except('noteable_id').merge('project_id' => project.id)
+ @relation_hash = relation_hash.except('noteable_id')
@members_mapper = members_mapper
@user = user
@project = project
@@ -58,22 +57,20 @@ module Gitlab
private
def setup_models
- if @relation_name == :notes
- set_note_author
-
- # attachment is deprecated and note uploads are handled by Markdown uploader
- @relation_hash['attachment'] = nil
+ case @relation_name
+ when :merge_request_diff_files then setup_diff
+ when :notes then setup_note
+ when :project_label, :project_labels then setup_label
+ when :milestone, :milestones then setup_milestone
+ else
+ @relation_hash['project_id'] = @project.id
end
update_user_references
update_project_references
- handle_group_label if group_label?
reset_tokens!
remove_encrypted_attributes!
-
- set_st_diff_commits if @relation_name == :merge_request_diff
- set_diff if @relation_name == :merge_request_diff_files
end
def update_user_references
@@ -84,6 +81,12 @@ module Gitlab
end
end
+ def setup_note
+ set_note_author
+ # attachment is deprecated and note uploads are handled by Markdown uploader
+ @relation_hash['attachment'] = nil
+ end
+
# Sets the author for a note. If the user importing the project
# has admin access, an actual mapping with new project members
# will be used. Otherwise, a note stating the original author name
@@ -136,11 +139,9 @@ module Gitlab
@relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
end
- def group_label?
- @relation_hash['type'] == 'GroupLabel'
- end
+ def setup_label
+ return unless @relation_hash['type'] == 'GroupLabel'
- def handle_group_label
# If there's no group, move the label to a project label
if @relation_hash['group_id']
@relation_hash['project_id'] = nil
@@ -150,6 +151,14 @@ module Gitlab
end
end
+ def setup_milestone
+ if @relation_hash['group_id']
+ @relation_hash['group_id'] = @project.group.id
+ else
+ @relation_hash['project_id'] = @project.id
+ end
+ end
+
def reset_tokens!
return unless Gitlab::ImportExport.reset_tokens? && TOKEN_RESET_MODELS.include?(@relation_name.to_s)
@@ -198,14 +207,7 @@ module Gitlab
relation_class: relation_class)
end
- def set_st_diff_commits
- @relation_hash['st_diffs'] = @relation_hash.delete('utf8_st_diffs')
-
- HashUtil.deep_symbolize_array!(@relation_hash['st_diffs'])
- HashUtil.deep_symbolize_array_with_date!(@relation_hash['st_commits'])
- end
-
- def set_diff
+ def setup_diff
@relation_hash['diff'] = @relation_hash.delete('utf8_diff')
end
@@ -250,7 +252,13 @@ module Gitlab
end
def find_or_create_object!
- finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
+ finder_attributes = if @relation_name == :group_label
+ %w[title group_id]
+ elsif parsed_relation_hash['project_id']
+ %w[title project_id]
+ else
+ %w[title group_id]
+ end
finder_hash = parsed_relation_hash.slice(*finder_attributes)
if label?
diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb
index f9ae5079d7c..627a487d577 100644
--- a/lib/gitlab/import_export/uploads_saver.rb
+++ b/lib/gitlab/import_export/uploads_saver.rb
@@ -24,8 +24,7 @@ module Gitlab
end
def uploads_path
- # TODO: decide what to do with uploads. We will use UUIDs here too?
- File.join(Rails.root.join('public/uploads'), @project.path_with_namespace)
+ FileUploader.dynamic_path_segment(@project)
end
end
end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 5404dc11a87..eeb03625479 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -8,14 +8,14 @@ module Gitlab
ImportSource = Struct.new(:name, :title, :importer)
ImportTable = [
- ImportSource.new('github', 'GitHub', Github::Import),
+ ImportSource.new('github', 'GitHub', Gitlab::GithubImport::ParallelImporter),
ImportSource.new('bitbucket', 'Bitbucket', Gitlab::BitbucketImport::Importer),
ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer),
ImportSource.new('google_code', 'Google Code', Gitlab::GoogleCodeImport::Importer),
ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer),
ImportSource.new('git', 'Repo by URL', nil),
ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer),
- ImportSource.new('gitea', 'Gitea', Gitlab::GithubImport::Importer)
+ ImportSource.new('gitea', 'Gitea', Gitlab::LegacyGithubImport::Importer)
].freeze
class << self
diff --git a/lib/gitlab/issuable_metadata.rb b/lib/gitlab/issuable_metadata.rb
index 977c05910d3..0c9de72329c 100644
--- a/lib/gitlab/issuable_metadata.rb
+++ b/lib/gitlab/issuable_metadata.rb
@@ -1,6 +1,14 @@
module Gitlab
module IssuableMetadata
def issuable_meta_data(issuable_collection, collection_type)
+ # ActiveRecord uses Object#extend for null relations.
+ if !(issuable_collection.singleton_class < ActiveRecord::NullRelation) &&
+ issuable_collection.respond_to?(:limit_value) &&
+ issuable_collection.limit_value.nil?
+
+ raise 'Collection must have a limit applied for preloading meta-data'
+ end
+
# map has to be used here since using pluck or select will
# throw an error when ordering issuables by priority which inserts
# a new order into the collection.
diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb
index 4d6bbda15f3..f654508c391 100644
--- a/lib/gitlab/job_waiter.rb
+++ b/lib/gitlab/job_waiter.rb
@@ -19,11 +19,13 @@ module Gitlab
Gitlab::Redis::SharedState.with { |redis| redis.lpush(key, jid) }
end
- attr_reader :key, :jobs_remaining, :finished
+ attr_reader :key, :finished
+ attr_accessor :jobs_remaining
# jobs_remaining - the number of jobs left to wait for
- def initialize(jobs_remaining)
- @key = "gitlab:job_waiter:#{SecureRandom.uuid}"
+ # key - The key of this waiter.
+ def initialize(jobs_remaining = 0, key = "gitlab:job_waiter:#{SecureRandom.uuid}")
+ @key = key
@jobs_remaining = jobs_remaining
@finished = []
end
diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb
new file mode 100644
index 00000000000..7a50f07f3c5
--- /dev/null
+++ b/lib/gitlab/kubernetes/helm.rb
@@ -0,0 +1,96 @@
+module Gitlab
+ module Kubernetes
+ class Helm
+ HELM_VERSION = '2.7.0'.freeze
+ NAMESPACE = 'gitlab-managed-apps'.freeze
+ INSTALL_DEPS = <<-EOS.freeze
+ set -eo pipefail
+ apk add -U ca-certificates openssl >/dev/null
+ wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
+ mv /tmp/linux-amd64/helm /usr/bin/
+ EOS
+
+ InstallCommand = Struct.new(:name, :install_helm, :chart) do
+ def pod_name
+ "install-#{name}"
+ end
+ end
+
+ def initialize(kubeclient)
+ @kubeclient = kubeclient
+ @namespace = Namespace.new(NAMESPACE, kubeclient)
+ end
+
+ def install(command)
+ @namespace.ensure_exists!
+ @kubeclient.create_pod(pod_resource(command))
+ end
+
+ ##
+ # Returns Pod phase
+ #
+ # https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
+ #
+ # values: "Pending", "Running", "Succeeded", "Failed", "Unknown"
+ #
+ def installation_status(pod_name)
+ @kubeclient.get_pod(pod_name, @namespace.name).status.phase
+ end
+
+ def installation_log(pod_name)
+ @kubeclient.get_pod_log(pod_name, @namespace.name).body
+ end
+
+ def delete_installation_pod!(pod_name)
+ @kubeclient.delete_pod(pod_name, @namespace.name)
+ end
+
+ private
+
+ def pod_resource(command)
+ labels = { 'gitlab.org/action': 'install', 'gitlab.org/application': command.name }
+ metadata = { name: command.pod_name, namespace: @namespace.name, labels: labels }
+ container = {
+ name: 'helm',
+ image: 'alpine:3.6',
+ env: generate_pod_env(command),
+ command: %w(/bin/sh),
+ args: %w(-c $(COMMAND_SCRIPT))
+ }
+ spec = { containers: [container], restartPolicy: 'Never' }
+
+ ::Kubeclient::Resource.new(metadata: metadata, spec: spec)
+ end
+
+ def generate_pod_env(command)
+ {
+ HELM_VERSION: HELM_VERSION,
+ TILLER_NAMESPACE: @namespace.name,
+ COMMAND_SCRIPT: generate_script(command)
+ }.map { |key, value| { name: key, value: value } }
+ end
+
+ def generate_script(command)
+ [
+ INSTALL_DEPS,
+ helm_init_command(command),
+ helm_install_command(command)
+ ].join("\n")
+ end
+
+ def helm_init_command(command)
+ if command.install_helm
+ 'helm init >/dev/null'
+ else
+ 'helm init --client-only >/dev/null'
+ end
+ end
+
+ def helm_install_command(command)
+ return if command.chart.nil?
+
+ "helm install #{command.chart} --name #{command.name} --namespace #{@namespace.name} >/dev/null"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/namespace.rb b/lib/gitlab/kubernetes/namespace.rb
new file mode 100644
index 00000000000..fbbddb7bffa
--- /dev/null
+++ b/lib/gitlab/kubernetes/namespace.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module Kubernetes
+ class Namespace
+ attr_accessor :name
+
+ def initialize(name, client)
+ @name = name
+ @client = client
+ end
+
+ def exists?
+ @client.get_namespace(name)
+ rescue ::KubeException => ke
+ raise ke unless ke.error_code == 404
+
+ false
+ end
+
+ def create!
+ resource = ::Kubeclient::Resource.new(metadata: { name: name })
+
+ @client.create_namespace(resource)
+ end
+
+ def ensure_exists!
+ exists? || create!
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/pod.rb b/lib/gitlab/kubernetes/pod.rb
new file mode 100644
index 00000000000..f3842cdf762
--- /dev/null
+++ b/lib/gitlab/kubernetes/pod.rb
@@ -0,0 +1,12 @@
+module Gitlab
+ module Kubernetes
+ module Pod
+ PENDING = 'Pending'.freeze
+ RUNNING = 'Running'.freeze
+ SUCCEEDED = 'Succeeded'.freeze
+ FAILED = 'Failed'.freeze
+ UNKNOWN = 'Unknown'.freeze
+ PHASES = [PENDING, RUNNING, SUCCEEDED, FAILED, UNKNOWN].freeze
+ end
+ end
+end
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
index 3123da17fd9..1bd0965679a 100644
--- a/lib/gitlab/ldap/auth_hash.rb
+++ b/lib/gitlab/ldap/auth_hash.rb
@@ -4,7 +4,7 @@ module Gitlab
module LDAP
class AuthHash < Gitlab::OAuth::AuthHash
def uid
- Gitlab::LDAP::Person.normalize_dn(super)
+ @uid ||= Gitlab::LDAP::Person.normalize_dn(super)
end
private
diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb
index ed1de73f8c6..7274d1c3b43 100644
--- a/lib/gitlab/ldap/authentication.rb
+++ b/lib/gitlab/ldap/authentication.rb
@@ -62,6 +62,7 @@ module Gitlab
def user
return nil unless ldap_user
+
Gitlab::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider)
end
end
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index 1793097363e..3945df27eed 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -9,10 +9,8 @@ module Gitlab
class User < Gitlab::OAuth::User
class << self
def find_by_uid_and_provider(uid, provider)
- # LDAP distinguished name is case-insensitive
- identity = ::Identity
- .where(provider: provider)
- .iwhere(extern_uid: uid).last
+ identity = ::Identity.with_extern_uid(provider, uid).take
+
identity && identity.user
end
end
diff --git a/lib/gitlab/legacy_github_import/base_formatter.rb b/lib/gitlab/legacy_github_import/base_formatter.rb
new file mode 100644
index 00000000000..2f07fde406c
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/base_formatter.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module LegacyGithubImport
+ class BaseFormatter
+ attr_reader :client, :formatter, :project, :raw_data
+
+ def initialize(project, raw_data, client = nil)
+ @project = project
+ @raw_data = raw_data
+ @client = client
+ @formatter = Gitlab::ImportFormatter.new
+ end
+
+ def create!
+ association = project.public_send(project_association) # rubocop:disable GitlabSecurity/PublicSend
+
+ association.find_or_create_by!(find_condition) do |record|
+ record.attributes = attributes
+ end
+ end
+
+ def url
+ raw_data.url || ''
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/branch_formatter.rb b/lib/gitlab/legacy_github_import/branch_formatter.rb
new file mode 100644
index 00000000000..80fe1d67209
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/branch_formatter.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module LegacyGithubImport
+ class BranchFormatter < BaseFormatter
+ delegate :repo, :sha, :ref, to: :raw_data
+
+ def exists?
+ branch_exists? && commit_exists?
+ end
+
+ def valid?
+ sha.present? && ref.present?
+ end
+
+ def user
+ raw_data.user&.login || 'unknown'
+ end
+
+ def short_sha
+ Commit.truncate_sha(sha)
+ end
+
+ private
+
+ def branch_exists?
+ project.repository.branch_exists?(ref)
+ end
+
+ def commit_exists?
+ project.repository.branch_names_contains(sha).include?(ref)
+ end
+
+ def short_id
+ sha.to_s[0..7]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/client.rb b/lib/gitlab/legacy_github_import/client.rb
new file mode 100644
index 00000000000..53c910d44bd
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/client.rb
@@ -0,0 +1,148 @@
+module Gitlab
+ module LegacyGithubImport
+ class Client
+ GITHUB_SAFE_REMAINING_REQUESTS = 100
+ GITHUB_SAFE_SLEEP_TIME = 500
+
+ attr_reader :access_token, :host, :api_version
+
+ def initialize(access_token, host: nil, api_version: 'v3')
+ @access_token = access_token
+ @host = host.to_s.sub(%r{/+\z}, '')
+ @api_version = api_version
+ @users = {}
+
+ if access_token
+ ::Octokit.auto_paginate = false
+ end
+ end
+
+ def api
+ @api ||= ::Octokit::Client.new(
+ access_token: access_token,
+ api_endpoint: api_endpoint,
+ # If there is no config, we're connecting to github.com and we
+ # should verify ssl.
+ connection_options: {
+ ssl: { verify: config ? config['verify_ssl'] : true }
+ }
+ )
+ end
+
+ def client
+ unless config
+ raise Projects::ImportService::Error,
+ 'OAuth configuration for GitHub missing.'
+ end
+
+ @client ||= ::OAuth2::Client.new(
+ config.app_id,
+ config.app_secret,
+ github_options.merge(ssl: { verify: config['verify_ssl'] })
+ )
+ end
+
+ def authorize_url(redirect_uri)
+ client.auth_code.authorize_url({
+ redirect_uri: redirect_uri,
+ scope: "repo, user, user:email"
+ })
+ end
+
+ def get_token(code)
+ client.auth_code.get_token(code).token
+ end
+
+ def method_missing(method, *args, &block)
+ if api.respond_to?(method)
+ request(method, *args, &block)
+ else
+ super(method, *args, &block)
+ end
+ end
+
+ def respond_to?(method)
+ api.respond_to?(method) || super
+ end
+
+ def user(login)
+ return nil unless login.present?
+ return @users[login] if @users.key?(login)
+
+ @users[login] = api.user(login)
+ end
+
+ private
+
+ def api_endpoint
+ if host.present? && api_version.present?
+ "#{host}/api/#{api_version}"
+ else
+ github_options[:site]
+ end
+ end
+
+ def config
+ Gitlab.config.omniauth.providers.find { |provider| provider.name == "github" }
+ end
+
+ def github_options
+ if config
+ config["args"]["client_options"].deep_symbolize_keys
+ else
+ OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys
+ end
+ end
+
+ def rate_limit
+ api.rate_limit!
+ # GitHub Rate Limit API returns 404 when the rate limit is
+ # disabled. In this case we just want to return gracefully
+ # instead of spitting out an error.
+ rescue Octokit::NotFound
+ nil
+ end
+
+ def has_rate_limit?
+ return @has_rate_limit if defined?(@has_rate_limit)
+
+ @has_rate_limit = rate_limit.present?
+ end
+
+ def rate_limit_exceed?
+ has_rate_limit? && rate_limit.remaining <= GITHUB_SAFE_REMAINING_REQUESTS
+ end
+
+ def rate_limit_sleep_time
+ rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME
+ end
+
+ def request(method, *args, &block)
+ sleep rate_limit_sleep_time if rate_limit_exceed?
+
+ data = api.__send__(method, *args) # rubocop:disable GitlabSecurity/PublicSend
+ return data unless data.is_a?(Array)
+
+ last_response = api.last_response
+
+ if block_given?
+ yield data
+ # api.last_response could change while we're yielding (e.g. fetching labels for each PR)
+ # so we cache our own last response
+ each_response_page(last_response, &block)
+ else
+ each_response_page(last_response) { |page| data.concat(page) }
+ data
+ end
+ end
+
+ def each_response_page(last_response)
+ while last_response.rels[:next]
+ sleep rate_limit_sleep_time if rate_limit_exceed?
+ last_response = last_response.rels[:next].get
+ yield last_response.data if last_response.data.is_a?(Array)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/comment_formatter.rb b/lib/gitlab/legacy_github_import/comment_formatter.rb
new file mode 100644
index 00000000000..d2c7a8ae9f4
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/comment_formatter.rb
@@ -0,0 +1,69 @@
+module Gitlab
+ module LegacyGithubImport
+ class CommentFormatter < BaseFormatter
+ attr_writer :author_id
+
+ def attributes
+ {
+ project: project,
+ note: note,
+ commit_id: raw_data.commit_id,
+ line_code: line_code,
+ author_id: author_id,
+ type: type,
+ created_at: raw_data.created_at,
+ updated_at: raw_data.updated_at
+ }
+ end
+
+ private
+
+ def author
+ @author ||= UserFormatter.new(client, raw_data.user)
+ end
+
+ def author_id
+ author.gitlab_id || project.creator_id
+ end
+
+ def body
+ raw_data.body || ""
+ end
+
+ def line_code
+ return unless on_diff?
+
+ parsed_lines = Gitlab::Diff::Parser.new.parse(diff_hunk.lines)
+ generate_line_code(parsed_lines.to_a.last)
+ end
+
+ def generate_line_code(line)
+ Gitlab::Git.diff_line_code(file_path, line.new_pos, line.old_pos)
+ end
+
+ def on_diff?
+ diff_hunk.present?
+ end
+
+ def diff_hunk
+ raw_data.diff_hunk
+ end
+
+ def file_path
+ raw_data.path
+ end
+
+ def note
+ if author.gitlab_id
+ body
+ else
+ formatter.author_line(author.login) + body
+ end
+ end
+
+ def type
+ 'LegacyDiffNote' if on_diff?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/importer.rb b/lib/gitlab/legacy_github_import/importer.rb
new file mode 100644
index 00000000000..0526ef9eb13
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/importer.rb
@@ -0,0 +1,334 @@
+module Gitlab
+ module LegacyGithubImport
+ class Importer
+ include Gitlab::ShellAdapter
+
+ def self.refmap
+ Gitlab::GithubImport.refmap
+ end
+
+ attr_reader :errors, :project, :repo, :repo_url
+
+ def initialize(project)
+ @project = project
+ @repo = project.import_source
+ @repo_url = project.import_url
+ @errors = []
+ @labels = {}
+ end
+
+ def client
+ return @client if defined?(@client)
+
+ unless credentials
+ raise Projects::ImportService::Error,
+ "Unable to find project import data credentials for project ID: #{@project.id}"
+ end
+
+ opts = {}
+ # Gitea plan to be GitHub compliant
+ if project.gitea_import?
+ uri = URI.parse(project.import_url)
+ host = "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}".sub(%r{/?[\w-]+/[\w-]+\.git\z}, '')
+ opts = {
+ host: host,
+ api_version: 'v1'
+ }
+ end
+
+ @client = Client.new(credentials[:user], opts)
+ end
+
+ def execute
+ # The ordering of importing is important here due to the way GitHub structures their data
+ # 1. Labels are required by other items while not having a dependency on anything else
+ # so need to be first
+ # 2. Pull requests must come before issues. Every pull request is also an issue but not
+ # all issues are pull requests. Only the issue entity has labels defined in GitHub. GitLab
+ # doesn't structure data like this so we need to make sure that we've created the MRs
+ # before we attempt to add the labels defined in the GitHub issue for the related, already
+ # imported, pull request
+ import_labels
+ import_milestones
+ import_pull_requests
+ import_issues
+ import_comments(:issues)
+ import_comments(:pull_requests)
+ import_wiki
+
+ # Gitea doesn't have a Release API yet
+ # See https://github.com/go-gitea/gitea/issues/330
+ unless project.gitea_import?
+ import_releases
+ end
+
+ handle_errors
+
+ true
+ end
+
+ private
+
+ def credentials
+ return @credentials if defined?(@credentials)
+
+ @credentials = project.import_data ? project.import_data.credentials : nil
+ end
+
+ def handle_errors
+ return unless errors.any?
+
+ project.update_column(:import_error, {
+ message: 'The remote data could not be fully imported.',
+ errors: errors
+ }.to_json)
+ end
+
+ def import_labels
+ fetch_resources(:labels, repo, per_page: 100) do |labels|
+ labels.each do |raw|
+ begin
+ gh_label = LabelFormatter.new(project, raw)
+ gh_label.create!
+ rescue => e
+ errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(gh_label.url), errors: e.message }
+ end
+ end
+ end
+
+ cache_labels!
+ end
+
+ def import_milestones
+ fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones|
+ milestones.each do |raw|
+ begin
+ gh_milestone = MilestoneFormatter.new(project, raw)
+ gh_milestone.create!
+ rescue => e
+ errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(gh_milestone.url), errors: e.message }
+ end
+ end
+ end
+ end
+
+ def import_issues
+ fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
+ issues.each do |raw|
+ gh_issue = IssueFormatter.new(project, raw, client)
+
+ begin
+ issuable =
+ if gh_issue.pull_request?
+ MergeRequest.find_by(target_project_id: project.id, iid: gh_issue.number)
+ else
+ gh_issue.create!
+ end
+
+ apply_labels(issuable, raw)
+ rescue => e
+ errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(gh_issue.url), errors: e.message }
+ end
+ end
+ end
+ end
+
+ def import_pull_requests
+ fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
+ pull_requests.each do |raw|
+ gh_pull_request = PullRequestFormatter.new(project, raw, client)
+
+ next unless gh_pull_request.valid?
+
+ begin
+ restore_source_branch(gh_pull_request) unless gh_pull_request.source_branch_exists?
+ restore_target_branch(gh_pull_request) unless gh_pull_request.target_branch_exists?
+
+ merge_request = gh_pull_request.create!
+
+ # Gitea doesn't return PR in the Issue API endpoint, so labels must be assigned at this stage
+ if project.gitea_import?
+ apply_labels(merge_request, raw)
+ end
+ rescue => e
+ errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(gh_pull_request.url), errors: e.message }
+ ensure
+ clean_up_restored_branches(gh_pull_request)
+ end
+ end
+ end
+
+ project.repository.after_remove_branch
+ end
+
+ def restore_source_branch(pull_request)
+ project.repository.create_branch(pull_request.source_branch_name, pull_request.source_branch_sha)
+ end
+
+ def restore_target_branch(pull_request)
+ project.repository.create_branch(pull_request.target_branch_name, pull_request.target_branch_sha)
+ end
+
+ def remove_branch(name)
+ project.repository.delete_branch(name)
+ rescue Gitlab::Git::Repository::DeleteBranchFailed
+ errors << { type: :remove_branch, name: name }
+ end
+
+ def clean_up_restored_branches(pull_request)
+ return if pull_request.opened?
+
+ remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists?
+ remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists?
+ end
+
+ def apply_labels(issuable, raw)
+ return unless raw.labels.count > 0
+
+ label_ids = raw.labels
+ .map { |attrs| @labels[attrs.name] }
+ .compact
+
+ issuable.update_attribute(:label_ids, label_ids)
+ end
+
+ def import_comments(issuable_type)
+ resource_type = "#{issuable_type}_comments".to_sym
+
+ # Two notes here:
+ # 1. We don't have a distinctive attribute for comments (unlike issues iid), so we fetch the last inserted note,
+ # compare it against every comment in the current imported page until we find match, and that's where start importing
+ # 2. GH returns comments for _both_ issues and PRs through issues_comments API, while pull_requests_comments returns
+ # only comments on diffs, so select last note not based on noteable_type but on line_code
+ line_code_is = issuable_type == :pull_requests ? 'NOT NULL' : 'NULL'
+ last_note = project.notes.where("line_code IS #{line_code_is}").last
+
+ fetch_resources(resource_type, repo, per_page: 100) do |comments|
+ if last_note
+ discard_inserted_comments(comments, last_note)
+ last_note = nil
+ end
+
+ create_comments(comments)
+ end
+ end
+
+ def create_comments(comments)
+ ActiveRecord::Base.no_touching do
+ comments.each do |raw|
+ begin
+ comment = CommentFormatter.new(project, raw, client)
+
+ # GH does not return info about comment's parent, so we guess it by checking its URL!
+ *_, parent, iid = URI(raw.html_url).path.split('/')
+
+ issuable = if parent == 'issues'
+ Issue.find_by(project_id: project.id, iid: iid)
+ else
+ MergeRequest.find_by(target_project_id: project.id, iid: iid)
+ end
+
+ next unless issuable
+
+ issuable.notes.create!(comment.attributes)
+ rescue => e
+ errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
+ end
+ end
+ end
+
+ def discard_inserted_comments(comments, last_note)
+ last_note_attrs = nil
+
+ cut_off_index = comments.find_index do |raw|
+ comment = CommentFormatter.new(project, raw)
+ comment_attrs = comment.attributes
+ last_note_attrs ||= last_note.slice(*comment_attrs.keys)
+
+ comment_attrs.with_indifferent_access == last_note_attrs
+ end
+
+ # No matching resource in the collection, which means we got halted right on the end of the last page, so all good
+ return unless cut_off_index
+
+ # Otherwise, remove the resources we've already inserted
+ comments.shift(cut_off_index + 1)
+ end
+
+ def import_wiki
+ unless project.wiki.repository_exists?
+ wiki = WikiFormatter.new(project)
+ gitlab_shell.import_repository(project.repository_storage_path, wiki.disk_path, wiki.import_url)
+ end
+ rescue Gitlab::Shell::Error => e
+ # GitHub error message when the wiki repo has not been created,
+ # this means that repo has wiki enabled, but have no pages. So,
+ # we can skip the import.
+ if e.message !~ /repository not exported/
+ errors << { type: :wiki, errors: e.message }
+ end
+ end
+
+ def import_releases
+ fetch_resources(:releases, repo, per_page: 100) do |releases|
+ releases.each do |raw|
+ begin
+ gh_release = ReleaseFormatter.new(project, raw)
+ gh_release.create! if gh_release.valid?
+ rescue => e
+ errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(gh_release.url), errors: e.message }
+ end
+ end
+ end
+ end
+
+ def cache_labels!
+ project.labels.select(:id, :title).find_each do |label|
+ @labels[label.title] = label.id
+ end
+ end
+
+ def fetch_resources(resource_type, *opts)
+ return if imported?(resource_type)
+
+ opts.last[:page] = current_page(resource_type)
+
+ client.public_send(resource_type, *opts) do |resources| # rubocop:disable GitlabSecurity/PublicSend
+ yield resources
+ increment_page(resource_type)
+ end
+
+ imported!(resource_type)
+ end
+
+ def imported?(resource_type)
+ Rails.cache.read("#{cache_key_prefix}:#{resource_type}:imported")
+ end
+
+ def imported!(resource_type)
+ Rails.cache.write("#{cache_key_prefix}:#{resource_type}:imported", true, ex: 1.day)
+ end
+
+ def increment_page(resource_type)
+ key = "#{cache_key_prefix}:#{resource_type}:current-page"
+
+ # Rails.cache.increment calls INCRBY directly on the value stored under the key, which is
+ # a serialized ActiveSupport::Cache::Entry, so it will return an error by Redis, hence this ugly work-around
+ page = Rails.cache.read(key)
+ page += 1
+ Rails.cache.write(key, page)
+
+ page
+ end
+
+ def current_page(resource_type)
+ Rails.cache.fetch("#{cache_key_prefix}:#{resource_type}:current-page", ex: 1.day) { 1 }
+ end
+
+ def cache_key_prefix
+ @cache_key_prefix ||= "github-import:#{project.id}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/issuable_formatter.rb b/lib/gitlab/legacy_github_import/issuable_formatter.rb
new file mode 100644
index 00000000000..de55382d3ad
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/issuable_formatter.rb
@@ -0,0 +1,66 @@
+module Gitlab
+ module LegacyGithubImport
+ class IssuableFormatter < BaseFormatter
+ attr_writer :assignee_id, :author_id
+
+ def project_association
+ raise NotImplementedError
+ end
+
+ delegate :number, to: :raw_data
+
+ def find_condition
+ { iid: number }
+ end
+
+ private
+
+ def state
+ raw_data.state == 'closed' ? 'closed' : 'opened'
+ end
+
+ def assigned?
+ raw_data.assignee.present?
+ end
+
+ def author
+ @author ||= UserFormatter.new(client, raw_data.user)
+ end
+
+ def author_id
+ @author_id ||= author.gitlab_id || project.creator_id
+ end
+
+ def assignee
+ if assigned?
+ @assignee ||= UserFormatter.new(client, raw_data.assignee)
+ end
+ end
+
+ def assignee_id
+ return @assignee_id if defined?(@assignee_id)
+
+ @assignee_id = assignee.try(:gitlab_id)
+ end
+
+ def body
+ raw_data.body || ""
+ end
+
+ def description
+ if author.gitlab_id
+ body
+ else
+ formatter.author_line(author.login) + body
+ end
+ end
+
+ def milestone
+ if raw_data.milestone.present?
+ milestone = MilestoneFormatter.new(project, raw_data.milestone)
+ project.milestones.find_by(milestone.find_condition)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/issue_formatter.rb b/lib/gitlab/legacy_github_import/issue_formatter.rb
new file mode 100644
index 00000000000..4c8825ccf19
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/issue_formatter.rb
@@ -0,0 +1,32 @@
+module Gitlab
+ module LegacyGithubImport
+ class IssueFormatter < IssuableFormatter
+ def attributes
+ {
+ iid: number,
+ project: project,
+ milestone: milestone,
+ title: raw_data.title,
+ description: description,
+ state: state,
+ author_id: author_id,
+ assignee_ids: Array(assignee_id),
+ created_at: raw_data.created_at,
+ updated_at: raw_data.updated_at
+ }
+ end
+
+ def has_comments?
+ raw_data.comments > 0
+ end
+
+ def project_association
+ :issues
+ end
+
+ def pull_request?
+ raw_data.pull_request.present?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/label_formatter.rb b/lib/gitlab/legacy_github_import/label_formatter.rb
new file mode 100644
index 00000000000..c3eed12e739
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/label_formatter.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module LegacyGithubImport
+ class LabelFormatter < BaseFormatter
+ def attributes
+ {
+ project: project,
+ title: title,
+ color: color
+ }
+ end
+
+ def project_association
+ :labels
+ end
+
+ def create!
+ params = attributes.except(:project)
+ service = ::Labels::FindOrCreateService.new(nil, project, params)
+ label = service.execute(skip_authorization: true)
+
+ raise ActiveRecord::RecordInvalid.new(label) unless label.persisted?
+
+ label
+ end
+
+ private
+
+ def color
+ "##{raw_data.color}"
+ end
+
+ def title
+ raw_data.name
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/milestone_formatter.rb b/lib/gitlab/legacy_github_import/milestone_formatter.rb
new file mode 100644
index 00000000000..a565294384d
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/milestone_formatter.rb
@@ -0,0 +1,40 @@
+module Gitlab
+ module LegacyGithubImport
+ class MilestoneFormatter < BaseFormatter
+ def attributes
+ {
+ iid: number,
+ project: project,
+ title: raw_data.title,
+ description: raw_data.description,
+ due_date: raw_data.due_on,
+ state: state,
+ created_at: raw_data.created_at,
+ updated_at: raw_data.updated_at
+ }
+ end
+
+ def project_association
+ :milestones
+ end
+
+ def find_condition
+ { iid: number }
+ end
+
+ def number
+ if project.gitea_import?
+ raw_data.id
+ else
+ raw_data.number
+ end
+ end
+
+ private
+
+ def state
+ raw_data.state == 'closed' ? 'closed' : 'active'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/project_creator.rb b/lib/gitlab/legacy_github_import/project_creator.rb
new file mode 100644
index 00000000000..41e7eac4d08
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/project_creator.rb
@@ -0,0 +1,52 @@
+module Gitlab
+ module LegacyGithubImport
+ class ProjectCreator
+ include Gitlab::CurrentSettings
+
+ attr_reader :repo, :name, :namespace, :current_user, :session_data, :type
+
+ def initialize(repo, name, namespace, current_user, session_data, type: 'github')
+ @repo = repo
+ @name = name
+ @namespace = namespace
+ @current_user = current_user
+ @session_data = session_data
+ @type = type
+ end
+
+ def execute
+ ::Projects::CreateService.new(
+ current_user,
+ name: name,
+ path: name,
+ description: repo.description,
+ namespace_id: namespace.id,
+ visibility_level: visibility_level,
+ import_type: type,
+ import_source: repo.full_name,
+ import_url: import_url,
+ skip_wiki: skip_wiki
+ ).execute
+ end
+
+ private
+
+ def import_url
+ repo.clone_url.sub('://', "://#{session_data[:github_access_token]}@")
+ end
+
+ def visibility_level
+ repo.private ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility
+ end
+
+ #
+ # If the GitHub project repository has wiki, we should not create the
+ # default wiki. Otherwise the GitHub importer will fail because the wiki
+ # repository already exist.
+ #
+ def skip_wiki
+ repo.has_wiki?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/pull_request_formatter.rb b/lib/gitlab/legacy_github_import/pull_request_formatter.rb
new file mode 100644
index 00000000000..94c2e99066a
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/pull_request_formatter.rb
@@ -0,0 +1,90 @@
+module Gitlab
+ module LegacyGithubImport
+ class PullRequestFormatter < IssuableFormatter
+ delegate :user, :project, :ref, :repo, :sha, to: :source_branch, prefix: true
+ delegate :user, :exists?, :project, :ref, :repo, :sha, :short_sha, to: :target_branch, prefix: true
+
+ def attributes
+ {
+ iid: number,
+ title: raw_data.title,
+ description: description,
+ source_project: source_branch_project,
+ source_branch: source_branch_name,
+ source_branch_sha: source_branch_sha,
+ target_project: target_branch_project,
+ target_branch: target_branch_name,
+ target_branch_sha: target_branch_sha,
+ state: state,
+ milestone: milestone,
+ author_id: author_id,
+ assignee_id: assignee_id,
+ created_at: raw_data.created_at,
+ updated_at: raw_data.updated_at,
+ imported: true
+ }
+ end
+
+ def project_association
+ :merge_requests
+ end
+
+ def valid?
+ source_branch.valid? && target_branch.valid?
+ end
+
+ def source_branch
+ @source_branch ||= BranchFormatter.new(project, raw_data.head)
+ end
+
+ def source_branch_name
+ @source_branch_name ||=
+ if cross_project? || !source_branch_exists?
+ source_branch_name_prefixed
+ else
+ source_branch_ref
+ end
+ end
+
+ def source_branch_name_prefixed
+ "gh-#{target_branch_short_sha}/#{number}/#{source_branch_user}/#{source_branch_ref}"
+ end
+
+ def source_branch_exists?
+ !cross_project? && source_branch.exists?
+ end
+
+ def target_branch
+ @target_branch ||= BranchFormatter.new(project, raw_data.base)
+ end
+
+ def target_branch_name
+ @target_branch_name ||= target_branch_exists? ? target_branch_ref : target_branch_name_prefixed
+ end
+
+ def target_branch_name_prefixed
+ "gl-#{target_branch_short_sha}/#{number}/#{target_branch_user}/#{target_branch_ref}"
+ end
+
+ def cross_project?
+ return true if source_branch_repo.nil?
+
+ source_branch_repo.id != target_branch_repo.id
+ end
+
+ def opened?
+ state == 'opened'
+ end
+
+ private
+
+ def state
+ if raw_data.state == 'closed' && raw_data.merged_at.present?
+ 'merged'
+ else
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/release_formatter.rb b/lib/gitlab/legacy_github_import/release_formatter.rb
new file mode 100644
index 00000000000..3ed9d4f76da
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/release_formatter.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module LegacyGithubImport
+ class ReleaseFormatter < BaseFormatter
+ def attributes
+ {
+ project: project,
+ tag: raw_data.tag_name,
+ description: raw_data.body,
+ created_at: raw_data.created_at,
+ updated_at: raw_data.created_at
+ }
+ end
+
+ def project_association
+ :releases
+ end
+
+ def find_condition
+ { tag: raw_data.tag_name }
+ end
+
+ def valid?
+ !raw_data.draft
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/user_formatter.rb b/lib/gitlab/legacy_github_import/user_formatter.rb
new file mode 100644
index 00000000000..6d8055622f1
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/user_formatter.rb
@@ -0,0 +1,45 @@
+module Gitlab
+ module LegacyGithubImport
+ class UserFormatter
+ attr_reader :client, :raw
+
+ delegate :id, :login, to: :raw, allow_nil: true
+
+ def initialize(client, raw)
+ @client = client
+ @raw = raw
+ end
+
+ def gitlab_id
+ return @gitlab_id if defined?(@gitlab_id)
+
+ @gitlab_id = find_by_external_uid || find_by_email
+ end
+
+ private
+
+ def email
+ @email ||= client.user(raw.login).try(:email)
+ end
+
+ def find_by_email
+ return nil unless email
+
+ User.find_by_any_email(email)
+ .try(:id)
+ end
+
+ def find_by_external_uid
+ return nil unless id
+
+ identities = ::Identity.arel_table
+
+ User.select(:id)
+ .joins(:identities).where(identities[:provider].eq(:github)
+ .and(identities[:extern_uid].eq(id)))
+ .first
+ .try(:id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/legacy_github_import/wiki_formatter.rb b/lib/gitlab/legacy_github_import/wiki_formatter.rb
new file mode 100644
index 00000000000..27f45875c7c
--- /dev/null
+++ b/lib/gitlab/legacy_github_import/wiki_formatter.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module LegacyGithubImport
+ class WikiFormatter
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def disk_path
+ project.wiki.disk_path
+ end
+
+ def import_url
+ project.import_url.sub(/\.git\z/, ".wiki.git")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb
index 8e57ba831c5..ead5d566871 100644
--- a/lib/gitlab/lfs_token.rb
+++ b/lib/gitlab/lfs_token.rb
@@ -27,6 +27,10 @@ module Gitlab
end
end
+ def deploy_key_pushable?(project)
+ actor.is_a?(DeployKey) && actor.can_push_to?(project)
+ end
+
def user?
actor.is_a?(User)
end
diff --git a/lib/gitlab/logger.rb b/lib/gitlab/logger.rb
index 6bffd410ed0..a42e312b5d3 100644
--- a/lib/gitlab/logger.rb
+++ b/lib/gitlab/logger.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def self.read_latest
- path = Rails.root.join("log", file_name)
+ path = self.full_log_path
return [] unless File.readable?(path)
@@ -22,7 +22,15 @@ module Gitlab
end
def self.build
- new(Rails.root.join("log", file_name))
+ RequestStore[self.cache_key] ||= new(self.full_log_path)
+ end
+
+ def self.full_log_path
+ Rails.root.join("log", file_name)
+ end
+
+ def self.cache_key
+ 'logger:'.freeze + self.full_log_path.to_s
end
end
end
diff --git a/lib/gitlab/metrics/background_transaction.rb b/lib/gitlab/metrics/background_transaction.rb
new file mode 100644
index 00000000000..5919ebb1493
--- /dev/null
+++ b/lib/gitlab/metrics/background_transaction.rb
@@ -0,0 +1,14 @@
+module Gitlab
+ module Metrics
+ class BackgroundTransaction < Transaction
+ def initialize(worker_class)
+ super()
+ @worker_class = worker_class
+ end
+
+ def labels
+ { controller: @worker_class.name, action: 'perform' }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/base_sampler.rb b/lib/gitlab/metrics/base_sampler.rb
deleted file mode 100644
index 716d20bb91a..00000000000
--- a/lib/gitlab/metrics/base_sampler.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-require 'logger'
-module Gitlab
- module Metrics
- class BaseSampler < Daemon
- # interval - The sampling interval in seconds.
- def initialize(interval)
- interval_half = interval.to_f / 2
-
- @interval = interval
- @interval_steps = (-interval_half..interval_half).step(0.1).to_a
-
- super()
- end
-
- def safe_sample
- sample
- rescue => e
- Rails.logger.warn("#{self.class}: #{e}, stopping")
- stop
- end
-
- def sample
- raise NotImplementedError
- end
-
- # Returns the sleep interval with a random adjustment.
- #
- # The random adjustment is put in place to ensure we:
- #
- # 1. Don't generate samples at the exact same interval every time (thus
- # potentially missing anything that happens in between samples).
- # 2. Don't sample data at the same interval two times in a row.
- def sleep_interval
- while (step = @interval_steps.sample)
- if step != @last_step
- @last_step = step
-
- return @interval + @last_step
- end
- end
- end
-
- private
-
- attr_reader :running
-
- def start_working
- @running = true
- sleep(sleep_interval)
-
- while running
- safe_sample
-
- sleep(sleep_interval)
- end
- end
-
- def stop_working
- @running = false
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb
index 7b06bb953aa..bdf7910b7c7 100644
--- a/lib/gitlab/metrics/influx_db.rb
+++ b/lib/gitlab/metrics/influx_db.rb
@@ -11,6 +11,8 @@ module Gitlab
settings[:enabled] || false
end
+ # Prometheus histogram buckets used for arbitrary code measurements
+ EXECUTION_MEASUREMENT_BUCKETS = [0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1].freeze
RAILS_ROOT = Rails.root.to_s
METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s
PATH_REGEX = /^#{RAILS_ROOT}\/?/
@@ -99,24 +101,27 @@ module Gitlab
cpu_stop = System.cpu_time
real_stop = Time.now.to_f
- real_time = (real_stop - real_start) * 1000.0
+ real_time = (real_stop - real_start)
cpu_time = cpu_stop - cpu_start
- trans.increment("#{name}_real_time", real_time)
- trans.increment("#{name}_cpu_time", cpu_time)
- trans.increment("#{name}_call_count", 1)
+ Gitlab::Metrics.histogram("gitlab_#{name}_real_duration_seconds".to_sym,
+ "Measure #{name}",
+ Transaction::BASE_LABELS,
+ EXECUTION_MEASUREMENT_BUCKETS)
+ .observe(trans.labels, real_time)
- retval
- end
+ Gitlab::Metrics.histogram("gitlab_#{name}_cpu_duration_seconds".to_sym,
+ "Measure #{name}",
+ Transaction::BASE_LABELS,
+ EXECUTION_MEASUREMENT_BUCKETS)
+ .observe(trans.labels, cpu_time / 1000.0)
- # Adds a tag to the current transaction (if any)
- #
- # name - The name of the tag to add.
- # value - The value of the tag.
- def tag_transaction(name, value)
- trans = current_transaction
+ # InfluxDB stores the _real_time time values as milliseconds
+ trans.increment("#{name}_real_time", real_time * 1000, false)
+ trans.increment("#{name}_cpu_time", cpu_time, false)
+ trans.increment("#{name}_call_count", 1, false)
- trans&.add_tag(name, value)
+ retval
end
# Sets the action of the current transaction (if any)
diff --git a/lib/gitlab/metrics/influx_sampler.rb b/lib/gitlab/metrics/influx_sampler.rb
deleted file mode 100644
index 6db1dd755b7..00000000000
--- a/lib/gitlab/metrics/influx_sampler.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-module Gitlab
- module Metrics
- # Class that sends certain metrics to InfluxDB at a specific interval.
- #
- # This class is used to gather statistics that can't be directly associated
- # with a transaction such as system memory usage, garbage collection
- # statistics, etc.
- class InfluxSampler < BaseSampler
- # interval - The sampling interval in seconds.
- def initialize(interval = Metrics.settings[:sample_interval])
- super(interval)
- @last_step = nil
-
- @metrics = []
-
- @last_minor_gc = Delta.new(GC.stat[:minor_gc_count])
- @last_major_gc = Delta.new(GC.stat[:major_gc_count])
-
- if Gitlab::Metrics.mri?
- require 'allocations'
-
- Allocations.start
- end
- end
-
- def sample
- sample_memory_usage
- sample_file_descriptors
- sample_objects
- sample_gc
-
- flush
- ensure
- GC::Profiler.clear
- @metrics.clear
- end
-
- def flush
- Metrics.submit_metrics(@metrics.map(&:to_hash))
- end
-
- def sample_memory_usage
- add_metric('memory_usage', value: System.memory_usage)
- end
-
- def sample_file_descriptors
- add_metric('file_descriptors', value: System.file_descriptor_count)
- end
-
- if Metrics.mri?
- def sample_objects
- sample = Allocations.to_hash
- counts = sample.each_with_object({}) do |(klass, count), hash|
- name = klass.name
-
- next unless name
-
- hash[name] = count
- end
-
- # Symbols aren't allocated so we'll need to add those manually.
- counts['Symbol'] = Symbol.all_symbols.length
-
- counts.each do |name, count|
- add_metric('object_counts', { count: count }, type: name)
- end
- end
- else
- def sample_objects
- end
- end
-
- def sample_gc
- time = GC::Profiler.total_time * 1000.0
- stats = GC.stat.merge(total_time: time)
-
- # We want the difference of GC runs compared to the last sample, not the
- # total amount since the process started.
- stats[:minor_gc_count] =
- @last_minor_gc.compared_with(stats[:minor_gc_count])
-
- stats[:major_gc_count] =
- @last_major_gc.compared_with(stats[:major_gc_count])
-
- stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count]
-
- add_metric('gc_statistics', stats)
- end
-
- def add_metric(series, values, tags = {})
- prefix = sidekiq? ? 'sidekiq_' : 'rails_'
-
- @metrics << Metric.new("#{prefix}#{series}", values, tags)
- end
-
- def sidekiq?
- Sidekiq.server?
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index 6aa38542cb4..023e9963493 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -118,19 +118,21 @@ module Gitlab
def self.instrument(type, mod, name)
return unless Metrics.enabled?
- name = name.to_sym
+ name = name.to_sym
target = type == :instance ? mod : mod.singleton_class
if type == :instance
target = mod
- label = "#{mod.name}##{name}"
+ method_name = "##{name}"
method = mod.instance_method(name)
else
target = mod.singleton_class
- label = "#{mod.name}.#{name}"
+ method_name = ".#{name}"
method = mod.method(name)
end
+ label = "#{mod.name}#{method_name}"
+
unless instrumented?(target)
target.instance_variable_set(PROXY_IVAR, Module.new)
end
@@ -153,7 +155,8 @@ module Gitlab
proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
def #{name}(#{args_signature})
if trans = Gitlab::Metrics::Instrumentation.transaction
- trans.method_call_for(#{label.to_sym.inspect}).measure { super }
+ trans.method_call_for(#{label.to_sym.inspect}, #{mod.name.inspect}, "#{method_name}")
+ .measure { super }
else
super
end
diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb
index d3465e5ec19..65d55576ac2 100644
--- a/lib/gitlab/metrics/method_call.rb
+++ b/lib/gitlab/metrics/method_call.rb
@@ -2,15 +2,31 @@ module Gitlab
module Metrics
# Class for tracking timing information about method calls
class MethodCall
- attr_reader :real_time, :cpu_time, :call_count
+ MUTEX = Mutex.new
+ BASE_LABELS = { module: nil, method: nil }.freeze
+ attr_reader :real_time, :cpu_time, :call_count, :labels
+
+ def self.call_duration_histogram
+ return @call_duration_histogram if @call_duration_histogram
+
+ MUTEX.synchronize do
+ @call_duration_histogram ||= Gitlab::Metrics.histogram(
+ :gitlab_method_call_duration_seconds,
+ 'Method calls real duration',
+ Transaction::BASE_LABELS.merge(BASE_LABELS),
+ [0.01, 0.05, 0.1, 0.5, 1])
+ end
+ end
# name - The full name of the method (including namespace) such as
# `User#sign_in`.
#
- # series - The series to use for storing the data.
- def initialize(name, series)
+ def initialize(name, module_name, method_name, transaction)
+ @module_name = module_name
+ @method_name = method_name
+ @transaction = transaction
@name = name
- @series = series
+ @labels = { module: @module_name, method: @method_name }
@real_time = 0
@cpu_time = 0
@call_count = 0
@@ -22,21 +38,28 @@ module Gitlab
start_cpu = System.cpu_time
retval = yield
- @real_time += System.monotonic_time - start_real
- @cpu_time += System.cpu_time - start_cpu
+ real_time = System.monotonic_time - start_real
+ cpu_time = System.cpu_time - start_cpu
+
+ @real_time += real_time
+ @cpu_time += cpu_time
@call_count += 1
+ if call_measurement_enabled? && above_threshold?
+ self.class.call_duration_histogram.observe(@transaction.labels.merge(labels), real_time / 1000.0)
+ end
+
retval
end
# Returns a Metric instance of the current method call.
def to_metric
Metric.new(
- @series,
+ Instrumentation.series,
{
- duration: real_time,
+ duration: real_time,
cpu_duration: cpu_time,
- call_count: call_count
+ call_count: call_count
},
method: @name
)
@@ -47,6 +70,10 @@ module Gitlab
def above_threshold?
real_time >= Metrics.method_call_threshold
end
+
+ def call_measurement_enabled?
+ Feature.get(:prometheus_metrics_method_instrumentation).enabled?
+ end
end
end
end
diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb
index 460dab47276..09103b4ca2d 100644
--- a/lib/gitlab/metrics/prometheus.rb
+++ b/lib/gitlab/metrics/prometheus.rb
@@ -5,6 +5,9 @@ module Gitlab
module Prometheus
include Gitlab::CurrentSettings
+ REGISTRY_MUTEX = Mutex.new
+ PROVIDER_MUTEX = Mutex.new
+
def metrics_folder_present?
multiprocess_files_dir = ::Prometheus::Client.configuration.multiprocess_files_dir
@@ -20,23 +23,38 @@ module Gitlab
end
def registry
- @registry ||= ::Prometheus::Client.registry
+ return @registry if @registry
+
+ REGISTRY_MUTEX.synchronize do
+ @registry ||= ::Prometheus::Client.registry
+ end
end
def counter(name, docstring, base_labels = {})
- provide_metric(name) || registry.counter(name, docstring, base_labels)
+ safe_provide_metric(:counter, name, docstring, base_labels)
end
def summary(name, docstring, base_labels = {})
- provide_metric(name) || registry.summary(name, docstring, base_labels)
+ safe_provide_metric(:summary, name, docstring, base_labels)
end
def gauge(name, docstring, base_labels = {}, multiprocess_mode = :all)
- provide_metric(name) || registry.gauge(name, docstring, base_labels, multiprocess_mode)
+ safe_provide_metric(:gauge, name, docstring, base_labels, multiprocess_mode)
end
def histogram(name, docstring, base_labels = {}, buckets = ::Prometheus::Client::Histogram::DEFAULT_BUCKETS)
- provide_metric(name) || registry.histogram(name, docstring, base_labels, buckets)
+ safe_provide_metric(:histogram, name, docstring, base_labels, buckets)
+ end
+
+ private
+
+ def safe_provide_metric(method, name, *args)
+ metric = provide_metric(name)
+ return metric if metric
+
+ PROVIDER_MUTEX.synchronize do
+ provide_metric(name) || registry.method(method).call(name, *args)
+ end
end
def provide_metric(name)
@@ -47,8 +65,6 @@ module Gitlab
end
end
- private
-
def prometheus_metrics_enabled_unmemoized
metrics_folder_present? && current_application_settings[:prometheus_metrics_enabled] || false
end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index adc0db1a874..2d45765df3f 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -2,20 +2,6 @@ module Gitlab
module Metrics
# Rack middleware for tracking Rails and Grape requests.
class RackMiddleware
- CONTROLLER_KEY = 'action_controller.instance'.freeze
- ENDPOINT_KEY = 'api.endpoint'.freeze
- CONTENT_TYPES = {
- 'text/html' => :html,
- 'text/plain' => :txt,
- 'application/json' => :json,
- 'text/js' => :js,
- 'application/atom+xml' => :atom,
- 'image/png' => :png,
- 'image/jpeg' => :jpeg,
- 'image/gif' => :gif,
- 'image/svg+xml' => :svg
- }.freeze
-
def initialize(app)
@app = app
end
@@ -35,12 +21,6 @@ module Gitlab
# Even in the event of an error we want to submit any metrics we
# might've gathered up to this point.
ensure
- if env[CONTROLLER_KEY]
- tag_controller(trans, env)
- elsif env[ENDPOINT_KEY]
- tag_endpoint(trans, env)
- end
-
trans.finish
end
@@ -48,60 +28,19 @@ module Gitlab
end
def transaction_from_env(env)
- trans = Transaction.new
+ trans = WebTransaction.new(env)
- trans.set(:request_uri, filtered_path(env))
- trans.set(:request_method, env['REQUEST_METHOD'])
+ trans.set(:request_uri, filtered_path(env), false)
+ trans.set(:request_method, env['REQUEST_METHOD'], false)
trans
end
- def tag_controller(trans, env)
- controller = env[CONTROLLER_KEY]
- action = "#{controller.class.name}##{controller.action_name}"
- suffix = CONTENT_TYPES[controller.content_type]
-
- if suffix && suffix != :html
- action += ".#{suffix}"
- end
-
- trans.action = action
- end
-
- def tag_endpoint(trans, env)
- endpoint = env[ENDPOINT_KEY]
-
- begin
- route = endpoint.route
- rescue
- # endpoint.route is calling env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info]
- # but env[Grape::Env::GRAPE_ROUTING_ARGS] is nil in the case of a 405 response
- # so we're rescuing exceptions and bailing out
- end
-
- if route
- path = endpoint_paths_cache[route.request_method][route.path]
- trans.action = "Grape##{route.request_method} #{path}"
- end
- end
-
private
def filtered_path(env)
ActionDispatch::Request.new(env).filtered_path.presence || env['REQUEST_URI']
end
-
- def endpoint_paths_cache
- @endpoint_paths_cache ||= Hash.new do |hash, http_method|
- hash[http_method] = Hash.new do |inner_hash, raw_path|
- inner_hash[raw_path] = endpoint_instrumentable_path(raw_path)
- end
- end
- end
-
- def endpoint_instrumentable_path(raw_path)
- raw_path.sub('(.:format)', '').sub('/:version', '')
- end
end
end
end
diff --git a/lib/gitlab/metrics/samplers/base_sampler.rb b/lib/gitlab/metrics/samplers/base_sampler.rb
new file mode 100644
index 00000000000..37f90c4673d
--- /dev/null
+++ b/lib/gitlab/metrics/samplers/base_sampler.rb
@@ -0,0 +1,64 @@
+require 'logger'
+
+module Gitlab
+ module Metrics
+ module Samplers
+ class BaseSampler < Daemon
+ # interval - The sampling interval in seconds.
+ def initialize(interval)
+ interval_half = interval.to_f / 2
+
+ @interval = interval
+ @interval_steps = (-interval_half..interval_half).step(0.1).to_a
+
+ super()
+ end
+
+ def safe_sample
+ sample
+ rescue => e
+ Rails.logger.warn("#{self.class}: #{e}, stopping")
+ stop
+ end
+
+ def sample
+ raise NotImplementedError
+ end
+
+ # Returns the sleep interval with a random adjustment.
+ #
+ # The random adjustment is put in place to ensure we:
+ #
+ # 1. Don't generate samples at the exact same interval every time (thus
+ # potentially missing anything that happens in between samples).
+ # 2. Don't sample data at the same interval two times in a row.
+ def sleep_interval
+ while step = @interval_steps.sample
+ if step != @last_step
+ @last_step = step
+
+ return @interval + @last_step
+ end
+ end
+ end
+
+ private
+
+ attr_reader :running
+
+ def start_working
+ @running = true
+ sleep(sleep_interval)
+ while running
+ safe_sample
+ sleep(sleep_interval)
+ end
+ end
+
+ def stop_working
+ @running = false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/samplers/influx_sampler.rb b/lib/gitlab/metrics/samplers/influx_sampler.rb
new file mode 100644
index 00000000000..f4f9b5ca792
--- /dev/null
+++ b/lib/gitlab/metrics/samplers/influx_sampler.rb
@@ -0,0 +1,103 @@
+module Gitlab
+ module Metrics
+ module Samplers
+ # Class that sends certain metrics to InfluxDB at a specific interval.
+ #
+ # This class is used to gather statistics that can't be directly associated
+ # with a transaction such as system memory usage, garbage collection
+ # statistics, etc.
+ class InfluxSampler < BaseSampler
+ # interval - The sampling interval in seconds.
+ def initialize(interval = Metrics.settings[:sample_interval])
+ super(interval)
+ @last_step = nil
+
+ @metrics = []
+
+ @last_minor_gc = Delta.new(GC.stat[:minor_gc_count])
+ @last_major_gc = Delta.new(GC.stat[:major_gc_count])
+
+ if Gitlab::Metrics.mri?
+ require 'allocations'
+
+ Allocations.start
+ end
+ end
+
+ def sample
+ sample_memory_usage
+ sample_file_descriptors
+ sample_objects
+ sample_gc
+
+ flush
+ ensure
+ GC::Profiler.clear
+ @metrics.clear
+ end
+
+ def flush
+ Metrics.submit_metrics(@metrics.map(&:to_hash))
+ end
+
+ def sample_memory_usage
+ add_metric('memory_usage', value: System.memory_usage)
+ end
+
+ def sample_file_descriptors
+ add_metric('file_descriptors', value: System.file_descriptor_count)
+ end
+
+ if Metrics.mri?
+ def sample_objects
+ sample = Allocations.to_hash
+ counts = sample.each_with_object({}) do |(klass, count), hash|
+ name = klass.name
+
+ next unless name
+
+ hash[name] = count
+ end
+
+ # Symbols aren't allocated so we'll need to add those manually.
+ counts['Symbol'] = Symbol.all_symbols.length
+
+ counts.each do |name, count|
+ add_metric('object_counts', { count: count }, type: name)
+ end
+ end
+ else
+ def sample_objects
+ end
+ end
+
+ def sample_gc
+ time = GC::Profiler.total_time * 1000.0
+ stats = GC.stat.merge(total_time: time)
+
+ # We want the difference of GC runs compared to the last sample, not the
+ # total amount since the process started.
+ stats[:minor_gc_count] =
+ @last_minor_gc.compared_with(stats[:minor_gc_count])
+
+ stats[:major_gc_count] =
+ @last_major_gc.compared_with(stats[:major_gc_count])
+
+ stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count]
+
+ add_metric('gc_statistics', stats)
+ end
+
+ def add_metric(series, values, tags = {})
+ prefix = sidekiq? ? 'sidekiq_' : 'rails_'
+
+ @metrics << Metric.new("#{prefix}#{series}", values, tags)
+ end
+
+ def sidekiq?
+ Sidekiq.server?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
new file mode 100644
index 00000000000..436a9e9550d
--- /dev/null
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -0,0 +1,111 @@
+require 'prometheus/client/support/unicorn'
+
+module Gitlab
+ module Metrics
+ module Samplers
+ class RubySampler < BaseSampler
+ def metrics
+ @metrics ||= init_metrics
+ end
+
+ def with_prefix(prefix, name)
+ "ruby_#{prefix}_#{name}".to_sym
+ end
+
+ def to_doc_string(name)
+ name.to_s.humanize
+ end
+
+ def labels
+ {}
+ end
+
+ def initialize(interval)
+ super(interval)
+
+ if Metrics.mri?
+ require 'allocations'
+
+ Allocations.start
+ end
+ end
+
+ def init_metrics
+ metrics = {}
+ metrics[:sampler_duration] = Metrics.histogram(with_prefix(:sampler_duration, :seconds), 'Sampler time', {})
+ metrics[:total_time] = Metrics.gauge(with_prefix(:gc, :time_total), 'Total GC time', labels, :livesum)
+ GC.stat.keys.each do |key|
+ metrics[key] = Metrics.gauge(with_prefix(:gc, key), to_doc_string(key), labels, :livesum)
+ end
+
+ metrics[:objects_total] = Metrics.gauge(with_prefix(:objects, :total), 'Objects total', labels.merge(class: nil), :livesum)
+ metrics[:memory_usage] = Metrics.gauge(with_prefix(:memory, :usage_total), 'Memory used total', labels, :livesum)
+ metrics[:file_descriptors] = Metrics.gauge(with_prefix(:file, :descriptors_total), 'File descriptors total', labels, :livesum)
+
+ metrics
+ end
+
+ def sample
+ start_time = System.monotonic_time
+ sample_gc
+ sample_objects
+
+ metrics[:memory_usage].set(labels, System.memory_usage)
+ metrics[:file_descriptors].set(labels, System.file_descriptor_count)
+
+ metrics[:sampler_duration].observe(labels.merge(worker_label), (System.monotonic_time - start_time) / 1000.0)
+ ensure
+ GC::Profiler.clear
+ end
+
+ private
+
+ def sample_gc
+ metrics[:total_time].set(labels, GC::Profiler.total_time * 1000)
+
+ GC.stat.each do |key, value|
+ metrics[key].set(labels, value)
+ end
+ end
+
+ def sample_objects
+ list_objects.each do |name, count|
+ metrics[:objects_total].set(labels.merge(class: name), count)
+ end
+ end
+
+ if Metrics.mri?
+ def list_objects
+ sample = Allocations.to_hash
+ counts = sample.each_with_object({}) do |(klass, count), hash|
+ name = klass.name
+
+ next unless name
+
+ hash[name] = count
+ end
+
+ # Symbols aren't allocated so we'll need to add those manually.
+ counts['Symbol'] = Symbol.all_symbols.length
+ counts
+ end
+ else
+ def list_objects
+ end
+ end
+
+ def worker_label
+ return {} unless defined?(Unicorn::Worker)
+
+ worker_no = ::Prometheus::Client::Support::Unicorn.worker_id
+
+ if worker_no
+ { unicorn: worker_no }
+ else
+ { unicorn: 'master' }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
new file mode 100644
index 00000000000..ea325651fbb
--- /dev/null
+++ b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ module Metrics
+ module Samplers
+ class UnicornSampler < BaseSampler
+ def initialize(interval)
+ super(interval)
+ end
+
+ def unicorn_active_connections
+ @unicorn_active_connections ||= Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max)
+ end
+
+ def unicorn_queued_connections
+ @unicorn_queued_connections ||= Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max)
+ end
+
+ def enabled?
+ # Raindrops::Linux.tcp_listener_stats is only present on Linux
+ unicorn_with_listeners? && Raindrops::Linux.respond_to?(:tcp_listener_stats)
+ end
+
+ def sample
+ Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats|
+ unicorn_active_connections.set({ type: 'tcp', address: addr }, stats.active)
+ unicorn_queued_connections.set({ type: 'tcp', address: addr }, stats.queued)
+ end
+
+ Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats|
+ unicorn_active_connections.set({ type: 'unix', address: addr }, stats.active)
+ unicorn_queued_connections.set({ type: 'unix', address: addr }, stats.queued)
+ end
+ end
+
+ private
+
+ def tcp_listeners
+ @tcp_listeners ||= Unicorn.listener_names.grep(%r{\A[^/]+:\d+\z})
+ end
+
+ def unix_listeners
+ @unix_listeners ||= Unicorn.listener_names - tcp_listeners
+ end
+
+ def unicorn_with_listeners?
+ defined?(Unicorn) && Unicorn.listener_names.any?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
index f9dd8e41912..df4bdf16847 100644
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -5,7 +5,7 @@ module Gitlab
# This middleware is intended to be used as a server-side middleware.
class SidekiqMiddleware
def call(worker, message, queue)
- trans = Transaction.new("#{worker.class.name}#perform")
+ trans = BackgroundTransaction.new(worker.class)
begin
# Old gitlad-shell messages don't provide enqueued_at/created_at attributes
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
index d435a33e9c7..3da474fc1ec 100644
--- a/lib/gitlab/metrics/subscribers/action_view.rb
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -15,10 +15,24 @@ module Gitlab
private
+ def metric_view_rendering_duration_seconds
+ @metric_view_rendering_duration_seconds ||= Gitlab::Metrics.histogram(
+ :gitlab_view_rendering_duration_seconds,
+ 'View rendering time',
+ Transaction::BASE_LABELS.merge({ path: nil }),
+ [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.500, 2.0, 10.0]
+ )
+ end
+
def track(event)
values = values_for(event)
tags = tags_for(event)
+ metric_view_rendering_duration_seconds.observe(
+ current_transaction.labels.merge(tags),
+ event.duration
+ )
+
current_transaction.increment(:view_duration, event.duration)
current_transaction.add_metric(SERIES, values, tags)
end
diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb
index 96cad941d5c..ead1acb8d44 100644
--- a/lib/gitlab/metrics/subscribers/active_record.rb
+++ b/lib/gitlab/metrics/subscribers/active_record.rb
@@ -8,8 +8,10 @@ module Gitlab
def sql(event)
return unless current_transaction
- current_transaction.increment(:sql_duration, event.duration)
- current_transaction.increment(:sql_count, 1)
+ metric_sql_duration_seconds.observe(current_transaction.labels, event.duration / 1000.0)
+
+ current_transaction.increment(:sql_duration, event.duration, false)
+ current_transaction.increment(:sql_count, 1, false)
end
private
@@ -17,6 +19,15 @@ module Gitlab
def current_transaction
Transaction.current
end
+
+ def metric_sql_duration_seconds
+ @metric_sql_duration_seconds ||= Gitlab::Metrics.histogram(
+ :gitlab_sql_duration_seconds,
+ 'SQL time',
+ Transaction::BASE_LABELS,
+ [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.500, 2.0, 10.0]
+ )
+ end
end
end
end
diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb
index aaed2184f44..efd3c9daf79 100644
--- a/lib/gitlab/metrics/subscribers/rails_cache.rb
+++ b/lib/gitlab/metrics/subscribers/rails_cache.rb
@@ -7,28 +7,29 @@ module Gitlab
attach_to :active_support
def cache_read(event)
- increment(:cache_read, event.duration)
+ observe(:read, event.duration)
return unless current_transaction
return if event.payload[:super_operation] == :fetch
if event.payload[:hit]
- current_transaction.increment(:cache_read_hit_count, 1)
+ current_transaction.increment(:cache_read_hit_count, 1, false)
else
- current_transaction.increment(:cache_read_miss_count, 1)
+ metric_cache_misses_total.increment(current_transaction.labels)
+ current_transaction.increment(:cache_read_miss_count, 1, false)
end
end
def cache_write(event)
- increment(:cache_write, event.duration)
+ observe(:write, event.duration)
end
def cache_delete(event)
- increment(:cache_delete, event.duration)
+ observe(:delete, event.duration)
end
def cache_exist?(event)
- increment(:cache_exists, event.duration)
+ observe(:exists, event.duration)
end
def cache_fetch_hit(event)
@@ -40,16 +41,18 @@ module Gitlab
def cache_generate(event)
return unless current_transaction
+ metric_cache_misses_total.increment(current_transaction.labels)
current_transaction.increment(:cache_read_miss_count, 1)
end
- def increment(key, duration)
+ def observe(key, duration)
return unless current_transaction
- current_transaction.increment(:cache_duration, duration)
- current_transaction.increment(:cache_count, 1)
- current_transaction.increment("#{key}_duration".to_sym, duration)
- current_transaction.increment("#{key}_count".to_sym, 1)
+ metric_cache_operation_duration_seconds.observe(current_transaction.labels.merge({ operation: key }), duration / 1000.0)
+ current_transaction.increment(:cache_duration, duration, false)
+ current_transaction.increment(:cache_count, 1, false)
+ current_transaction.increment("cache_#{key}_duration".to_sym, duration, false)
+ current_transaction.increment("cache_#{key}_count".to_sym, 1, false)
end
private
@@ -57,6 +60,23 @@ module Gitlab
def current_transaction
Transaction.current
end
+
+ def metric_cache_operation_duration_seconds
+ @metric_cache_operation_duration_seconds ||= Gitlab::Metrics.histogram(
+ :gitlab_cache_operation_duration_seconds,
+ 'Cache access time',
+ Transaction::BASE_LABELS.merge({ action: nil }),
+ [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.500, 2.0, 10.0]
+ )
+ end
+
+ def metric_cache_misses_total
+ @metric_cache_misses_total ||= Gitlab::Metrics.counter(
+ :gitlab_cache_misses_total,
+ 'Cache read miss',
+ Transaction::BASE_LABELS
+ )
+ end
end
end
end
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index aba3e0df382..c2cbd3c16a1 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -46,14 +46,14 @@ module Gitlab
# Returns the current real time in a given precision.
#
- # Returns the time as a Float.
+ # Returns the time as a Fixnum.
def self.real_time(precision = :millisecond)
Process.clock_gettime(Process::CLOCK_REALTIME, precision)
end
# Returns the current monotonic clock time in a given precision.
#
- # Returns the time as a Float.
+ # Returns the time as a Fixnum.
def self.monotonic_time(precision = :millisecond)
Process.clock_gettime(Process::CLOCK_MONOTONIC, precision)
end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 4f9fb1c7853..ee3afc5ffdb 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -2,34 +2,33 @@ module Gitlab
module Metrics
# Class for storing metrics information of a single transaction.
class Transaction
+ # base labels shared among all transactions
+ BASE_LABELS = { controller: nil, action: nil }.freeze
+
THREAD_KEY = :_gitlab_metrics_transaction
+ METRICS_MUTEX = Mutex.new
# The series to store events (e.g. Git pushes) in.
EVENT_SERIES = 'events'.freeze
attr_reader :tags, :values, :method, :metrics
- attr_accessor :action
-
def self.current
Thread.current[THREAD_KEY]
end
- # action - A String describing the action performed, usually the class
- # plus method name.
- def initialize(action = nil)
+ def initialize
@metrics = []
@methods = {}
- @started_at = nil
+ @started_at = nil
@finished_at = nil
@values = Hash.new(0)
- @tags = {}
- @action = action
+ @tags = {}
@memory_before = 0
- @memory_after = 0
+ @memory_after = 0
end
def duration
@@ -44,12 +43,15 @@ module Gitlab
Thread.current[THREAD_KEY] = self
@memory_before = System.memory_usage
- @started_at = System.monotonic_time
+ @started_at = System.monotonic_time
yield
ensure
@memory_after = System.memory_usage
- @finished_at = System.monotonic_time
+ @finished_at = System.monotonic_time
+
+ self.class.metric_transaction_duration_seconds.observe(labels, duration * 1000)
+ self.class.metric_transaction_allocated_memory_bytes.observe(labels, allocated_memory * 1024.0)
Thread.current[THREAD_KEY] = nil
end
@@ -66,33 +68,29 @@ module Gitlab
# event_name - The name of the event (e.g. "git_push").
# tags - A set of tags to attach to the event.
def add_event(event_name, tags = {})
- @metrics << Metric.new(EVENT_SERIES,
- { count: 1 },
- { event: event_name }.merge(tags),
- :event)
+ self.class.metric_event_counter(event_name, tags).increment(tags.merge(labels))
+ @metrics << Metric.new(EVENT_SERIES, { count: 1 }, tags.merge(event: event_name), :event)
end
# Returns a MethodCall object for the given name.
- def method_call_for(name)
+ def method_call_for(name, module_name, method_name)
unless method = @methods[name]
- @methods[name] = method = MethodCall.new(name, Instrumentation.series)
+ @methods[name] = method = MethodCall.new(name, module_name, method_name, self)
end
method
end
- def increment(name, value)
+ def increment(name, value, use_prometheus = true)
+ self.class.metric_transaction_counter(name).increment(labels, value) if use_prometheus
@values[name] += value
end
- def set(name, value)
+ def set(name, value, use_prometheus = true)
+ self.class.metric_transaction_gauge(name).set(labels, value) if use_prometheus
@values[name] = value
end
- def add_tag(key, value)
- @tags[key] = value
- end
-
def finish
track_self
submit
@@ -117,14 +115,83 @@ module Gitlab
submit_hashes = submit.map do |metric|
hash = metric.to_hash
-
- hash[:tags][:action] ||= @action if @action && !metric.event?
+ hash[:tags][:action] ||= action if action && !metric.event?
hash
end
Metrics.submit_metrics(submit_hashes)
end
+
+ def labels
+ BASE_LABELS
+ end
+
+ # returns string describing the action performed, usually the class plus method name.
+ def action
+ "#{labels[:controller]}##{labels[:action]}" if labels && !labels.empty?
+ end
+
+ def self.metric_transaction_duration_seconds
+ return @metric_transaction_duration_seconds if @metric_transaction_duration_seconds
+
+ METRICS_MUTEX.synchronize do
+ @metric_transaction_duration_seconds ||= Gitlab::Metrics.histogram(
+ :gitlab_transaction_duration_seconds,
+ 'Transaction duration',
+ BASE_LABELS,
+ [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.500, 2.0, 10.0]
+ )
+ end
+ end
+
+ def self.metric_transaction_allocated_memory_bytes
+ return @metric_transaction_allocated_memory_bytes if @metric_transaction_allocated_memory_bytes
+
+ METRICS_MUTEX.synchronize do
+ @metric_transaction_allocated_memory_bytes ||= Gitlab::Metrics.histogram(
+ :gitlab_transaction_allocated_memory_bytes,
+ 'Transaction allocated memory bytes',
+ BASE_LABELS,
+ [1000, 10000, 20000, 500000, 1000000, 2000000, 5000000, 10000000, 20000000, 100000000]
+ )
+ end
+ end
+
+ def self.metric_event_counter(event_name, tags)
+ return @metric_event_counters[event_name] if @metric_event_counters&.has_key?(event_name)
+
+ METRICS_MUTEX.synchronize do
+ @metric_event_counters ||= {}
+ @metric_event_counters[event_name] ||= Gitlab::Metrics.counter(
+ "gitlab_transaction_event_#{event_name}_total".to_sym,
+ "Transaction event #{event_name} counter",
+ tags.merge(BASE_LABELS)
+ )
+ end
+ end
+
+ def self.metric_transaction_counter(name)
+ return @metric_transaction_counters[name] if @metric_transaction_counters&.has_key?(name)
+
+ METRICS_MUTEX.synchronize do
+ @metric_transaction_counters ||= {}
+ @metric_transaction_counters[name] ||= Gitlab::Metrics.counter(
+ "gitlab_transaction_#{name}_total".to_sym, "Transaction #{name} counter", BASE_LABELS
+ )
+ end
+ end
+
+ def self.metric_transaction_gauge(name)
+ return @metric_transaction_gauges[name] if @metric_transaction_gauges&.has_key?(name)
+
+ METRICS_MUTEX.synchronize do
+ @metric_transaction_gauges ||= {}
+ @metric_transaction_gauges[name] ||= Gitlab::Metrics.gauge(
+ "gitlab_transaction_#{name}".to_sym, "Transaction gauge #{name}", BASE_LABELS, :livesum
+ )
+ end
+ end
end
end
end
diff --git a/lib/gitlab/metrics/unicorn_sampler.rb b/lib/gitlab/metrics/unicorn_sampler.rb
deleted file mode 100644
index f6987252039..00000000000
--- a/lib/gitlab/metrics/unicorn_sampler.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-module Gitlab
- module Metrics
- class UnicornSampler < BaseSampler
- def initialize(interval)
- super(interval)
- end
-
- def unicorn_active_connections
- @unicorn_active_connections ||= Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max)
- end
-
- def unicorn_queued_connections
- @unicorn_queued_connections ||= Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max)
- end
-
- def enabled?
- # Raindrops::Linux.tcp_listener_stats is only present on Linux
- unicorn_with_listeners? && Raindrops::Linux.respond_to?(:tcp_listener_stats)
- end
-
- def sample
- Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats|
- unicorn_active_connections.set({ type: 'tcp', address: addr }, stats.active)
- unicorn_queued_connections.set({ type: 'tcp', address: addr }, stats.queued)
- end
-
- Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats|
- unicorn_active_connections.set({ type: 'unix', address: addr }, stats.active)
- unicorn_queued_connections.set({ type: 'unix', address: addr }, stats.queued)
- end
- end
-
- private
-
- def tcp_listeners
- @tcp_listeners ||= Unicorn.listener_names.grep(%r{\A[^/]+:\d+\z})
- end
-
- def unix_listeners
- @unix_listeners ||= Unicorn.listener_names - tcp_listeners
- end
-
- def unicorn_with_listeners?
- defined?(Unicorn) && Unicorn.listener_names.any?
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/web_transaction.rb b/lib/gitlab/metrics/web_transaction.rb
new file mode 100644
index 00000000000..89ff02a96d6
--- /dev/null
+++ b/lib/gitlab/metrics/web_transaction.rb
@@ -0,0 +1,82 @@
+module Gitlab
+ module Metrics
+ class WebTransaction < Transaction
+ CONTROLLER_KEY = 'action_controller.instance'.freeze
+ ENDPOINT_KEY = 'api.endpoint'.freeze
+
+ CONTENT_TYPES = {
+ 'text/html' => :html,
+ 'text/plain' => :txt,
+ 'application/json' => :json,
+ 'text/js' => :js,
+ 'application/atom+xml' => :atom,
+ 'image/png' => :png,
+ 'image/jpeg' => :jpeg,
+ 'image/gif' => :gif,
+ 'image/svg+xml' => :svg
+ }.freeze
+
+ def initialize(env)
+ super()
+ @env = env
+ end
+
+ def labels
+ return @labels if @labels
+
+ # memoize transaction labels only source env variables were present
+ @labels = if @env[CONTROLLER_KEY]
+ labels_from_controller || {}
+ elsif @env[ENDPOINT_KEY]
+ labels_from_endpoint || {}
+ end
+
+ @labels || {}
+ end
+
+ private
+
+ def labels_from_controller
+ controller = @env[CONTROLLER_KEY]
+
+ action = "#{controller.action_name}"
+ suffix = CONTENT_TYPES[controller.content_type]
+
+ if suffix && suffix != :html
+ action += ".#{suffix}"
+ end
+
+ { controller: controller.class.name, action: action }
+ end
+
+ def labels_from_endpoint
+ endpoint = @env[ENDPOINT_KEY]
+
+ begin
+ route = endpoint.route
+ rescue
+ # endpoint.route is calling env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info]
+ # but env[Grape::Env::GRAPE_ROUTING_ARGS] is nil in the case of a 405 response
+ # so we're rescuing exceptions and bailing out
+ end
+
+ if route
+ path = endpoint_paths_cache[route.request_method][route.path]
+ { controller: 'Grape', action: "#{route.request_method} #{path}" }
+ end
+ end
+
+ def endpoint_paths_cache
+ @endpoint_paths_cache ||= Hash.new do |hash, http_method|
+ hash[http_method] = Hash.new do |inner_hash, raw_path|
+ inner_hash[raw_path] = endpoint_instrumentable_path(raw_path)
+ end
+ end
+ end
+
+ def endpoint_instrumentable_path(raw_path)
+ raw_path.sub('(.:format)', '').sub('/:version', '')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index f42168c720e..c6a56277922 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -4,6 +4,7 @@ module Gitlab
module Middleware
class Go
include ActionView::Helpers::TagHelper
+ include Gitlab::CurrentSettings
PROJECT_PATH_REGEX = %r{\A(#{Gitlab::PathRegex.full_namespace_route_regex}/#{Gitlab::PathRegex.project_route_regex})/}.freeze
@@ -37,10 +38,19 @@ module Gitlab
end
def go_body(path)
- project_url = URI.join(Gitlab.config.gitlab.url, path)
+ config = Gitlab.config
+ project_url = URI.join(config.gitlab.url, path)
import_prefix = strip_url(project_url.to_s)
- meta_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{project_url}.git"
+ repository_url = if current_application_settings.enabled_git_access_protocol == 'ssh'
+ shell = config.gitlab_shell
+ port = ":#{shell.ssh_port}" unless shell.ssh_port == 22
+ "ssh://#{shell.ssh_user}@#{shell.ssh_host}#{port}/#{path}.git"
+ else
+ "#{project_url}.git"
+ end
+
+ meta_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{repository_url}"
head_tag = content_tag :head, meta_tag
content_tag :html, head_tag
end
@@ -55,6 +65,7 @@ module Gitlab
project_path_match = "#{path_info}/".match(PROJECT_PATH_REGEX)
return unless project_path_match
+
path = project_path_match[1]
# Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`.
diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb
index 63c3372da51..bc70b2459ef 100644
--- a/lib/gitlab/middleware/rails_queue_duration.rb
+++ b/lib/gitlab/middleware/rails_queue_duration.rb
@@ -14,11 +14,22 @@ module Gitlab
proxy_start = env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence
if trans && proxy_start
# Time in milliseconds since gitlab-workhorse started the request
- trans.set(:rails_queue_duration, Time.now.to_f * 1_000 - proxy_start.to_f / 1_000_000)
+ duration = Time.now.to_f * 1_000 - proxy_start.to_f / 1_000_000
+ trans.set(:rails_queue_duration, duration)
+ metric_rails_queue_duration_seconds.observe(trans.labels, duration / 1_000)
end
@app.call(env)
end
+
+ private
+
+ def metric_rails_queue_duration_seconds
+ @metric_rails_queue_duration_seconds ||= Gitlab::Metrics.histogram(
+ :gitlab_rails_queue_duration_seconds,
+ Gitlab::Metrics::Transaction::BASE_LABELS
+ )
+ end
end
end
end
diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb
index 0de0cddcce4..c26656704d7 100644
--- a/lib/gitlab/middleware/read_only.rb
+++ b/lib/gitlab/middleware/read_only.rb
@@ -12,6 +12,7 @@ module Gitlab
def call(env)
@env = env
+ @route_hash = nil
if disallowed_request? && Gitlab::Database.read_only?
Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation')
@@ -57,7 +58,7 @@ module Gitlab
end
def last_visited_url
- @env['HTTP_REFERER'] || rack_session['user_return_to'] || Rails.application.routes.url_helpers.root_url
+ @env['HTTP_REFERER'] || rack_session['user_return_to'] || Gitlab::Routing.url_helpers.root_url
end
def route_hash
@@ -65,11 +66,7 @@ module Gitlab
end
def whitelisted_routes
- logout_route || grack_route || @whitelisted.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
- end
-
- def logout_route
- route_hash[:controller] == 'sessions' && route_hash[:action] == 'destroy'
+ grack_route || @whitelisted.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
end
def sidekiq_route
@@ -77,11 +74,17 @@ module Gitlab
end
def grack_route
- request.path.end_with?('.git/git-upload-pack')
+ # Calling route_hash may be expensive. Only do it if we think there's a possible match
+ return false unless request.path.end_with?('.git/git-upload-pack')
+
+ route_hash[:controller] == 'projects/git_http' && route_hash[:action] == 'git_upload_pack'
end
def lfs_route
- request.path.end_with?('/info/lfs/objects/batch')
+ # Calling route_hash may be expensive. Only do it if we think there's a possible match
+ return false unless request.path.end_with?('/info/lfs/objects/batch')
+
+ route_hash[:controller] == 'projects/lfs_api' && route_hash[:action] == 'batch'
end
end
end
diff --git a/lib/gitlab/multi_collection_paginator.rb b/lib/gitlab/multi_collection_paginator.rb
new file mode 100644
index 00000000000..c22d0a84860
--- /dev/null
+++ b/lib/gitlab/multi_collection_paginator.rb
@@ -0,0 +1,63 @@
+module Gitlab
+ class MultiCollectionPaginator
+ attr_reader :first_collection, :second_collection, :per_page
+
+ def initialize(*collections, per_page: nil)
+ raise ArgumentError.new('Only 2 collections are supported') if collections.size != 2
+
+ @per_page = per_page || Kaminari.config.default_per_page
+ @first_collection, @second_collection = collections
+ end
+
+ def paginate(page)
+ page = page.to_i
+ paginated_first_collection(page) + paginated_second_collection(page)
+ end
+
+ def total_count
+ @total_count ||= first_collection.size + second_collection.size
+ end
+
+ private
+
+ def paginated_first_collection(page)
+ @first_collection_pages ||= Hash.new do |hash, page|
+ hash[page] = first_collection.page(page).per(per_page)
+ end
+
+ @first_collection_pages[page]
+ end
+
+ def paginated_second_collection(page)
+ @second_collection_pages ||= Hash.new do |hash, page|
+ second_collection_page = page - first_collection_page_count
+
+ offset = if second_collection_page < 1 || first_collection_page_count.zero?
+ 0
+ else
+ per_page - first_collection_last_page_size
+ end
+ hash[page] = second_collection.page(second_collection_page)
+ .per(per_page - paginated_first_collection(page).size)
+ .padding(offset)
+ end
+
+ @second_collection_pages[page]
+ end
+
+ def first_collection_page_count
+ return @first_collection_page_count if defined?(@first_collection_page_count)
+
+ first_collection_page = paginated_first_collection(0)
+ @first_collection_page_count = first_collection_page.total_pages
+ end
+
+ def first_collection_last_page_size
+ return @first_collection_last_page_size if defined?(@first_collection_last_page_size)
+
+ @first_collection_last_page_size = paginated_first_collection(first_collection_page_count)
+ .except(:select)
+ .size
+ end
+ end
+end
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 47c2a422387..552133234a3 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -157,7 +157,7 @@ module Gitlab
end
def find_by_uid_and_provider
- identity = Identity.find_by(provider: auth_hash.provider, extern_uid: auth_hash.uid)
+ identity = Identity.with_extern_uid(auth_hash.provider, auth_hash.uid).take
identity && identity.user
end
@@ -179,7 +179,7 @@ module Gitlab
valid_username = ::Namespace.clean_path(username)
uniquify = Uniquify.new
- valid_username = uniquify.string(valid_username) { |s| !DynamicPathValidator.valid_user_path?(s) }
+ valid_username = uniquify.string(valid_username) { |s| !UserPathValidator.valid_path?(s) }
name = auth_hash.name
name = valid_username if name.strip.empty?
diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb
index 962ff4d3985..1d9a5d1a20a 100644
--- a/lib/gitlab/optimistic_locking.rb
+++ b/lib/gitlab/optimistic_locking.rb
@@ -11,6 +11,7 @@ module Gitlab
rescue ActiveRecord::StaleObjectError
retries -= 1
raise unless retries >= 0
+
subject.reload
end
end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index e68160c8faf..7e5dfd33502 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -26,14 +26,12 @@ module Gitlab
apple-touch-icon.png
assets
autocomplete
- boards
ci
dashboard
deploy.html
explore
favicon.ico
files
- google_api
groups
health_check
help
@@ -53,7 +51,6 @@ module Gitlab
slash-command-logo.png
snippets
u
- unicorn_test
unsubscribes
uploads
users
@@ -114,23 +111,6 @@ module Gitlab
# this would map to the activity-page of its parent.
GROUP_ROUTES = %w[
-
- activity
- analytics
- audit_events
- avatar
- edit
- group_members
- hooks
- issues
- labels
- ldap
- ldap_group_links
- merge_requests
- milestones
- notification_setting
- pipeline_quota
- projects
- subgroups
].freeze
ILLEGAL_PROJECT_PATH_WORDS = PROJECT_WILDCARD_ROUTES
diff --git a/lib/gitlab/performance_bar/peek_query_tracker.rb b/lib/gitlab/performance_bar/peek_query_tracker.rb
index 67fee8c227d..f2825db59ae 100644
--- a/lib/gitlab/performance_bar/peek_query_tracker.rb
+++ b/lib/gitlab/performance_bar/peek_query_tracker.rb
@@ -36,8 +36,8 @@ module Gitlab
end
def track_query(raw_query, bindings, start, finish)
- query = Gitlab::Sherlock::Query.new(raw_query, start, finish)
- query_info = { duration: query.duration.round(3), sql: query.formatted_query }
+ duration = (finish - start) * 1000.0
+ query_info = { duration: duration.round(3), sql: raw_query }
PEEK_DB_CLIENT.query_details << query_info
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 561aa9e162c..e2662fc362b 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -47,8 +47,11 @@ module Gitlab
startline = 0
result.each_line.each_with_index do |line, index|
- if line =~ /^.*:.*:\d+:/
- ref, filename, startline = line.split(':')
+ matches = line.match(/^(?<ref>[^:]*):(?<filename>.*):(?<startline>\d+):/)
+ if matches
+ ref = matches[:ref]
+ filename = matches[:filename]
+ startline = matches[:startline]
startline = startline.to_i - index
extname = Regexp.escape(File.extname(filename))
basename = filename.sub(/#{extname}$/, '')
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index 732fbf68dad..ae136202f0c 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -1,9 +1,9 @@
module Gitlab
class ProjectTemplate
- attr_reader :title, :name
+ attr_reader :title, :name, :description, :preview
- def initialize(name, title)
- @name, @title = name, title
+ def initialize(name, title, description, preview)
+ @name, @title, @description, @preview = name, title, description, preview
end
alias_method :logo, :name
@@ -25,9 +25,9 @@ module Gitlab
end
TEMPLATES_TABLE = [
- ProjectTemplate.new('rails', 'Ruby on Rails'),
- ProjectTemplate.new('spring', 'Spring'),
- ProjectTemplate.new('express', 'NodeJS Express')
+ ProjectTemplate.new('rails', 'Ruby on Rails', 'Includes an MVC structure, gemfile, rakefile, and .gitlab-ci.yml file, along with many others, to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/rails'),
+ ProjectTemplate.new('spring', 'Spring', 'Includes an MVC structure, mvnw, pom.xml, and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/spring'),
+ ProjectTemplate.new('express', 'NodeJS Express', 'Includes an MVC structure and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/express')
].freeze
class << self
diff --git a/lib/gitlab/prometheus/queries/query_additional_metrics.rb b/lib/gitlab/prometheus/queries/query_additional_metrics.rb
index 7ac6162b54d..5cddc96a643 100644
--- a/lib/gitlab/prometheus/queries/query_additional_metrics.rb
+++ b/lib/gitlab/prometheus/queries/query_additional_metrics.rb
@@ -76,7 +76,7 @@ module Gitlab
timeframe_start: timeframe_start,
timeframe_end: timeframe_end,
ci_environment_slug: environment.slug,
- kube_namespace: environment.project.kubernetes_service&.actual_namespace || '',
+ kube_namespace: environment.project.deployment_platform&.actual_namespace || '',
environment_filter: %{container_name!="POD",environment="#{environment.slug}"}
}
end
diff --git a/lib/gitlab/quick_actions/spend_time_and_date_separator.rb b/lib/gitlab/quick_actions/spend_time_and_date_separator.rb
new file mode 100644
index 00000000000..3f52402b31f
--- /dev/null
+++ b/lib/gitlab/quick_actions/spend_time_and_date_separator.rb
@@ -0,0 +1,54 @@
+module Gitlab
+ module QuickActions
+ # This class takes spend command argument
+ # and separates date and time from spend command arguments if it present
+ # example:
+ # spend_command_time_and_date = "15m 2017-01-02"
+ # SpendTimeAndDateSeparator.new(spend_command_time_and_date).execute
+ # => [900, Mon, 02 Jan 2017]
+ # if date doesn't present return time with current date
+ # in other cases return nil
+ class SpendTimeAndDateSeparator
+ DATE_REGEX = /(\d{2,4}[\/\-.]\d{1,2}[\/\-.]\d{1,2})/
+
+ def initialize(spend_command_arg)
+ @spend_arg = spend_command_arg
+ end
+
+ def execute
+ return if @spend_arg.blank?
+ return [get_time, DateTime.now.to_date] unless date_present?
+ return unless valid_date?
+
+ [get_time, get_date]
+ end
+
+ private
+
+ def get_time
+ raw_time = @spend_arg.gsub(DATE_REGEX, '')
+ Gitlab::TimeTrackingFormatter.parse(raw_time)
+ end
+
+ def get_date
+ string_date = @spend_arg.match(DATE_REGEX)[0]
+ Date.parse(string_date)
+ end
+
+ def date_present?
+ DATE_REGEX =~ @spend_arg
+ end
+
+ def valid_date?
+ string_date = @spend_arg.match(DATE_REGEX)[0]
+ date = Date.parse(string_date) rescue nil
+
+ date_past_or_today?(date)
+ end
+
+ def date_past_or_today?(date)
+ date&.past? || date&.today?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index bd677ec4bf3..2c7b8af83f2 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -25,7 +25,7 @@ module Gitlab
# See https://github.com/docker/distribution/blob/master/reference/regexp.go.
#
def container_repository_name_regex
- @container_repository_regex ||= %r{\A[a-z0-9]+(?:[-._/][a-z0-9]+)*\Z}
+ @container_repository_regex ||= %r{\A[a-z0-9]+((?:[._/]|__|[-])[a-z0-9]+)*\Z}
end
##
diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb
index e57890f1143..2c994536060 100644
--- a/lib/gitlab/routing.rb
+++ b/lib/gitlab/routing.rb
@@ -40,5 +40,24 @@ module Gitlab
def self.url_helpers
@url_helpers ||= Gitlab::Application.routes.url_helpers
end
+
+ def self.redirect_legacy_paths(router, *paths)
+ build_redirect_path = lambda do |request, _params, path|
+ # Only replace the last occurence of `path`.
+ #
+ # `request.fullpath` includes the querystring
+ new_path = request.path.sub(%r{/#{path}(/*)(?!.*#{path})}, "/-/#{path}\\1")
+ new_path << "?#{request.query_string}" if request.query_string.present?
+
+ new_path
+ end
+
+ paths.each do |path|
+ router.match "/#{path}(/*rest)",
+ via: [:get, :post, :patch, :delete],
+ to: router.redirect { |params, request| build_redirect_path.call(request, params, path) },
+ as: "legacy_#{path}_redirect"
+ end
+ end
end
end
diff --git a/lib/gitlab/saml/auth_hash.rb b/lib/gitlab/saml/auth_hash.rb
index 67a5f368bdb..33d19373098 100644
--- a/lib/gitlab/saml/auth_hash.rb
+++ b/lib/gitlab/saml/auth_hash.rb
@@ -2,7 +2,7 @@ module Gitlab
module Saml
class AuthHash < Gitlab::OAuth::AuthHash
def groups
- get_raw(Gitlab::Saml::Config.groups)
+ Array.wrap(get_raw(Gitlab::Saml::Config.groups))
end
private
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
index e0a9d1dee77..d8faf7aad8c 100644
--- a/lib/gitlab/saml/user.rb
+++ b/lib/gitlab/saml/user.rb
@@ -28,6 +28,7 @@ module Gitlab
def changed?
return true unless gl_user
+
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index efe8095beea..fef9d3e31d4 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -30,7 +30,7 @@ module Gitlab
def initialize(current_user, limit_projects, query)
@current_user = current_user
@limit_projects = limit_projects || Project.all
- @query = Shellwords.shellescape(query) if query.present?
+ @query = query
end
def objects(scope, page = nil)
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index a37112ae5c4..996d213fdb4 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -101,8 +101,7 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
def import_repository(storage, name, url)
- # Timeout should be less than 900 ideally, to prevent the memory killer
- # to silently kill the process without knowing we are timing out here.
+ # The timeout ensures the subprocess won't hang forever
cmd = [gitlab_shell_projects_path, 'import-project',
storage, "#{name}.git", url, "#{Gitlab.config.gitlab_shell.git_timeout}"]
gitlab_shell_fast_execute_raise_error(cmd)
@@ -368,6 +367,7 @@ module Gitlab
output, status = gitlab_shell_fast_execute_helper(cmd, vars)
raise Error, output unless status.zero?
+
true
end
diff --git a/lib/gitlab/shell_adapter.rb b/lib/gitlab/shell_adapter.rb
index fbe2a7a0d72..053dd4ab9e0 100644
--- a/lib/gitlab/shell_adapter.rb
+++ b/lib/gitlab/shell_adapter.rb
@@ -5,7 +5,7 @@
module Gitlab
module ShellAdapter
def gitlab_shell
- Gitlab::Shell.new
+ @gitlab_shell ||= Gitlab::Shell.new
end
end
end
diff --git a/lib/gitlab/sherlock/transaction.rb b/lib/gitlab/sherlock/transaction.rb
index 3489fb251b6..400a552bf99 100644
--- a/lib/gitlab/sherlock/transaction.rb
+++ b/lib/gitlab/sherlock/transaction.rb
@@ -89,7 +89,9 @@ module Gitlab
ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, data|
next unless same_thread?
- track_query(data[:sql].strip, data[:binds], start, finish)
+ unless data.fetch(:cached, data[:name] == 'CACHE')
+ track_query(data[:sql].strip, data[:binds], start, finish)
+ end
end
end
diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb
index d7d24eeb37b..2bfb7caefd9 100644
--- a/lib/gitlab/sidekiq_middleware/memory_killer.rb
+++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb
@@ -7,7 +7,6 @@ module Gitlab
GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i
# Wait 30 seconds for running jobs to finish during graceful shutdown
SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i
- SHUTDOWN_SIGNAL = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL'] || 'SIGKILL').to_s
# Create a mutex used to ensure there will be only one thread waiting to
# shut Sidekiq down
@@ -15,6 +14,7 @@ module Gitlab
def call(worker, job, queue)
yield
+
current_rss = get_rss
return unless MAX_RSS > 0 && current_rss > MAX_RSS
@@ -23,32 +23,45 @@ module Gitlab
# Return if another thread is already waiting to shut Sidekiq down
return unless MUTEX.try_lock
- Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\
- "#{MAX_RSS}"
- Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']} "\
- "in #{GRACE_TIME} seconds"
- sleep(GRACE_TIME)
+ Sidekiq.logger.warn "Sidekiq worker PID-#{pid} current RSS #{current_rss}"\
+ " exceeds maximum RSS #{MAX_RSS} after finishing job #{worker.class} JID-#{job['jid']}"
+ Sidekiq.logger.warn "Sidekiq worker PID-#{pid} will stop fetching new jobs in #{GRACE_TIME} seconds, and will be shut down #{SHUTDOWN_WAIT} seconds later"
- Sidekiq.logger.warn "sending SIGTERM to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"
- Process.kill('SIGTERM', Process.pid)
+ # Wait `GRACE_TIME` to give the memory intensive job time to finish.
+ # Then, tell Sidekiq to stop fetching new jobs.
+ wait_and_signal(GRACE_TIME, 'SIGSTP', 'stop fetching new jobs')
- Sidekiq.logger.warn "waiting #{SHUTDOWN_WAIT} seconds before sending "\
- "#{SHUTDOWN_SIGNAL} to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"
- sleep(SHUTDOWN_WAIT)
+ # Wait `SHUTDOWN_WAIT` to give already fetched jobs time to finish.
+ # Then, tell Sidekiq to gracefully shut down by giving jobs a few more
+ # moments to finish, killing and requeuing them if they didn't, and
+ # then terminating itself.
+ wait_and_signal(SHUTDOWN_WAIT, 'SIGTERM', 'gracefully shut down')
- Sidekiq.logger.warn "sending #{SHUTDOWN_SIGNAL} to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"
- Process.kill(SHUTDOWN_SIGNAL, Process.pid)
+ # Wait for Sidekiq to shutdown gracefully, and kill it if it didn't.
+ wait_and_signal(Sidekiq.options[:timeout] + 2, 'SIGKILL', 'die')
end
end
private
def get_rss
- output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{Process.pid}))
+ output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{pid}))
return 0 unless status.zero?
output.to_i
end
+
+ def wait_and_signal(time, signal, explanation)
+ Sidekiq.logger.warn "waiting #{time} seconds before sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
+ sleep(time)
+
+ Sidekiq.logger.warn "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
+ Process.kill(signal, pid)
+ end
+
+ def pid
+ Process.pid
+ end
end
end
end
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
index a0a2769cf9e..a1f689d94d9 100644
--- a/lib/gitlab/sidekiq_status.rb
+++ b/lib/gitlab/sidekiq_status.rb
@@ -51,6 +51,13 @@ module Gitlab
self.num_running(job_ids).zero?
end
+ # Returns true if the given job is running
+ #
+ # job_id - The Sidekiq job ID to check.
+ def self.running?(job_id)
+ num_running([job_id]) > 0
+ end
+
# Returns the number of jobs that are running.
#
# job_ids - The Sidekiq job IDs to check.
diff --git a/lib/gitlab/sql/pattern.rb b/lib/gitlab/sql/pattern.rb
index 7c2d1d8f887..5f0c98cb5a4 100644
--- a/lib/gitlab/sql/pattern.rb
+++ b/lib/gitlab/sql/pattern.rb
@@ -4,9 +4,15 @@ module Gitlab
extend ActiveSupport::Concern
MIN_CHARS_FOR_PARTIAL_MATCHING = 3
- REGEX_QUOTED_WORD = /(?<=^| )"[^"]+"(?= |$)/
+ REGEX_QUOTED_WORD = /(?<=\A| )"[^"]+"(?= |\z)/
class_methods do
+ def fuzzy_search(query, columns)
+ matches = columns.map { |col| fuzzy_arel_match(col, query) }.compact.reduce(:or)
+
+ where(matches)
+ end
+
def to_pattern(query)
if partial_matching?(query)
"%#{sanitize_sql_like(query)}%"
@@ -19,12 +25,19 @@ module Gitlab
query.length >= MIN_CHARS_FOR_PARTIAL_MATCHING
end
- def to_fuzzy_arel(column, query)
- words = select_fuzzy_words(query)
+ def fuzzy_arel_match(column, query)
+ query = query.squish
+ return nil unless query.present?
- matches = words.map { |word| arel_table[column].matches(to_pattern(word)) }
+ words = select_fuzzy_words(query)
- matches.reduce { |result, match| result.and(match) }
+ if words.any?
+ words.map { |word| arel_table[column].matches(to_pattern(word)) }.reduce(:and)
+ else
+ # No words of at least 3 chars, but we can search for an exact
+ # case insensitive match with the query as a whole
+ arel_table[column].matches(sanitize_sql_like(query))
+ end
end
def select_fuzzy_words(query)
@@ -32,7 +45,7 @@ module Gitlab
query = quoted_words.reduce(query) { |q, quoted_word| q.sub(quoted_word, '') }
- words = query.split(/\s+/)
+ words = query.split
quoted_words.map! { |quoted_word| quoted_word[1..-2] }
diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb
index f30c771837a..c99b262f1ca 100644
--- a/lib/gitlab/sql/union.rb
+++ b/lib/gitlab/sql/union.rb
@@ -26,7 +26,11 @@ module Gitlab
@relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?)
end
- fragments.join("\n#{union_keyword}\n")
+ if fragments.any?
+ fragments.join("\n#{union_keyword}\n")
+ else
+ 'NULL'
+ end
end
def union_keyword
diff --git a/lib/gitlab/string_range_marker.rb b/lib/gitlab/string_range_marker.rb
index 11aeec1ebfa..f9faa134206 100644
--- a/lib/gitlab/string_range_marker.rb
+++ b/lib/gitlab/string_range_marker.rb
@@ -90,6 +90,7 @@ module Gitlab
# Takes an array of integers, and returns an array of ranges covering the same integers
def collapse_ranges(positions)
return [] if positions.empty?
+
ranges = []
start = prev = positions[0]
diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb
index cb7957e2af9..33f07fa0120 100644
--- a/lib/gitlab/template/finders/repo_template_finder.rb
+++ b/lib/gitlab/template/finders/repo_template_finder.rb
@@ -18,6 +18,7 @@ module Gitlab
def read(path)
blob = @repository.blob_at(@commit.id, path) if @commit
raise FileNotFoundError if blob.nil?
+
blob.data
end
diff --git a/lib/gitlab/testing/request_blocker_middleware.rb b/lib/gitlab/testing/request_blocker_middleware.rb
index aa67fa08577..4a8e3c2eee0 100644
--- a/lib/gitlab/testing/request_blocker_middleware.rb
+++ b/lib/gitlab/testing/request_blocker_middleware.rb
@@ -7,6 +7,7 @@ module Gitlab
class RequestBlockerMiddleware
@@num_active_requests = Concurrent::AtomicFixnum.new(0)
@@block_requests = Concurrent::AtomicBoolean.new(false)
+ @@slow_requests = Concurrent::AtomicBoolean.new(false)
# Returns the number of requests the server is currently processing.
def self.num_active_requests
@@ -19,9 +20,15 @@ module Gitlab
@@block_requests.value = true
end
+ # Slows down incoming requests (useful for race conditions).
+ def self.slow_requests!
+ @@slow_requests.value = true
+ end
+
# Allows the server to accept requests again.
def self.allow_requests!
@@block_requests.value = false
+ @@slow_requests.value = false
end
def initialize(app)
@@ -33,6 +40,7 @@ module Gitlab
if block_requests?
block_request(env)
else
+ sleep 0.2 if slow_requests?
@app.call(env)
end
ensure
@@ -45,6 +53,10 @@ module Gitlab
@@block_requests.true?
end
+ def slow_requests?
+ @@slow_requests.true?
+ end
+
def block_request(env)
[503, {}, []]
end
diff --git a/lib/gitlab/testing/request_inspector_middleware.rb b/lib/gitlab/testing/request_inspector_middleware.rb
new file mode 100644
index 00000000000..e387667480d
--- /dev/null
+++ b/lib/gitlab/testing/request_inspector_middleware.rb
@@ -0,0 +1,71 @@
+# rubocop:disable Style/ClassVars
+
+module Gitlab
+ module Testing
+ class RequestInspectorMiddleware
+ @@log_requests = Concurrent::AtomicBoolean.new(false)
+ @@logged_requests = Concurrent::Array.new
+ @@inject_headers = Concurrent::Hash.new
+
+ # Resets the current request log and starts logging requests
+ def self.log_requests!(headers = {})
+ @@inject_headers.replace(headers)
+ @@logged_requests.replace([])
+ @@log_requests.value = true
+ end
+
+ # Stops logging requests
+ def self.stop_logging!
+ @@log_requests.value = false
+ end
+
+ def self.requests
+ @@logged_requests
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ return @app.call(env) unless @@log_requests.true?
+
+ url = env['REQUEST_URI']
+ env.merge! http_headers_env(@@inject_headers) if @@inject_headers.any?
+ request_headers = env_http_headers(env)
+ status, headers, body = @app.call(env)
+
+ request = OpenStruct.new(
+ url: url,
+ status_code: status,
+ request_headers: request_headers,
+ response_headers: headers
+ )
+ log_request request
+
+ [status, headers, body]
+ end
+
+ private
+
+ def env_http_headers(env)
+ Hash[*env.select { |k, v| k.start_with? 'HTTP_' }
+ .collect { |k, v| [k.sub(/^HTTP_/, ''), v] }
+ .collect { |k, v| [k.split('_').collect(&:capitalize).join('-'), v] }
+ .sort
+ .flatten]
+ end
+
+ def http_headers_env(headers)
+ Hash[*headers
+ .collect { |k, v| [k.split('-').collect(&:upcase).join('_'), v] }
+ .collect { |k, v| [k.prepend('HTTP_'), v] }
+ .flatten]
+ end
+
+ def log_request(response)
+ @@logged_requests.push(response)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index fee1a127fd7..13150ddab67 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -22,10 +22,12 @@ module Gitlab
return true if blocked_user_or_hostname?(uri.user)
return true if blocked_user_or_hostname?(uri.hostname)
- server_ips = Resolv.getaddresses(uri.hostname)
+ server_ips = Addrinfo.getaddrinfo(uri.hostname, 80, nil, :STREAM).map(&:ip_address)
return true if (blocked_ips & server_ips).any?
rescue Addressable::URI::InvalidURIError
return true
+ rescue SocketError
+ return false
end
false
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 1caa791c1be..59331c827af 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -70,6 +70,7 @@ module Gitlab
def generate_full_url
return @url unless valid_credentials?
+
@full_url = @url.dup
@full_url.password = credentials[:password] if credentials[:password].present?
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 3f3ba77d47f..2adcc9809b3 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -48,7 +48,9 @@ module Gitlab
deploy_keys: DeployKey.count,
deployments: Deployment.count,
environments: ::Environment.count,
- gcp_clusters: ::Gcp::Cluster.count,
+ clusters: ::Clusters::Cluster.count,
+ clusters_enabled: ::Clusters::Cluster.enabled.count,
+ clusters_disabled: ::Clusters::Cluster.disabled.count,
in_review_folder: ::Environment.in_review_folder.count,
groups: Group.count,
issues: Issue.count,
@@ -77,7 +79,7 @@ module Gitlab
def features_usage_data_ce
{
- signup: current_application_settings.signup_enabled?,
+ signup: current_application_settings.allow_signup?,
ldap: Gitlab.config.ldap.enabled,
gravatar: current_application_settings.gravatar_enabled?,
omniauth: Gitlab.config.omniauth.enabled,
diff --git a/lib/gitlab/utils/merge_hash.rb b/lib/gitlab/utils/merge_hash.rb
new file mode 100644
index 00000000000..385141d44d0
--- /dev/null
+++ b/lib/gitlab/utils/merge_hash.rb
@@ -0,0 +1,117 @@
+module Gitlab
+ module Utils
+ module MergeHash
+ extend self
+ # Deep merges an array of hashes
+ #
+ # [{ hello: ["world"] },
+ # { hello: "Everyone" },
+ # { hello: { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzien dobry'] } },
+ # "Goodbye", "Hallo"]
+ # => [
+ # {
+ # hello:
+ # [
+ # "world",
+ # "Everyone",
+ # { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzien dobry'] }
+ # ]
+ # },
+ # "Goodbye"
+ # ]
+ def merge(elements)
+ merged, *other_elements = elements
+
+ other_elements.each do |element|
+ merged = merge_hash_tree(merged, element)
+ end
+
+ merged
+ end
+
+ # This extracts all keys and values from a hash into an array
+ #
+ # { hello: "world", this: { crushes: ["an entire", "hash"] } }
+ # => [:hello, "world", :this, :crushes, "an entire", "hash"]
+ def crush(array_or_hash)
+ if array_or_hash.is_a?(Array)
+ crush_array(array_or_hash)
+ else
+ crush_hash(array_or_hash)
+ end
+ end
+
+ private
+
+ def merge_hash_into_array(array, new_hash)
+ crushed_new_hash = crush_hash(new_hash)
+ # Merge the hash into an existing element of the array if there is overlap
+ if mergeable_index = array.index { |element| crushable?(element) && (crush(element) & crushed_new_hash).any? }
+ array[mergeable_index] = merge_hash_tree(array[mergeable_index], new_hash)
+ else
+ array << new_hash
+ end
+
+ array
+ end
+
+ def merge_hash_tree(first_element, second_element)
+ # If one of the elements is an object, and the other is a Hash or Array
+ # we can check if the object is already included. If so, we don't need to do anything
+ #
+ # Handled cases
+ # [Hash, Object], [Array, Object]
+ if crushable?(first_element) && crush(first_element).include?(second_element)
+ first_element
+ elsif crushable?(second_element) && crush(second_element).include?(first_element)
+ second_element
+ # When the first is an array, we need to go over every element to see if
+ # we can merge deeper. If no match is found, we add the element to the array
+ #
+ # Handled cases:
+ # [Array, Hash]
+ elsif first_element.is_a?(Array) && second_element.is_a?(Hash)
+ merge_hash_into_array(first_element, second_element)
+ elsif first_element.is_a?(Hash) && second_element.is_a?(Array)
+ merge_hash_into_array(second_element, first_element)
+ # If both of them are hashes, we can deep_merge with the same logic
+ #
+ # Handled cases:
+ # [Hash, Hash]
+ elsif first_element.is_a?(Hash) && second_element.is_a?(Hash)
+ first_element.deep_merge(second_element) { |key, first, second| merge_hash_tree(first, second) }
+ # If both elements are arrays, we try to merge each element separatly
+ #
+ # Handled cases
+ # [Array, Array]
+ elsif first_element.is_a?(Array) && second_element.is_a?(Array)
+ first_element.map { |child_element| merge_hash_tree(child_element, second_element) }
+ # If one or both elements are a GroupDescendant, we wrap create an array
+ # combining them.
+ #
+ # Handled cases:
+ # [Object, Object], [Array, Array]
+ else
+ (Array.wrap(first_element) + Array.wrap(second_element)).uniq
+ end
+ end
+
+ def crushable?(element)
+ element.is_a?(Hash) || element.is_a?(Array)
+ end
+
+ def crush_hash(hash)
+ hash.flat_map do |key, value|
+ crushed_value = crushable?(value) ? crush(value) : value
+ Array.wrap(key) + Array.wrap(crushed_value)
+ end
+ end
+
+ def crush_array(array)
+ array.flat_map do |element|
+ crushable?(element) ? crush(element) : element
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/utils/strong_memoize.rb b/lib/gitlab/utils/strong_memoize.rb
new file mode 100644
index 00000000000..a2ac9285b56
--- /dev/null
+++ b/lib/gitlab/utils/strong_memoize.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module Utils
+ module StrongMemoize
+ # Instead of writing patterns like this:
+ #
+ # def trigger_from_token
+ # return @trigger if defined?(@trigger)
+ #
+ # @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ # end
+ #
+ # We could write it like:
+ #
+ # def trigger_from_token
+ # strong_memoize(:trigger) do
+ # Ci::Trigger.find_by_token(params[:token].to_s)
+ # end
+ # end
+ #
+ def strong_memoize(name)
+ ivar_name = "@#{name}"
+
+ if instance_variable_defined?(ivar_name)
+ instance_variable_get(ivar_name)
+ else
+ instance_variable_set(ivar_name, yield)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index c60bd91ea6e..11472ce6cce 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -99,6 +99,7 @@ module Gitlab
def level_value(level)
return level.to_i if level.to_i.to_s == level.to_s && string_options.key(level.to_i)
+
string_options[level] || PRIVATE
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index f200c694562..864a9e04888 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -16,14 +16,15 @@ module Gitlab
SECRET_LENGTH = 32
class << self
- def git_http_ok(repository, is_wiki, user, action)
+ def git_http_ok(repository, is_wiki, user, action, show_all_refs: false)
project = repository.project
repo_path = repository.path_to_repo
params = {
GL_ID: Gitlab::GlId.gl_id(user),
GL_REPOSITORY: Gitlab::GlRepository.gl_repository(project, is_wiki),
GL_USERNAME: user&.username,
- RepoPath: repo_path
+ RepoPath: repo_path,
+ ShowAllRefs: show_all_refs
}
server = {
address: Gitlab::GitalyClient.address(project.repository_storage),
@@ -103,11 +104,16 @@ module Gitlab
end
def send_git_diff(repository, diff_refs)
- params = {
- 'RepoPath' => repository.path_to_repo,
- 'ShaFrom' => diff_refs.base_sha,
- 'ShaTo' => diff_refs.head_sha
- }
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff)
+ {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'RawDiffRequest' => Gitaly::RawDiffRequest.new(
+ gitaly_diff_or_patch_hash(repository, diff_refs)
+ ).to_json
+ }
+ else
+ workhorse_diff_or_patch_hash(repository, diff_refs)
+ end
[
SEND_DATA_HEADER,
@@ -116,11 +122,16 @@ module Gitlab
end
def send_git_patch(repository, diff_refs)
- params = {
- 'RepoPath' => repository.path_to_repo,
- 'ShaFrom' => diff_refs.base_sha,
- 'ShaTo' => diff_refs.head_sha
- }
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch)
+ {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'RawPatchRequest' => Gitaly::RawPatchRequest.new(
+ gitaly_diff_or_patch_hash(repository, diff_refs)
+ ).to_json
+ }
+ else
+ workhorse_diff_or_patch_hash(repository, diff_refs)
+ end
[
SEND_DATA_HEADER,
@@ -163,6 +174,7 @@ module Gitlab
@secret ||= begin
bytes = Base64.strict_decode64(File.read(secret_path).chomp)
raise "#{secret_path} does not contain #{SECRET_LENGTH} bytes" if bytes.length != SECRET_LENGTH
+
bytes
end
end
@@ -216,6 +228,22 @@ module Gitlab
token: Gitlab::GitalyClient.token(repository.project.repository_storage)
}
end
+
+ def workhorse_diff_or_patch_hash(repository, diff_refs)
+ {
+ 'RepoPath' => repository.path_to_repo,
+ 'ShaFrom' => diff_refs.base_sha,
+ 'ShaTo' => diff_refs.head_sha
+ }
+ end
+
+ def gitaly_diff_or_patch_hash(repository, diff_refs)
+ {
+ repository: repository.gitaly_repository,
+ left_commit_id: diff_refs.base_sha,
+ right_commit_id: diff_refs.head_sha
+ }
+ end
end
end
end
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index a440a3e3562..9242cbe840c 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -3,7 +3,6 @@ require 'google/apis/container_v1'
module GoogleApi
module CloudPlatform
class Client < GoogleApi::Auth
- DEFAULT_MACHINE_TYPE = 'n1-standard-1'.freeze
SCOPE = 'https://www.googleapis.com/auth/cloud-platform'.freeze
LEAST_TOKEN_LIFE_TIME = 10.minutes
diff --git a/lib/haml_lint/inline_javascript.rb b/lib/haml_lint/inline_javascript.rb
index 05668c69006..f5485eb89fa 100644
--- a/lib/haml_lint/inline_javascript.rb
+++ b/lib/haml_lint/inline_javascript.rb
@@ -9,6 +9,7 @@ unless Rails.env.production?
def visit_filter(node)
return unless node.filter_type == 'javascript'
+
record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)')
end
end
diff --git a/lib/milestone_array.rb b/lib/milestone_array.rb
new file mode 100644
index 00000000000..4ed8485b36a
--- /dev/null
+++ b/lib/milestone_array.rb
@@ -0,0 +1,40 @@
+module MilestoneArray
+ class << self
+ def sort(array, sort_method)
+ case sort_method
+ when 'due_date_asc'
+ sort_asc_nulls_last(array, 'due_date')
+ when 'due_date_desc'
+ sort_desc_nulls_last(array, 'due_date')
+ when 'start_date_asc'
+ sort_asc_nulls_last(array, 'start_date')
+ when 'start_date_desc'
+ sort_desc_nulls_last(array, 'start_date')
+ when 'name_asc'
+ sort_asc(array, 'title')
+ when 'name_desc'
+ sort_asc(array, 'title').reverse
+ else
+ array
+ end
+ end
+
+ private
+
+ def sort_asc_nulls_last(array, attribute)
+ attribute = attribute.to_sym
+
+ array.select(&attribute).sort_by(&attribute) + array.reject(&attribute)
+ end
+
+ def sort_desc_nulls_last(array, attribute)
+ attribute = attribute.to_sym
+
+ array.select(&attribute).sort_by(&attribute).reverse + array.reject(&attribute)
+ end
+
+ def sort_asc(array, attribute)
+ array.sort_by(&attribute.to_sym)
+ end
+ end
+end
diff --git a/lib/omni_auth/strategies/bitbucket.rb b/lib/omni_auth/strategies/bitbucket.rb
index 5a7d67c2390..ce1bdfe6ee4 100644
--- a/lib/omni_auth/strategies/bitbucket.rb
+++ b/lib/omni_auth/strategies/bitbucket.rb
@@ -36,6 +36,10 @@ module OmniAuth
email_response = access_token.get('api/2.0/user/emails').parsed
@emails ||= email_response && email_response['values'] || nil
end
+
+ def callback_url
+ options[:redirect_uri] || (full_host + script_name + callback_path)
+ end
end
end
end
diff --git a/lib/peek/views/gitaly.rb b/lib/peek/views/gitaly.rb
new file mode 100644
index 00000000000..d519d8e86fa
--- /dev/null
+++ b/lib/peek/views/gitaly.rb
@@ -0,0 +1,34 @@
+module Peek
+ module Views
+ class Gitaly < View
+ def duration
+ ::Gitlab::GitalyClient.query_time
+ end
+
+ def calls
+ ::Gitlab::GitalyClient.get_request_count
+ end
+
+ def results
+ { duration: formatted_duration, calls: calls }
+ end
+
+ private
+
+ def formatted_duration
+ ms = duration * 1000
+ if ms >= 1000
+ "%.2fms" % ms
+ else
+ "%.0fms" % ms
+ end
+ end
+
+ def setup_subscribers
+ subscribe 'start_processing.action_controller' do
+ ::Gitlab::GitalyClient.query_time = 0
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rouge/lexers/math.rb b/lib/rouge/lexers/math.rb
deleted file mode 100644
index 939b23a3421..00000000000
--- a/lib/rouge/lexers/math.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Rouge
- module Lexers
- class Math < PlainText
- title "A passthrough lexer used for LaTeX input"
- desc "PLEASE REFACTOR - this should be handled by SyntaxHighlightFilter"
- tag 'math'
- end
- end
-end
diff --git a/lib/rouge/lexers/plantuml.rb b/lib/rouge/lexers/plantuml.rb
deleted file mode 100644
index 63c461764fc..00000000000
--- a/lib/rouge/lexers/plantuml.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Rouge
- module Lexers
- class Plantuml < PlainText
- title "A passthrough lexer used for PlantUML input"
- desc "PLEASE REFACTOR - this should be handled by SyntaxHighlightFilter"
- tag 'plantuml'
- end
- end
-end
diff --git a/lib/system_check/app/git_user_default_ssh_config_check.rb b/lib/system_check/app/git_user_default_ssh_config_check.rb
index 9af21078403..ad41760dff2 100644
--- a/lib/system_check/app/git_user_default_ssh_config_check.rb
+++ b/lib/system_check/app/git_user_default_ssh_config_check.rb
@@ -11,10 +11,10 @@ module SystemCheck
].freeze
set_name 'Git user has default SSH configuration?'
- set_skip_reason 'skipped (GitLab read-only, or git user is not present / configured)'
+ set_skip_reason 'skipped (git user is not present / configured)'
def skip?
- Gitlab::Database.read_only? || !home_dir || !File.directory?(home_dir)
+ !home_dir || !File.directory?(home_dir)
end
def check?
diff --git a/lib/system_check/app/ruby_version_check.rb b/lib/system_check/app/ruby_version_check.rb
index 08a2c495bd4..57bbabece1f 100644
--- a/lib/system_check/app/ruby_version_check.rb
+++ b/lib/system_check/app/ruby_version_check.rb
@@ -5,7 +5,7 @@ module SystemCheck
set_check_pass -> { "yes (#{self.current_version})" }
def self.required_version
- @required_version ||= Gitlab::VersionInfo.new(2, 3, 3)
+ @required_version ||= Gitlab::VersionInfo.new(2, 3, 5)
end
def self.current_version
diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb
index 00221f77cf4..8b145fb4511 100644
--- a/lib/system_check/simple_executor.rb
+++ b/lib/system_check/simple_executor.rb
@@ -24,6 +24,7 @@ module SystemCheck
# @param [BaseCheck] check class
def <<(check)
raise ArgumentError unless check.is_a?(Class) && check < BaseCheck
+
@checks << check
end
diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake
index 99b3168d9eb..2301ec9b228 100644
--- a/lib/tasks/brakeman.rake
+++ b/lib/tasks/brakeman.rake
@@ -2,7 +2,7 @@ desc 'Security check via brakeman'
task :brakeman do
# We get 0 warnings at level 'w3' but we would like to reach 'w2'. Merge
# requests are welcome!
- if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb,app/controllers/unicorn_test_controller.rb -w3 -z))
+ if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z))
puts 'Security check succeed'
else
puts 'Security check failed'
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index 87ca39b079b..c2d3a6b6950 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -1,5 +1,28 @@
namespace :gemojione do
desc 'Generates Emoji SHA256 digests'
+
+ task aliases: ['yarn:check', 'environment'] do
+ require 'json'
+
+ aliases = {}
+
+ index_file = File.join(Rails.root, 'fixtures', 'emojis', 'index.json')
+ index = JSON.parse(File.read(index_file))
+
+ index.each_pair do |key, data|
+ data['aliases'].each do |a|
+ a.tr!(':', '')
+
+ aliases[a] = key
+ end
+ end
+
+ out = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
+ File.open(out, 'w') do |handle|
+ handle.write(JSON.pretty_generate(aliases, indent: ' ', space: '', space_before: ''))
+ end
+ end
+
task digests: ['yarn:check', 'environment'] do
require 'digest/sha2'
require 'json'
@@ -16,8 +39,13 @@ namespace :gemojione do
fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
hash_digest = Digest::SHA256.file(fpath).hexdigest
+ category = emoji_hash['category']
+ if name == 'gay_pride_flag'
+ category = 'flags'
+ end
+
entry = {
- category: emoji_hash['category'],
+ category: category,
moji: emoji_hash['moji'],
description: emoji_hash['description'],
unicodeVersion: Gitlab::Emoji.emoji_unicode_version(name),
@@ -29,7 +57,6 @@ namespace :gemojione do
end
out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
-
File.open(out, 'w') do |handle|
handle.write(JSON.pretty_generate(resultant_emoji_map))
end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index 1650263b98d..9dcf44fdc3e 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -33,24 +33,29 @@ namespace :gitlab do
backup.unpack
unless backup.skipped?('db')
- unless ENV['force'] == 'yes'
- warning = <<-MSG.strip_heredoc
- Before restoring the database we recommend removing all existing
- tables to avoid future upgrade problems. Be aware that if you have
- custom tables in the GitLab database these tables and all data will be
- removed.
- MSG
- puts warning.color(:red)
- ask_to_continue
- puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow)
- sleep(5)
+ begin
+ unless ENV['force'] == 'yes'
+ warning = <<-MSG.strip_heredoc
+ Before restoring the database, we will remove all existing
+ tables to avoid future upgrade problems. Be aware that if you have
+ custom tables in the GitLab database these tables and all data will be
+ removed.
+ MSG
+ puts warning.color(:red)
+ ask_to_continue
+ puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow)
+ sleep(5)
+ end
+ # Drop all tables Load the schema to ensure we don't have any newer tables
+ # hanging out from a failed upgrade
+ $progress.puts 'Cleaning the database ... '.color(:blue)
+ Rake::Task['gitlab:db:drop_tables'].invoke
+ $progress.puts 'done'.color(:green)
+ Rake::Task['gitlab:backup:db:restore'].invoke
+ rescue Gitlab::TaskAbortedByUserError
+ puts "Quitting...".color(:red)
+ exit 1
end
- # Drop all tables Load the schema to ensure we don't have any newer tables
- # hanging out from a failed upgrade
- $progress.puts 'Cleaning the database ... '.color(:blue)
- Rake::Task['gitlab:db:drop_tables'].invoke
- $progress.puts 'done'.color(:green)
- Rake::Task['gitlab:backup:db:restore'].invoke
end
Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories')
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 8ae1b6a626a..eb0f757aea7 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -1,11 +1,14 @@
namespace :gitlab do
namespace :cleanup do
+ HASHED_REPOSITORY_NAME = '@hashed'.freeze
+
desc "GitLab | Cleanup | Clean namespaces"
task dirs: :environment do
warn_user_is_not_gitlab
remove_flag = ENV['REMOVE']
- namespaces = Namespace.pluck(:path)
+ namespaces = Namespace.pluck(:path)
+ namespaces << HASHED_REPOSITORY_NAME # add so that it will be ignored
Gitlab.config.repositories.storages.each do |name, repository_storage|
git_base_path = repository_storage['path']
all_dirs = Dir.glob(git_base_path + '/*')
@@ -59,7 +62,11 @@ namespace :gitlab do
.sub(%r{^/*}, '')
.chomp('.git')
.chomp('.wiki')
- next if Project.find_by_full_path(repo_with_namespace)
+
+ # TODO ignoring hashed repositories for now. But revisit to fully support
+ # possible orphaned hashed repos
+ next if repo_with_namespace.start_with?("#{HASHED_REPOSITORY_NAME}/") || Project.find_by_full_path(repo_with_namespace)
+
new_path = path + move_suffix
puts path.inspect + ' -> ' + new_path.inspect
File.rename(path, new_path)
@@ -75,6 +82,7 @@ namespace :gitlab do
User.find_each do |user|
next unless user.ldap_user?
+
print "#{user.name} (#{user.ldap_identity.extern_uid}) ..."
if Gitlab::LDAP::Access.allowed?(user)
puts " [OK]".color(:green)
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
index b4d05f5995a..ba221e44e5d 100644
--- a/lib/tasks/gitlab/dev.rake
+++ b/lib/tasks/gitlab/dev.rake
@@ -5,8 +5,9 @@ namespace :gitlab do
opts =
if ENV['CI']
{
- ce_repo: ENV['CI_REPOSITORY_URL'],
- branch: ENV['CI_COMMIT_REF_NAME']
+ ce_project_url: ENV['CI_PROJECT_URL'],
+ branch: ENV['CI_COMMIT_REF_NAME'],
+ job_id: ENV['CI_JOB_ID']
}
else
unless args[:branch]
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index 8377fe3269d..4d880c05f99 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -14,18 +14,18 @@ namespace :gitlab do
checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir)
+ command = %w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE]
+
_, status = Gitlab::Popen.popen(%w[which gmake])
- command = status.zero? ? ['gmake'] : ['make']
+ command << (status.zero? ? 'gmake' : 'make')
- if Rails.env.test?
- command += %W[BUNDLE_PATH=#{Bundler.bundle_path}]
- end
+ command << 'BUNDLE_FLAGS=--no-deployment' if Rails.env.test?
Dir.chdir(args.dir) do
create_gitaly_configuration
# In CI we run scripts/gitaly-test-build instead of this command
unless ENV['CI'].present?
- Bundler.with_original_env { run_command!(%w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE] + command) }
+ Bundler.with_original_env { run_command!(command) }
end
end
end
@@ -78,13 +78,18 @@ namespace :gitlab do
config[:auth] = { token: 'secret' } if Rails.env.test?
config[:'gitaly-ruby'] = { dir: File.join(Dir.pwd, 'ruby') } if gitaly_ruby
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
+ config[:bin_dir] = Gitlab.config.gitaly.client_path
+
TOML.dump(config)
end
def create_gitaly_configuration
- File.open("config.toml", "w") do |f|
+ File.open("config.toml", File::WRONLY | File::CREAT | File::EXCL) do |f|
f.puts gitaly_configuration_toml
end
+ rescue Errno::EEXIST
+ puts "Skipping config.toml generation:"
+ puts "A configuration file already exists."
rescue ArgumentError => e
puts "Skipping config.toml generation:"
puts e.message
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index d227a0c8bdb..adfcc3cda22 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -2,23 +2,21 @@ namespace :gitlab do
namespace :import do
# How to use:
#
- # 1. copy the bare repos under the repository storage paths (commonly the default path is /home/git/repositories)
- # 2. run: bundle exec rake gitlab:import:repos RAILS_ENV=production
+ # 1. copy the bare repos to a specific path that contain the group or subgroups structure as folders
+ # 2. run: bundle exec rake gitlab:import:repos[/path/to/repos] RAILS_ENV=production
#
# Notes:
# * The project owner will set to the first administator of the system
# * Existing projects will be skipped
- #
- #
desc "GitLab | Import bare repositories from repositories -> storages into GitLab project instance"
- task repos: :environment do
- if Project.current_application_settings.hashed_storage_enabled
- puts 'Cannot import repositories when Hashed Storage is enabled'.color(:red)
+ task :repos, [:import_path] => :environment do |_t, args|
+ unless args.import_path
+ puts 'Please specify an import path that contains the repositories'.color(:red)
exit 1
end
- Gitlab::BareRepositoryImporter.execute
+ Gitlab::BareRepositoryImport::Importer.execute(args.import_path)
end
end
end
diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake
deleted file mode 100644
index 6cbc83b8973..00000000000
--- a/lib/tasks/gitlab/sidekiq.rake
+++ /dev/null
@@ -1,47 +0,0 @@
-namespace :gitlab do
- namespace :sidekiq do
- QUEUE = 'queue:post_receive'.freeze
-
- desc 'Drop all Sidekiq PostReceive jobs for a given project'
- task :drop_post_receive, [:project] => :environment do |t, args|
- unless args.project.present?
- abort "Please specify the project you want to drop PostReceive jobs for:\n rake gitlab:sidekiq:drop_post_receive[group/project]"
- end
- project_path = Project.find_by_full_path(args.project).repository.path_to_repo
-
- Sidekiq.redis do |redis|
- unless redis.exists(QUEUE)
- abort "Queue #{QUEUE} is empty"
- end
-
- temp_queue = "#{QUEUE}_#{Time.now.to_i}"
- redis.rename(QUEUE, temp_queue)
-
- # At this point, then post_receive queue is empty. It may be receiving
- # new jobs already. We will repopulate it with the old jobs, skipping the
- # ones we want to drop.
- dropped = 0
- while (job = redis.lpop(temp_queue))
- if repo_path(job) == project_path
- dropped += 1
- else
- redis.rpush(QUEUE, job)
- end
- end
- # The temp_queue will delete itself after we have popped all elements
- # from it
-
- puts "Dropped #{dropped} jobs containing #{project_path} from #{QUEUE}"
- end
- end
-
- def repo_path(job)
- job_args = JSON.parse(job)['args']
- if job_args
- job_args.first
- else
- nil
- end
- end
- end
-end
diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake
index e05be4a3405..8ac73bc8ff2 100644
--- a/lib/tasks/gitlab/storage.rake
+++ b/lib/tasks/gitlab/storage.rake
@@ -2,10 +2,10 @@ namespace :gitlab do
namespace :storage do
desc 'GitLab | Storage | Migrate existing projects to Hashed Storage'
task migrate_to_hashed: :environment do
- legacy_projects_count = Project.with_legacy_storage.count
+ legacy_projects_count = Project.with_unmigrated_storage.count
if legacy_projects_count == 0
- puts 'There are no projects using legacy storage. Nothing to do!'
+ puts 'There are no projects requiring storage migration. Nothing to do!'
next
end
@@ -23,22 +23,42 @@ namespace :gitlab do
desc 'Gitlab | Storage | Summary of existing projects using Legacy Storage'
task legacy_projects: :environment do
- projects_summary(Project.with_legacy_storage)
+ relation_summary('projects', Project.without_storage_feature(:repository))
end
desc 'Gitlab | Storage | List existing projects using Legacy Storage'
task list_legacy_projects: :environment do
- projects_list(Project.with_legacy_storage)
+ projects_list('projects using Legacy Storage', Project.without_storage_feature(:repository))
end
desc 'Gitlab | Storage | Summary of existing projects using Hashed Storage'
task hashed_projects: :environment do
- projects_summary(Project.with_hashed_storage)
+ relation_summary('projects using Hashed Storage', Project.with_storage_feature(:repository))
end
desc 'Gitlab | Storage | List existing projects using Hashed Storage'
task list_hashed_projects: :environment do
- projects_list(Project.with_hashed_storage)
+ projects_list('projects using Hashed Storage', Project.with_storage_feature(:repository))
+ end
+
+ desc 'Gitlab | Storage | Summary of project attachments using Legacy Storage'
+ task legacy_attachments: :environment do
+ relation_summary('attachments using Legacy Storage', legacy_attachments_relation)
+ end
+
+ desc 'Gitlab | Storage | List existing project attachments using Legacy Storage'
+ task list_legacy_attachments: :environment do
+ attachments_list('attachments using Legacy Storage', legacy_attachments_relation)
+ end
+
+ desc 'Gitlab | Storage | Summary of project attachments using Hashed Storage'
+ task hashed_attachments: :environment do
+ relation_summary('attachments using Hashed Storage', hashed_attachments_relation)
+ end
+
+ desc 'Gitlab | Storage | List existing project attachments using Hashed Storage'
+ task list_hashed_attachments: :environment do
+ attachments_list('attachments using Hashed Storage', hashed_attachments_relation)
end
def batch_size
@@ -46,29 +66,43 @@ namespace :gitlab do
end
def project_id_batches(&block)
- Project.with_legacy_storage.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches
+ Project.with_unmigrated_storage.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches
ids = relation.pluck(:id)
yield ids.min, ids.max
end
end
- def projects_summary(relation)
- projects_count = relation.count
- puts "* Found #{projects_count} projects".color(:green)
+ def legacy_attachments_relation
+ Upload.joins(<<~SQL).where('projects.storage_version < :version OR projects.storage_version IS NULL', version: Project::HASHED_STORAGE_FEATURES[:attachments])
+ JOIN projects
+ ON (uploads.model_type='Project' AND uploads.model_id=projects.id)
+ SQL
+ end
+
+ def hashed_attachments_relation
+ Upload.joins(<<~SQL).where('projects.storage_version >= :version', version: Project::HASHED_STORAGE_FEATURES[:attachments])
+ JOIN projects
+ ON (uploads.model_type='Project' AND uploads.model_id=projects.id)
+ SQL
+ end
+
+ def relation_summary(relation_name, relation)
+ relation_count = relation.count
+ puts "* Found #{relation_count} #{relation_name}".color(:green)
- projects_count
+ relation_count
end
- def projects_list(relation)
- projects_count = projects_summary(relation)
+ def projects_list(relation_name, relation)
+ relation_count = relation_summary(relation_name, relation)
projects = relation.with_route
limit = ENV.fetch('LIMIT', 500).to_i
- return unless projects_count > 0
+ return unless relation_count > 0
- puts " ! Displaying first #{limit} projects..." if projects_count > limit
+ puts " ! Displaying first #{limit} #{relation_name}..." if relation_count > limit
counter = 0
projects.find_in_batches(batch_size: batch_size) do |batch|
@@ -81,5 +115,26 @@ namespace :gitlab do
end
end
end
+
+ def attachments_list(relation_name, relation)
+ relation_count = relation_summary(relation_name, relation)
+
+ limit = ENV.fetch('LIMIT', 500).to_i
+
+ return unless relation_count > 0
+
+ puts " ! Displaying first #{limit} #{relation_name}..." if relation_count > limit
+
+ counter = 0
+ relation.find_in_batches(batch_size: batch_size) do |batch|
+ batch.each do |upload|
+ counter += 1
+
+ puts " - #{upload.path} (id: #{upload.id})".color(:red)
+
+ return if counter >= limit # rubocop:disable Lint/NonLocalExitFromIterator
+ end
+ end
+ end
end
end
diff --git a/lib/tasks/gitlab/users.rake b/lib/tasks/gitlab/users.rake
deleted file mode 100644
index 3a16ace60bd..00000000000
--- a/lib/tasks/gitlab/users.rake
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace :gitlab do
- namespace :users do
- desc "GitLab | Clear the authentication token for all users"
- task clear_all_authentication_tokens: :environment do |t, args|
- # Do small batched updates because these updates will be slow and locking
- User.select(:id).find_in_batches(batch_size: 100) do |batch|
- User.where(id: batch.map(&:id)).update_all(authentication_token: nil)
- end
- end
- end
-end
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index 7f86fd7b45e..aafbe52e5f8 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -7,14 +7,16 @@ class GithubImport
end
def initialize(token, gitlab_username, project_path, extras)
- @options = { token: token, verbose: true }
+ @options = { token: token }
@project_path = project_path
@current_user = User.find_by_username(gitlab_username)
@github_repo = extras.empty? ? nil : extras.first
end
def run!
- @repo = GithubRepos.new(@options, @current_user, @github_repo).choose_one!
+ @repo = GithubRepos
+ .new(@options[:token], @current_user, @github_repo)
+ .choose_one!
raise 'No repo found!' unless @repo
@@ -28,7 +30,7 @@ class GithubImport
private
def show_warning!
- puts "This will import GitHub #{@repo['full_name'].bright} into GitLab #{@project_path.bright} as #{@current_user.name}"
+ puts "This will import GitHub #{@repo.full_name.bright} into GitLab #{@project_path.bright} as #{@current_user.name}"
puts "Permission checks are ignored. Press any key to continue.".color(:red)
STDIN.getch
@@ -42,7 +44,9 @@ class GithubImport
import_success = false
timings = Benchmark.measure do
- import_success = Github::Import.new(@project, @options).execute
+ import_success = Gitlab::GithubImport::SequentialImporter
+ .new(@project, token: @options[:token])
+ .execute
end
if import_success
@@ -63,16 +67,16 @@ class GithubImport
@current_user,
name: name,
path: name,
- description: @repo['description'],
+ description: @repo.description,
namespace_id: namespace.id,
visibility_level: visibility_level,
- skip_wiki: @repo['has_wiki']
+ skip_wiki: @repo.has_wiki
).execute
project.update!(
import_type: 'github',
- import_source: @repo['full_name'],
- import_url: @repo['clone_url'].sub('://', "://#{@options[:token]}@")
+ import_source: @repo.full_name,
+ import_url: @repo.clone_url.sub('://', "://#{@options[:token]}@")
)
project
@@ -91,13 +95,15 @@ class GithubImport
end
def visibility_level
- @repo['private'] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::CurrentSettings.current_application_settings.default_project_visibility
+ @repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::CurrentSettings.current_application_settings.default_project_visibility
end
end
class GithubRepos
- def initialize(options, current_user, github_repo)
- @options = options
+ def initialize(token, current_user, github_repo)
+ @client = Gitlab::GithubImport::Client.new(token)
+ @client.octokit.auto_paginate = true
+
@current_user = current_user
@github_repo = github_repo
end
@@ -106,17 +112,17 @@ class GithubRepos
return found_github_repo if @github_repo
repos.each do |repo|
- print "ID: #{repo['id'].to_s.bright}".color(:green)
- print "\tName: #{repo['full_name']}\n".color(:green)
+ print "ID: #{repo.id.to_s.bright}".color(:green)
+ print "\tName: #{repo.full_name}\n".color(:green)
end
print 'ID? '.bright
- repos.find { |repo| repo['id'] == repo_id }
+ repos.find { |repo| repo.id == repo_id }
end
def found_github_repo
- repos.find { |repo| repo['full_name'] == @github_repo }
+ repos.find { |repo| repo.full_name == @github_repo }
end
def repo_id
@@ -124,7 +130,7 @@ class GithubRepos
end
def repos
- Github::Repositories.new(@options).fetch
+ @client.octokit.list_repositories
end
end
diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake
index ad1818ff1fa..693597afdf8 100644
--- a/lib/tasks/tokens.rake
+++ b/lib/tasks/tokens.rake
@@ -1,12 +1,7 @@
require_relative '../../app/models/concerns/token_authenticatable.rb'
namespace :tokens do
- desc "Reset all GitLab user auth tokens"
- task reset_all_auth: :environment do
- reset_all_users_token(:reset_authentication_token!)
- end
-
- desc "Reset all GitLab email tokens"
+ desc "Reset all GitLab incoming email tokens"
task reset_all_email: :environment do
reset_all_users_token(:reset_incoming_email_token!)
end
@@ -31,11 +26,6 @@ class TmpUser < ActiveRecord::Base
self.table_name = 'users'
- def reset_authentication_token!
- write_new_token(:authentication_token)
- save!(validate: false)
- end
-
def reset_incoming_email_token!
write_new_token(:incoming_email_token)
save!(validate: false)
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
index 38d63315fdc..8e688dede89 100644
--- a/locale/bg/gitlab.po
+++ b/locale/bg/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:45-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:29-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Bulgarian\n"
"Language: bg_BG\n"
@@ -21,6 +21,11 @@ msgid_plural "%d commits"
msgstr[0] "%d подаване"
msgstr[1] "%d подаваниÑ"
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] "%s подаване беше пропуÑнато, за да не Ñе натоварва ÑиÑтемата."
@@ -29,6 +34,11 @@ msgstr[1] "%s Ð¿Ð¾Ð´Ð°Ð²Ð°Ð½Ð¸Ñ Ð±Ñха пропуÑнати, за да не Ñ
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} подаде %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -49,6 +59,12 @@ msgstr[1] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 Ñхема"
@@ -57,6 +73,9 @@ msgstr[1] "%d Ñхеми"
msgid "1st contribution!"
msgstr ""
+msgid "2FA enabled"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Ðабор от графики отноÑно непрекъÑнатата интеграциÑ"
@@ -81,12 +100,18 @@ msgstr "Ðктивно"
msgid "Activity"
msgstr "ДейноÑÑ‚"
+msgid "Add"
+msgstr ""
+
msgid "Add Changelog"
msgstr "ДобавÑне на ÑпиÑък Ñ Ð¿Ñ€Ð¾Ð¼ÐµÐ½Ð¸"
msgid "Add Contribution guide"
msgstr "ДобавÑне на ръководÑтво за ÑътрудничеÑтво"
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Add License"
msgstr "ДобавÑне на лиценз"
@@ -96,9 +121,18 @@ msgstr "Добавете SSH ключ в профила Ñи, за да може
msgid "Add new directory"
msgstr "ДобавÑне на нова папка"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr ""
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -114,6 +148,9 @@ msgstr "ÐаиÑтина ли иÑкате да изтриете този пла
msgid "Are you sure you want to discard your changes?"
msgstr ""
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -132,25 +169,34 @@ msgstr "Прикачете файл чрез влачене и пуÑкане и
msgid "Authentication Log"
msgstr ""
-msgid "Auto DevOps (Beta)"
+msgid "Author"
msgstr ""
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto DevOps documentation"
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgid "AutoDevOps|Auto DevOps documentation"
+msgstr ""
+
+msgid "AutoDevOps|Enable in settings"
+msgstr ""
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
msgstr ""
-msgid "AutoDevOps|Learn more in the"
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
+msgstr ""
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
msgid "Billing"
@@ -168,6 +214,9 @@ msgstr ""
msgid "BillingPlans|Customer Support"
msgstr ""
+msgid "BillingPlans|Downgrade"
+msgstr ""
+
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
msgstr ""
@@ -204,12 +253,6 @@ msgstr ""
msgid "BillingPlans|per user"
msgstr ""
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
-
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "Клон"
@@ -218,6 +261,9 @@ 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 has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "ТърÑете в клоновете"
@@ -332,12 +378,18 @@ msgstr ""
msgid "CI configuration"
msgstr "ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ð½Ð° непрекъÑната интеграциÑ"
+msgid "CICD|Jobs"
+msgstr ""
+
msgid "Cancel"
msgstr "Отказ"
msgid "Cancel edit"
msgstr ""
+msgid "Change Weight"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Избиране в клона"
@@ -365,6 +417,9 @@ msgstr "Подбиране на това подаване"
msgid "Cherry-pick this merge request"
msgstr "Подбиране на тази заÑвка за Ñливане"
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr ""
+
msgid "CiStatusLabel|canceled"
msgstr "отказано"
@@ -419,6 +474,147 @@ msgstr "пропуÑнато"
msgid "CiStatus|running"
msgstr "протича в момента"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
+msgid "Clone repository"
+msgstr ""
+
+msgid "Close"
+msgstr ""
+
+msgid "Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Create cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr ""
+
+msgid "ClusterIntegration|Machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr ""
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
+msgstr ""
+
+msgid "ClusterIntegration|See your projects"
+msgstr ""
+
+msgid "ClusterIntegration|See zones"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr ""
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|cluster"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -427,6 +623,14 @@ msgid_plural "Commits"
msgstr[0] "Подаване"
msgstr[1] "ПодаваниÑ"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Commit Message"
+msgstr ""
+
msgid "Commit duration in minutes for last 30 commits"
msgstr "Времетраене на подаваниÑта в минути за поÑледните 30 подаваниÑ"
@@ -454,12 +658,63 @@ msgstr "Подадено от"
msgid "Compare"
msgstr "Сравнение"
+msgid "Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Created"
+msgstr ""
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr ""
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr ""
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Learn more about"
+msgstr ""
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr ""
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr ""
+
+msgid "ContainerRegistry|Remove repository"
+msgstr ""
+
+msgid "ContainerRegistry|Remove tag"
+msgstr ""
+
+msgid "ContainerRegistry|Size"
+msgstr ""
+
+msgid "ContainerRegistry|Tag"
+msgstr ""
+
+msgid "ContainerRegistry|Tag ID"
+msgstr ""
+
+msgid "ContainerRegistry|Use different image names"
+msgstr ""
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr ""
+
msgid "Contribution guide"
msgstr "РъководÑтво за ÑътрудничеÑтво"
msgid "Contributors"
msgstr "Сътрудници"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -472,11 +727,8 @@ 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 "Създайте Ñи личен жетон за доÑтъп в профила Ñи, за да можете да изтеглÑте и изпращате промени чрез %{protocol}."
+msgstr "Създайте Ñи личен жетон за доÑтъп в акаунта Ñи, за да можете да изтеглÑте и изпращате промени чрез %{protocol}."
msgid "Create directory"
msgstr "Създаване на папка"
@@ -484,9 +736,21 @@ msgstr "Създаване на папка"
msgid "Create empty bare repository"
msgstr "Създаване на празно хранилище"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "Създаване на заÑвка за Ñливане"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "Създаване на нов…"
@@ -538,6 +802,12 @@ msgstr "Подготовка за издаване"
msgid "CycleAnalyticsStage|Test"
msgstr "ТеÑтване"
+msgid "DashboardProjects|All"
+msgstr ""
+
+msgid "DashboardProjects|Personal"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "Задайте потребителÑки шаблон, използвайки ÑинтакÑиÑа на „Cron“"
@@ -555,6 +825,9 @@ msgstr ""
msgid "Description"
msgstr "ОпиÑание"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr ""
+
msgid "Details"
msgstr ""
@@ -564,6 +837,12 @@ msgstr "Име на папката"
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Dismiss Merge Request promotion"
+msgstr ""
+
msgid "Don't show again"
msgstr "Да не Ñе показва повече"
@@ -603,9 +882,6 @@ msgstr "Редактиране на плана %{id} за Ñхема"
msgid "Emails"
msgstr ""
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -636,12 +912,18 @@ msgstr "Ð’ÑÑка Ñедмица (в неделÑ, в 4 ч. Ñутринта)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "СобÑтвеникът не може да бъде променен"
msgid "Failed to remove the pipeline schedule"
msgstr "Планът за Ñхема не може да бъде премахнат"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "Файлове"
@@ -668,6 +950,12 @@ msgstr[1] "РазклонениÑ"
msgid "ForkedFromProjectPath|Forked from"
msgstr "Разклонение на"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr ""
+
+msgid "Format"
+msgstr ""
+
msgid "From issue creation until deploy to production"
msgstr "От Ñъздаването на проблема до внедрÑването в крайната верÑиÑ"
@@ -680,6 +968,18 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
+msgid "Geo|Groups to replicate"
+msgstr ""
+
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
+msgid "Geo|Select groups to replicate."
+msgstr ""
+
msgid "Git storage health information has been reset"
msgstr ""
@@ -692,6 +992,9 @@ msgstr "Към Вашето разклонение"
msgid "GoToYourFork|Fork"
msgstr "Разклонение"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -716,6 +1019,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -734,28 +1082,60 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
+msgid "History"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "ОÑвежаването започна уÑпешно"
msgid "Import repository"
msgstr "ВнаÑÑне на хранилище"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Шаблон за интервала"
msgid "Introducing Cycle Analytics"
msgstr "ПредÑтавÑме Ви анализа на циклите"
+msgid "Issue board focus mode"
+msgstr ""
+
+msgid "Issue boards with milestones"
+msgstr ""
+
msgid "Issue events"
msgstr ""
-msgid "Issues"
+msgid "IssueBoards|Board"
msgstr ""
-msgid "Jobs"
+msgid "IssueBoards|Boards"
+msgstr ""
+
+msgid "Issues"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -775,12 +1155,21 @@ msgstr[1] "ПоÑледните %d дни"
msgid "Last Pipeline"
msgstr "ПоÑледна Ñхема"
-msgid "Last Update"
-msgstr "ПоÑледна промÑна"
-
msgid "Last commit"
msgstr "ПоÑледно подаване"
+msgid "Last edited %{date}"
+msgstr ""
+
+msgid "Last edited by %{name}"
+msgstr ""
+
+msgid "Last update"
+msgstr ""
+
+msgid "Last updated"
+msgstr ""
+
msgid "LastPushEvent|You pushed to"
msgstr ""
@@ -793,6 +1182,9 @@ msgstr "Ðаучете повече в"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "документациÑта отноÑно планирането на Ñхеми"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "ÐапуÑкане на групата"
@@ -807,9 +1199,21 @@ msgid_plural "Limited to showing %d events at most"
msgstr[0] "Ограничено до показване на най-много %d Ñъбитие"
msgstr[1] "Ограничено до показване на най-много %d ÑъбитиÑ"
+msgid "Lock"
+msgstr ""
+
+msgid "Locked"
+msgstr ""
+
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "Медиана"
@@ -837,6 +1241,12 @@ msgstr ""
msgid "More information is available|here"
msgstr ""
+msgid "Multiple issue boards"
+msgstr ""
+
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Ðов проблем"
@@ -854,27 +1264,42 @@ msgstr "Ðова папка"
msgid "New file"
msgstr "Ðов файл"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "Ðов проблем"
msgid "New merge request"
msgstr "Ðова заÑвка за Ñливане"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "Ðов план"
msgid "New snippet"
msgstr "Ðов отрÑзък"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "Ðов етикет"
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr ""
+
msgid "No repository"
msgstr "ÐÑма хранилище"
msgid "No schedules"
msgstr "ÐÑма планове"
+msgid "None"
+msgstr ""
+
msgid "Not available"
msgstr "Ðе е налично"
@@ -938,12 +1363,24 @@ msgstr "Ðаблюдение"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Филтър"
+msgid "Only project members can comment."
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Отворен"
+msgid "Opens in a new window"
+msgstr ""
+
msgid "Options"
msgstr "Опции"
@@ -968,6 +1405,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr ""
+
msgid "Pipeline"
msgstr "Схема"
@@ -1070,9 +1510,54 @@ msgstr "Ñ ÐµÑ‚Ð°Ð¿Ð¸"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Проектът „%{project_name}“ е добавен в опашката за изтриване."
@@ -1082,9 +1567,6 @@ msgstr "Проектът „%{project_name}“ беше Ñъздаден уÑпÐ
msgid "Project '%{project_name}' was successfully updated."
msgstr "Проектът „%{project_name}“ беше обновен уÑпешно."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "Проектът „%{project_name}“ ще бъде изтрит."
-
msgid "Project access must be granted explicitly to each user."
msgstr "ДоÑтъпът до проекта Ñ‚Ñ€Ñбва да бъде даван поотделно на вÑеки потребител."
@@ -1127,6 +1609,27 @@ msgstr "Етап"
msgid "ProjectNetworkGraph|Graph"
msgstr "Графика"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr ""
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr ""
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1148,12 +1651,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr ""
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Прочетете повече"
@@ -1217,6 +1729,12 @@ msgstr "ОтмÑна на тази заÑвка за Ñливане"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
+msgid "Save changes"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "Запазване на плана за Ñхема"
@@ -1232,15 +1750,21 @@ msgstr "Планиране на Ñхемите"
msgid "Search branches and tags"
msgstr "ТърÑете в клоновете и етикетите"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "Изберете формата на архива"
msgid "Select a timezone"
msgstr "Изберете чаÑова зона"
-msgid "Select existing branch"
-msgstr ""
-
msgid "Select target branch"
msgstr "Изберете целеви клон"
@@ -1248,7 +1772,7 @@ msgid "Service Templates"
msgstr ""
msgid "Set a password on your account to pull or push via %{protocol}."
-msgstr "Задайте парола на профила Ñи, за да можете да изтеглÑте и изпращате промени чрез %{protocol}."
+msgstr "Задайте парола на акаунта Ñи, за да можете да изтеглÑте и изпращате промени чрез %{protocol}."
msgid "Set up CI"
msgstr "ÐаÑтройка на ÐИ"
@@ -1279,6 +1803,21 @@ msgstr[1] "Показване на %d ÑъбитиÑ"
msgid "Snippets"
msgstr ""
+msgid "Something went wrong on our end."
+msgstr ""
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
+msgid "Something went wrong while fetching the projects."
+msgstr ""
+
+msgid "Something went wrong while fetching the registry list."
+msgstr ""
+
+msgid "Sort by"
+msgstr ""
+
msgid "SortOptions|Access level, ascending"
msgstr ""
@@ -1396,6 +1935,12 @@ msgstr "Създайте %{new_merge_request} Ñ Ñ‚ÐµÐ·Ð¸ промени"
msgid "Start the Runner!"
msgstr ""
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Преминаване към клон/етикет"
@@ -1416,6 +1961,15 @@ msgstr "Целеви клон"
msgid "Team"
msgstr ""
+msgid "Thanks! Don't show me this again"
+msgstr ""
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr ""
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+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 "Етапът на програмиране показва времето от първото подаване до Ñъздаването на заÑвката за Ñливане. Данните ще бъдат добавени тук автоматично Ñлед като бъде Ñъздадена първата заÑвка за Ñливане."
@@ -1428,6 +1982,15 @@ 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 number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "Етапът от цикъла на разработка"
@@ -1458,6 +2021,12 @@ 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 time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "Времето, което отнема вÑеки Ð·Ð°Ð¿Ð¸Ñ Ð¾Ñ‚ данни за ÑÑŠÐ¾Ñ‚Ð²ÐµÑ‚Ð½Ð¸Ñ ÐµÑ‚Ð°Ð¿."
@@ -1467,12 +2036,27 @@ msgstr "СтойноÑтта, коÑто Ñе намира в Ñредата нÐ
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "This is the author's first Merge Request to this project. Handle with care."
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
+msgid "This is a confidential issue."
+msgstr ""
+
+msgid "This is the author's first Merge Request to this project."
+msgstr ""
+
+msgid "This issue is confidential and locked."
+msgstr ""
+
+msgid "This issue is locked."
msgstr ""
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Това означава, че нÑма да можете да изпращате код, докато не Ñъздадете празно хранилище или не внеÑете ÑъщеÑтвуващо такова."
+msgid "This merge request is locked."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Време преди един проблем да бъде планиран за работа"
@@ -1551,9 +2135,6 @@ msgstr "преди един меÑец"
msgid "Timeago|a week ago"
msgstr "преди една Ñедмица"
-msgid "Timeago|a while"
-msgstr "преди извеÑтно време"
-
msgid "Timeago|a year ago"
msgstr "преди една година"
@@ -1605,6 +2186,9 @@ msgstr "Ñлед 1 Ñедмица"
msgid "Timeago|in 1 year"
msgstr "Ñлед 1 година"
+msgid "Timeago|in a while"
+msgstr ""
+
msgid "Timeago|less than a minute ago"
msgstr "преди по-малко от минута"
@@ -1627,9 +2211,36 @@ msgstr "Общо време"
msgid "Total test time for all commits/merges"
msgstr "Общо време за теÑтване на вÑички подаваниÑ/ÑливаниÑ"
+msgid "Track activity with Contribution Analytics."
+msgstr ""
+
+msgid "Unlock"
+msgstr ""
+
+msgid "Unlocked"
+msgstr ""
+
msgid "Unstar"
msgstr "Без звезда"
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr ""
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr ""
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr ""
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr ""
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr ""
+
msgid "Upload New File"
msgstr "Качване на нов файл"
@@ -1672,9 +2283,120 @@ msgstr "ИÑкате ли да видите данните? Помолете аÐ
msgid "We don't have enough data to show this stage."
msgstr "ÐÑма доÑтатъчно данни за този етап."
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr ""
+
+msgid "Weight"
+msgstr ""
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
+msgid "WikiClone|Clone your wiki"
+msgstr ""
+
+msgid "WikiClone|Git Access"
+msgstr ""
+
+msgid "WikiClone|Install Gollum"
+msgstr ""
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr ""
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr ""
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr ""
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr ""
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr ""
+
+msgid "WikiHistoricalPage|history"
+msgstr ""
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr ""
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr ""
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr ""
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr ""
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr ""
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr ""
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr ""
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr ""
+
+msgid "WikiPageConflictMessage|the page"
+msgstr ""
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr ""
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr ""
+
+msgid "WikiPage|Page slug"
+msgstr ""
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr ""
+
+msgid "Wiki|Create Page"
+msgstr ""
+
+msgid "Wiki|Create page"
+msgstr ""
+
+msgid "Wiki|Edit Page"
+msgstr ""
+
+msgid "Wiki|Empty page"
+msgstr ""
+
+msgid "Wiki|More Pages"
+msgstr ""
+
+msgid "Wiki|New page"
+msgstr ""
+
+msgid "Wiki|Page history"
+msgstr ""
+
+msgid "Wiki|Page version"
+msgstr ""
+
+msgid "Wiki|Pages"
+msgstr ""
+
+msgid "Wiki|Wiki Pages"
+msgstr ""
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr ""
+
msgid "Withdraw Access Request"
msgstr "ОттеглÑне на заÑвката за доÑтъп"
@@ -1690,9 +2412,21 @@ msgstr "Ðа път Ñте да премахнете връзката на раÐ
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 on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "Можете да добавÑте файлове Ñамо когато Ñе намирате в клон"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Ðе можете да Ñъздавате повече проекти"
@@ -1718,11 +2452,17 @@ msgid "You will receive notifications only for comments in which you were @menti
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} за профила Ñи"
+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 "Your comment will not be visible to the public."
+msgstr ""
+
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "Вашето име"
@@ -1748,3 +2488,15 @@ msgid_plural "parents"
msgstr[0] "родител"
msgstr[1] "родители"
+msgid "password"
+msgstr ""
+
+msgid "personal access token"
+msgstr ""
+
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index fc3c60166b7..431e11818c1 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:45-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:30-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -21,6 +21,11 @@ msgid_plural "%d commits"
msgstr[0] "%d Commit"
msgstr[1] "%d Commits"
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] "%s zusätzlicher Commit wurde ausgelassen um Leistungsprobleme zu verhindern."
@@ -29,6 +34,11 @@ msgstr[1] "%s zusätzliche Commits wurden ausgelassen um Leistungsprobleme zu ve
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} hat %{commit_timeago} committet"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -49,6 +59,12 @@ msgstr[1] "%{storage_name}: %{failed_attempts} fehlgeschlagene Speicherzugriffe:
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(beachte die Informationen zur Installation auf %{link})."
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -57,6 +73,9 @@ msgstr[1] ""
msgid "1st contribution!"
msgstr ""
+msgid "2FA enabled"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Eine Sammlung von Graphen bezüglich kontinuierlicher Integration"
@@ -81,12 +100,18 @@ msgstr "Aktiv"
msgid "Activity"
msgstr "Aktivität"
+msgid "Add"
+msgstr ""
+
msgid "Add Changelog"
msgstr "Änderungsliste hinzufügen "
msgid "Add Contribution guide"
msgstr "Mitarbeitsanleitung hinzufügen"
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Add License"
msgstr "Lizenz hinzufügen"
@@ -96,9 +121,18 @@ msgstr "Füge einen SSH Schlüssel zu deinem Profil hinzu, um mittels SSH zu üb
msgid "Add new directory"
msgstr "Erstelle eine neues Verzeichnis"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr "Alle"
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -114,6 +148,9 @@ msgstr "Bist Du sicher, dass Du diesen Pipeline-Zeitplan löschen möchtest?"
msgid "Are you sure you want to discard your changes?"
msgstr "Bist Du sicher, dass Du alle Änderungen zurücksetzen willst?"
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "Bist Du sicher, dass Du den Registrierungstoken zurücksetzen willst?"
@@ -132,30 +169,39 @@ msgstr "Datei mittels Drag &amp; Drop oder %{upload_link} hinzufügen"
msgid "Authentication Log"
msgstr ""
-msgid "Auto DevOps (Beta)"
+msgid "Author"
msgstr ""
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto DevOps documentation"
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
-msgid "AutoDevOps|Learn more in the"
+msgid "AutoDevOps|Enable in settings"
msgstr ""
-msgid "Billing"
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
+msgstr ""
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
+msgid "Billing"
+msgstr "Abrechnung"
+
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
msgstr ""
@@ -168,6 +214,9 @@ msgstr ""
msgid "BillingPlans|Customer Support"
msgstr ""
+msgid "BillingPlans|Downgrade"
+msgstr ""
+
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
msgstr ""
@@ -204,12 +253,6 @@ msgstr ""
msgid "BillingPlans|per user"
msgstr ""
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
-
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "Zweig"
@@ -218,6 +261,9 @@ msgstr[1] "Zweige"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "Branch <strong>%{branch_name}</strong> wurde erstellt. Um die automatische Bereitstellung einzurichten, wähle eine GitLab CI Yaml Vorlage und committe Deine Änderungen. %{link_to_autodeploy_doc}"
+msgid "Branch has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Branches durchsuchen"
@@ -332,12 +378,18 @@ msgstr "CI / CD"
msgid "CI configuration"
msgstr "CI-Konfiguration"
+msgid "CICD|Jobs"
+msgstr ""
+
msgid "Cancel"
msgstr "Abbrechen"
msgid "Cancel edit"
msgstr "Bearbeitung abbrechen"
+msgid "Change Weight"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "In dem Branch wählen"
@@ -365,6 +417,9 @@ msgstr "Diesen Commit herauspicken "
msgid "Cherry-pick this merge request"
msgstr "Diesen Merge Request herauspicken"
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr ""
+
msgid "CiStatusLabel|canceled"
msgstr "abgebrochen"
@@ -419,6 +474,147 @@ msgstr "übersprungen"
msgid "CiStatus|running"
msgstr "laufend"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
+msgid "Clone repository"
+msgstr ""
+
+msgid "Close"
+msgstr "Schließen"
+
+msgid "Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Create cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr ""
+
+msgid "ClusterIntegration|Machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr ""
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
+msgstr ""
+
+msgid "ClusterIntegration|See your projects"
+msgstr ""
+
+msgid "ClusterIntegration|See zones"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr ""
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|cluster"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
msgid "Comments"
msgstr "Kommentare"
@@ -427,6 +623,14 @@ msgid_plural "Commits"
msgstr[0] "Commit"
msgstr[1] "Commits"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Commit Message"
+msgstr ""
+
msgid "Commit duration in minutes for last 30 commits"
msgstr "Dauer der Commits in Minuten für die letzten 30 Commits"
@@ -454,15 +658,66 @@ msgstr "Committed von"
msgid "Compare"
msgstr "Vergleichen"
+msgid "Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Created"
+msgstr ""
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr ""
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr ""
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Learn more about"
+msgstr ""
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr ""
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr ""
+
+msgid "ContainerRegistry|Remove repository"
+msgstr ""
+
+msgid "ContainerRegistry|Remove tag"
+msgstr ""
+
+msgid "ContainerRegistry|Size"
+msgstr ""
+
+msgid "ContainerRegistry|Tag"
+msgstr ""
+
+msgid "ContainerRegistry|Tag ID"
+msgstr ""
+
+msgid "ContainerRegistry|Use different image names"
+msgstr ""
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr ""
+
msgid "Contribution guide"
msgstr "Mitarbeitsanleitung"
msgid "Contributors"
msgstr "Mitarbeiter"
-msgid "Copy SSH public key to clipboard"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
msgstr ""
+msgid "Copy SSH public key to clipboard"
+msgstr "Öffentlichen SSH-Schlüssel in die Zwischenablage kopieren"
+
msgid "Copy URL to clipboard"
msgstr "Kopiere URL in die Zwischenablage"
@@ -472,9 +727,6 @@ msgstr "Kopiere Commit SHA in die Zwischenablage"
msgid "Create New Directory"
msgstr "Erstelle neues Verzeichnis"
-msgid "Create a new branch"
-msgstr "Erstelle einen neuen Branch"
-
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "Erstelle einen persönlichen Zugriffstoken in Deinem Konto um mittels %{protocol} zu übertragen (push) oder abzurufen (pull)."
@@ -484,9 +736,21 @@ msgstr "Erstelle Verzeichnis"
msgid "Create empty bare repository"
msgstr "Erstelle leeres Repository"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "Erstelle Merge Request"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "Erstelle neues..."
@@ -538,6 +802,12 @@ msgstr "Staging"
msgid "CycleAnalyticsStage|Test"
msgstr "Test"
+msgid "DashboardProjects|All"
+msgstr ""
+
+msgid "DashboardProjects|Personal"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "Erstelle ein individuelles Muster mittels Cron Syntax"
@@ -555,6 +825,9 @@ msgstr ""
msgid "Description"
msgstr "Beschreibung"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr ""
+
msgid "Details"
msgstr "Details"
@@ -564,6 +837,12 @@ msgstr "Verzeichnisname"
msgid "Discard changes"
msgstr "Änderungen verwerfen"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Dismiss Merge Request promotion"
+msgstr ""
+
msgid "Don't show again"
msgstr "Nicht erneut anzeigen"
@@ -603,9 +882,6 @@ msgstr "Pipeline Zeitplan bearbeiten %{id}"
msgid "Emails"
msgstr "E-Mails"
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr "Filtere alle"
@@ -636,12 +912,18 @@ msgstr "Wöchentlich (Sonntags um 4:00 Uhr)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Wechsel des Besitzers fehlgeschlagen"
msgid "Failed to remove the pipeline schedule"
msgstr "Entfernung der Pipelineplanung fehlgeschlagen"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "Dateien"
@@ -668,6 +950,12 @@ msgstr[1] "Ableger"
msgid "ForkedFromProjectPath|Forked from"
msgstr "Ableger von"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr ""
+
+msgid "Format"
+msgstr ""
+
msgid "From issue creation until deploy to production"
msgstr "Von der Ticketbeschreibung bis zur Bereitstellung"
@@ -680,6 +968,18 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
+msgid "Geo|Groups to replicate"
+msgstr ""
+
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
+msgid "Geo|Select groups to replicate."
+msgstr ""
+
msgid "Git storage health information has been reset"
msgstr "Informationen über den Speicherzustand von Gitlab wurden zurückgesetzt."
@@ -692,6 +992,9 @@ msgstr "Gehe zu Deinem Ableger"
msgid "GoToYourFork|Fork"
msgstr "Ableger"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -716,6 +1019,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr "Systemzustand"
@@ -734,28 +1082,60 @@ msgstr "Keine Probleme erkannt"
msgid "HealthCheck|Unhealthy"
msgstr "Problematisch"
+msgid "History"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "Aufräumen erfolgreich gestartet"
msgid "Import repository"
msgstr "Repository importieren"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Install a Runner compatible with GitLab CI"
msgstr "Installiere einen Runner der mit GitLab CI kompatibel ist"
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Intervallmuster"
msgid "Introducing Cycle Analytics"
msgstr "Arbeitsablaufsanalysen vorgestellt"
+msgid "Issue board focus mode"
+msgstr ""
+
+msgid "Issue boards with milestones"
+msgstr ""
+
msgid "Issue events"
msgstr "Ticketereignisse"
-msgid "Issues"
+msgid "IssueBoards|Board"
msgstr ""
-msgid "Jobs"
+msgid "IssueBoards|Boards"
+msgstr ""
+
+msgid "Issues"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -775,12 +1155,21 @@ msgstr[1] "Letzten %d Tage"
msgid "Last Pipeline"
msgstr "Letzte Pipeline"
-msgid "Last Update"
-msgstr "Letzte Aktualisierung"
-
msgid "Last commit"
msgstr "Letzter Commit"
+msgid "Last edited %{date}"
+msgstr ""
+
+msgid "Last edited by %{name}"
+msgstr ""
+
+msgid "Last update"
+msgstr ""
+
+msgid "Last updated"
+msgstr ""
+
msgid "LastPushEvent|You pushed to"
msgstr "Du übertrugst an"
@@ -793,6 +1182,9 @@ msgstr "Erfahre mehr in den"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "Pipelineplanungsdokumentation"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "Verlasse die Gruppe"
@@ -800,14 +1192,26 @@ msgid "Leave project"
msgstr "Verlasse das Projekt"
msgid "License"
-msgstr ""
+msgstr "Lizenz"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "Limitiere die Anzeige auf höchstens %d Ereignis"
msgstr[1] "Limitiere die Anzeige auf höchstens %d Ereignisse"
+msgid "Lock"
+msgstr ""
+
+msgid "Locked"
+msgstr ""
+
msgid "Locked Files"
+msgstr "Gesperrte Dateien"
+
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
msgstr ""
msgid "Median"
@@ -837,6 +1241,12 @@ msgstr "Ãœberwachung"
msgid "More information is available|here"
msgstr "hier"
+msgid "Multiple issue boards"
+msgstr ""
+
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Neues Ticket"
@@ -854,27 +1264,42 @@ msgstr "Neues Verzeichnis"
msgid "New file"
msgstr "Neue Datei"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "Neues Ticket"
msgid "New merge request"
msgstr "Neuer Merge Request"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "Neuer Zeitplan"
msgid "New snippet"
msgstr "Neuer Schnipsel"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "Neuer Tag"
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr ""
+
msgid "No repository"
msgstr "Kein Repository"
msgid "No schedules"
msgstr "Keine Zeitpläne"
+msgid "None"
+msgstr ""
+
msgid "Not available"
msgstr "Nicht verfügbar"
@@ -938,12 +1363,24 @@ msgstr "Beobachten"
msgid "Notifications"
msgstr "Benachrichtigungen"
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filter"
+msgid "Only project members can comment."
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Ungelöst"
+msgid "Opens in a new window"
+msgstr ""
+
msgid "Options"
msgstr "Optionen"
@@ -968,6 +1405,9 @@ msgstr ""
msgid "Password"
msgstr "Passwort"
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr ""
+
msgid "Pipeline"
msgstr ""
@@ -1070,7 +1510,52 @@ msgstr "mit Stages"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
+msgstr "Profil"
+
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@@ -1082,9 +1567,6 @@ msgstr "Das Projekt '%{project_name}' wurde erfolgreich erstellt."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Das Projekt '%{project_name}' wurde erfolgreich aktualisiert."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "Das Projekt '%{project_name}' wird gelöscht."
-
msgid "Project access must be granted explicitly to each user."
msgstr "Jedem Nutzer muss explizit der Zugriff auf das Projekt gewährt werden."
@@ -1127,6 +1609,27 @@ msgstr "Stage"
msgid "ProjectNetworkGraph|Graph"
msgstr "Diagramm"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr ""
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr ""
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1148,12 +1651,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr "Ãœbertragungsereignisse"
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Mehr lesen"
@@ -1217,6 +1729,12 @@ msgstr "Merge Request zurücksetzen"
msgid "SSH Keys"
msgstr "SSH-Schlüssel"
+msgid "Save"
+msgstr ""
+
+msgid "Save changes"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "Zeitplan der Pipeline speichern"
@@ -1232,15 +1750,21 @@ msgstr "Pipelines planen"
msgid "Search branches and tags"
msgstr "Suche nach Branches und Tags"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "Archivierungsformat auswählen"
msgid "Select a timezone"
msgstr "Zeitzone auswählen"
-msgid "Select existing branch"
-msgstr "Existierenden Branch auswählen"
-
msgid "Select target branch"
msgstr "Zielbranch auswählen"
@@ -1279,6 +1803,21 @@ msgstr[1] "Zeige %d Ereignisse"
msgid "Snippets"
msgstr ""
+msgid "Something went wrong on our end."
+msgstr ""
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
+msgid "Something went wrong while fetching the projects."
+msgstr ""
+
+msgid "Something went wrong while fetching the registry list."
+msgstr ""
+
+msgid "Sort by"
+msgstr ""
+
msgid "SortOptions|Access level, ascending"
msgstr ""
@@ -1396,6 +1935,12 @@ msgstr "Beginne einen %{new_merge_request} mit diesen Änderungen"
msgid "Start the Runner!"
msgstr "Starte den Runner!"
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Zu Branch/Tag wechseln"
@@ -1416,6 +1961,15 @@ msgstr "Zielbranch"
msgid "Team"
msgstr ""
+msgid "Thanks! Don't show me this again"
+msgstr ""
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr ""
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+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 Entwicklungsphase stellt die Zeit vom ersten Commit bis zum Erstellen eines Merge Requests dar. Sobald Du Deinen ersten Merge Request anlegst, werden dessen Daten automatisch ergänzt."
@@ -1428,6 +1982,15 @@ msgstr "Die Beziehung des Ablegers wurde entfernt."
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "Die Ticketphase stellt die Zeit vom Anlegen eines Tickets bis zum Zuweisen eines Meilensteins oder Hinzufügen zur Aufgabentafel dar. Erstelle einen Ticket, damit dessen Daten hier erscheinen."
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "Die Phase des Entwicklungslebenszyklus."
@@ -1458,6 +2021,12 @@ msgstr "Die Staging-Phase stellt die Zeit zwischen der Umsetzung eines Merge Re
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr "Die Testphase stellt die Zeit dar, die GitLab CI benötigt um die Pipelines von zugehörigen Merge Requests abzuarbeiten. Sobald die erste Pipeline abgeschlossen ist, werden deren Daten hier automatisch angezeigt."
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "Zeit, die für das jeweilige Ereignis in der Phase ermittelt wurde."
@@ -1467,12 +2036,27 @@ msgstr "Der mittlere aller erfassten Werte. Zum Beispiel ist für 3, 5, 9 der Me
msgid "There are problems accessing Git storage: "
msgstr "Es gibt ein Problem beim Zugriff auf den Gitspeicher:"
-msgid "This is the author's first Merge Request to this project. Handle with care."
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
+msgid "This is a confidential issue."
+msgstr ""
+
+msgid "This is the author's first Merge Request to this project."
+msgstr ""
+
+msgid "This issue is confidential and locked."
+msgstr ""
+
+msgid "This issue is locked."
msgstr ""
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Dies bedeutet, dass Du keinen Code übertragen kannst, bevor Du kein leeres Repositorium erstellt oder ein Existierendes importiert hast."
+msgid "This merge request is locked."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Zeit bis ein Ticket geplant wird"
@@ -1551,9 +2135,6 @@ msgstr "vor einem Monat"
msgid "Timeago|a week ago"
msgstr "vor einer Woche"
-msgid "Timeago|a while"
-msgstr "eine Weile"
-
msgid "Timeago|a year ago"
msgstr "vor einem Jahr"
@@ -1605,6 +2186,9 @@ msgstr "in 1 Woche"
msgid "Timeago|in 1 year"
msgstr "in 1 Jahr"
+msgid "Timeago|in a while"
+msgstr ""
+
msgid "Timeago|less than a minute ago"
msgstr "vor weniger als einer Minute"
@@ -1627,9 +2211,36 @@ msgstr "Gesamtzeit"
msgid "Total test time for all commits/merges"
msgstr "Gesamte Testzeit für alle Commits/Merges"
+msgid "Track activity with Contribution Analytics."
+msgstr ""
+
+msgid "Unlock"
+msgstr ""
+
+msgid "Unlocked"
+msgstr ""
+
msgid "Unstar"
msgstr "Entfavorisieren"
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr ""
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr ""
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr ""
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr ""
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr ""
+
msgid "Upload New File"
msgstr "Eine Neue Datei hochladen"
@@ -1672,7 +2283,118 @@ msgstr "Du möchtest diese Daten sehen? Bitte frage einen Administrator nach dem
msgid "We don't have enough data to show this stage."
msgstr "Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen."
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr ""
+
+msgid "Weight"
+msgstr ""
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
+msgstr "Wiki"
+
+msgid "WikiClone|Clone your wiki"
+msgstr ""
+
+msgid "WikiClone|Git Access"
+msgstr ""
+
+msgid "WikiClone|Install Gollum"
+msgstr ""
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr ""
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr ""
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr ""
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr ""
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr ""
+
+msgid "WikiHistoricalPage|history"
+msgstr ""
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr ""
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr ""
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr ""
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr ""
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr ""
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr ""
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr ""
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr ""
+
+msgid "WikiPageConflictMessage|the page"
+msgstr ""
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr ""
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr ""
+
+msgid "WikiPage|Page slug"
+msgstr ""
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr ""
+
+msgid "Wiki|Create Page"
+msgstr ""
+
+msgid "Wiki|Create page"
+msgstr ""
+
+msgid "Wiki|Edit Page"
+msgstr ""
+
+msgid "Wiki|Empty page"
+msgstr ""
+
+msgid "Wiki|More Pages"
+msgstr ""
+
+msgid "Wiki|New page"
+msgstr ""
+
+msgid "Wiki|Page history"
+msgstr ""
+
+msgid "Wiki|Page version"
+msgstr ""
+
+msgid "Wiki|Pages"
+msgstr ""
+
+msgid "Wiki|Wiki Pages"
+msgstr ""
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
msgstr ""
msgid "Withdraw Access Request"
@@ -1690,9 +2412,21 @@ msgstr "Du bist dabei, die Beziehung des Ablegers zum Ursprungsprojekt %{forked_
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Du bist dabei %{project_name_with_namespace} einem andere Besitzer zu übergeben. Bist Du dir WIRKLICH sicher?"
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "Du kannst Dateien nur hinzufügen, wenn Du dich auf einem Branch befindest."
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Du hast die Projektbegrenzung erreicht."
@@ -1723,14 +2457,20 @@ msgstr "Du kannst erst mittels '%{protocol}' übertragen (push) oder abrufen (pu
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "Du kannst erst mittels SSH übertragen (push) oder abrufen (pull), nachdem Du Deinem Konto '%{add_ssh_key_link}'."
+msgid "Your comment will not be visible to the public."
+msgstr ""
+
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "Dein Name"
msgid "Your projects"
-msgstr ""
+msgstr "Deine Projekte"
msgid "commit"
-msgstr ""
+msgstr "Commit"
msgid "day"
msgid_plural "days"
@@ -1748,3 +2488,15 @@ msgid_plural "parents"
msgstr[0] "Vorgänger"
msgstr[1] "Vorgänger"
+msgid "password"
+msgstr ""
+
+msgid "personal access token"
+msgstr ""
+
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index e8c2195e4e3..0a1b379b3d3 100644
--- a/locale/eo/gitlab.po
+++ b/locale/eo/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:45-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:30-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Esperanto\n"
"Language: eo_UY\n"
@@ -21,6 +21,11 @@ msgid_plural "%d commits"
msgstr[0] "%d enmetado"
msgstr[1] "%d enmetadoj"
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] "%s enmetado estis transsaltita, por ne troÅarÄi la sistemon."
@@ -29,6 +34,11 @@ 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 "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -49,6 +59,12 @@ msgstr[1] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 ĉenstablo"
@@ -57,6 +73,9 @@ msgstr[1] "%d ĉenstabloj"
msgid "1st contribution!"
msgstr ""
+msgid "2FA enabled"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Aro da diagramoj pri la seninterrompa integrado"
@@ -81,12 +100,18 @@ msgstr "Aktiva"
msgid "Activity"
msgstr "Aktiveco"
+msgid "Add"
+msgstr ""
+
msgid "Add Changelog"
msgstr "Aldoni liston de ÅanÄoj"
msgid "Add Contribution guide"
msgstr "Aldoni gvidliniojn por kontribuado"
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Add License"
msgstr "Aldoni rajtigilon"
@@ -96,9 +121,18 @@ msgstr "Aldonu SSH-Ålosilon al via profilo por ebligi al vi eltiri kaj alpuÅi
msgid "Add new directory"
msgstr "Aldoni novan dosierujon"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr ""
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -114,6 +148,9 @@ 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 leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -132,25 +169,34 @@ msgstr "Alkroĉu dosieron per Åovmetado aÅ­ %{upload_link}"
msgid "Authentication Log"
msgstr ""
-msgid "Auto DevOps (Beta)"
+msgid "Author"
msgstr ""
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto DevOps documentation"
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgid "AutoDevOps|Auto DevOps documentation"
+msgstr ""
+
+msgid "AutoDevOps|Enable in settings"
+msgstr ""
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
msgstr ""
-msgid "AutoDevOps|Learn more in the"
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
+msgstr ""
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
msgid "Billing"
@@ -168,6 +214,9 @@ msgstr ""
msgid "BillingPlans|Customer Support"
msgstr ""
+msgid "BillingPlans|Downgrade"
+msgstr ""
+
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
msgstr ""
@@ -204,12 +253,6 @@ msgstr ""
msgid "BillingPlans|per user"
msgstr ""
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
-
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "Branĉo"
@@ -218,6 +261,9 @@ 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 has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Serĉu branĉon"
@@ -332,12 +378,18 @@ msgstr ""
msgid "CI configuration"
msgstr "Agordoj de seninterrompa integrado"
+msgid "CICD|Jobs"
+msgstr ""
+
msgid "Cancel"
msgstr "Nuligi"
msgid "Cancel edit"
msgstr ""
+msgid "Change Weight"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Elekti en branĉon"
@@ -365,6 +417,9 @@ msgstr "Precize elekti ĉi tiun kunmetadon"
msgid "Cherry-pick this merge request"
msgstr "Precize elekti ĉi tiun peton pri kunfando"
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr ""
+
msgid "CiStatusLabel|canceled"
msgstr "nuligita"
@@ -419,6 +474,147 @@ msgstr "transsaltita"
msgid "CiStatus|running"
msgstr "plenumiÄanta"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
+msgid "Clone repository"
+msgstr ""
+
+msgid "Close"
+msgstr ""
+
+msgid "Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Create cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr ""
+
+msgid "ClusterIntegration|Machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr ""
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
+msgstr ""
+
+msgid "ClusterIntegration|See your projects"
+msgstr ""
+
+msgid "ClusterIntegration|See zones"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr ""
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|cluster"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -427,6 +623,14 @@ msgid_plural "Commits"
msgstr[0] "Enmetado"
msgstr[1] "Enmetadoj"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Commit Message"
+msgstr ""
+
msgid "Commit duration in minutes for last 30 commits"
msgstr "DaÅ­ro de la enmetadoj por la lastaj 30 enmetadoj"
@@ -454,12 +658,63 @@ msgstr "Enmetita de"
msgid "Compare"
msgstr "Kompari"
+msgid "Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Created"
+msgstr ""
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr ""
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr ""
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Learn more about"
+msgstr ""
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr ""
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr ""
+
+msgid "ContainerRegistry|Remove repository"
+msgstr ""
+
+msgid "ContainerRegistry|Remove tag"
+msgstr ""
+
+msgid "ContainerRegistry|Size"
+msgstr ""
+
+msgid "ContainerRegistry|Tag"
+msgstr ""
+
+msgid "ContainerRegistry|Tag ID"
+msgstr ""
+
+msgid "ContainerRegistry|Use different image names"
+msgstr ""
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr ""
+
msgid "Contribution guide"
msgstr "Gvidlinioj por kontribuado"
msgid "Contributors"
msgstr "Kontribuantoj"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -472,9 +727,6 @@ msgstr "Kopii la identigilon de la enmetado"
msgid "Create New Directory"
msgstr "Krei novan dosierujon"
-msgid "Create a new branch"
-msgstr ""
-
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}."
@@ -484,9 +736,21 @@ msgstr "Krei dosierujon"
msgid "Create empty bare repository"
msgstr "Krei malplenan deponejon"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "Krei peton pri kunfando"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "Krei novan…"
@@ -538,6 +802,12 @@ msgstr "Preparo por eldono"
msgid "CycleAnalyticsStage|Test"
msgstr "Testado"
+msgid "DashboardProjects|All"
+msgstr ""
+
+msgid "DashboardProjects|Personal"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "Difini propran Åablonon, uzante la sintakson de Cron"
@@ -555,6 +825,9 @@ msgstr ""
msgid "Description"
msgstr "Priskribo"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr ""
+
msgid "Details"
msgstr ""
@@ -564,6 +837,12 @@ msgstr "Nomo de dosierujo"
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Dismiss Merge Request promotion"
+msgstr ""
+
msgid "Don't show again"
msgstr "Ne montru denove"
@@ -603,9 +882,6 @@ msgstr "Redakti ĉenstablan planon %{id}"
msgid "Emails"
msgstr ""
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -636,12 +912,18 @@ msgstr "Ĉiusemajne (en dimanĉo, je 4:00)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Ne eblas ÅanÄi la posedanton"
msgid "Failed to remove the pipeline schedule"
msgstr "Ne eblas forigi la ĉenstablan planon"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "Dosieroj"
@@ -668,6 +950,12 @@ msgstr[1] "Disbranĉigoj"
msgid "ForkedFromProjectPath|Forked from"
msgstr "Disbranĉigita el"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr ""
+
+msgid "Format"
+msgstr ""
+
msgid "From issue creation until deploy to production"
msgstr "De la kreado de la problemo Äis la disponigado en la publika versio"
@@ -680,6 +968,18 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
+msgid "Geo|Groups to replicate"
+msgstr ""
+
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
+msgid "Geo|Select groups to replicate."
+msgstr ""
+
msgid "Git storage health information has been reset"
msgstr ""
@@ -692,6 +992,9 @@ msgstr "Al via disbranĉigo"
msgid "GoToYourFork|Fork"
msgstr "Disbranĉigo"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -716,6 +1019,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -734,28 +1082,60 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
+msgid "History"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "La refreÅigo komenciÄis sukcese"
msgid "Import repository"
msgstr "Enporti deponejon"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Intervala Åablono"
msgid "Introducing Cycle Analytics"
msgstr "Ni prezentas al vi la ciklan analizon"
+msgid "Issue board focus mode"
+msgstr ""
+
+msgid "Issue boards with milestones"
+msgstr ""
+
msgid "Issue events"
msgstr ""
-msgid "Issues"
+msgid "IssueBoards|Board"
msgstr ""
-msgid "Jobs"
+msgid "IssueBoards|Boards"
+msgstr ""
+
+msgid "Issues"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -775,12 +1155,21 @@ msgstr[1] "La lastaj %d tagoj"
msgid "Last Pipeline"
msgstr "Lasta ĉenstablo"
-msgid "Last Update"
-msgstr "Lasta Äisdatigo"
-
msgid "Last commit"
msgstr "Lasta enmetado"
+msgid "Last edited %{date}"
+msgstr ""
+
+msgid "Last edited by %{name}"
+msgstr ""
+
+msgid "Last update"
+msgstr ""
+
+msgid "Last updated"
+msgstr ""
+
msgid "LastPushEvent|You pushed to"
msgstr ""
@@ -793,6 +1182,9 @@ msgstr "Lernu pli en la"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "dokumentado pri ĉenstablaj planoj"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "Forlasi la grupon"
@@ -807,9 +1199,21 @@ msgid_plural "Limited to showing %d events at most"
msgstr[0] "Limigita al montrado de ne pli ol %d evento"
msgstr[1] "Limigita al montrado de ne pli ol %d eventoj"
+msgid "Lock"
+msgstr ""
+
+msgid "Locked"
+msgstr ""
+
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "Mediano"
@@ -837,6 +1241,12 @@ msgstr ""
msgid "More information is available|here"
msgstr ""
+msgid "Multiple issue boards"
+msgstr ""
+
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nova problemo"
@@ -854,27 +1264,42 @@ msgstr "Nova dosierujo"
msgid "New file"
msgstr "Nova dosiero"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "Nova problemo"
msgid "New merge request"
msgstr "Nova peto pri kunfando"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "Nova plano"
msgid "New snippet"
msgstr "Nova kodaĵo"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "Nova etikedo"
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr ""
+
msgid "No repository"
msgstr "Ne estas deponejo"
msgid "No schedules"
msgstr "Ne estas planoj"
+msgid "None"
+msgstr ""
+
msgid "Not available"
msgstr "Ne disponebla"
@@ -938,12 +1363,24 @@ msgstr "Rigardado"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrilo"
+msgid "Only project members can comment."
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Malfermita"
+msgid "Opens in a new window"
+msgstr ""
+
msgid "Options"
msgstr "Opcioj"
@@ -968,6 +1405,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr ""
+
msgid "Pipeline"
msgstr "Ĉenstablo"
@@ -1070,9 +1510,54 @@ msgstr "kun etapoj"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "La projekto „%{project_name}“ estis alvicigita por forigado."
@@ -1082,9 +1567,6 @@ msgstr "La projekto „%{project_name}“ estis sukcese kreita."
msgid "Project '%{project_name}' was successfully updated."
msgstr "La projekto „%{project_name}“ estis sukcese Äisdatigita."
-msgid "Project '%{project_name}' will be deleted."
-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."
@@ -1127,6 +1609,27 @@ msgstr "Etapo"
msgid "ProjectNetworkGraph|Graph"
msgstr "Grafeo"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr ""
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr ""
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1148,12 +1651,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr ""
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Legu pli"
@@ -1217,6 +1729,12 @@ msgstr "Malfari ĉi tiun peton pri kunfando"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
+msgid "Save changes"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "Konservi ĉenstablan planon"
@@ -1232,15 +1750,21 @@ msgstr "Planado de la ĉenstabloj"
msgid "Search branches and tags"
msgstr "Serĉu branĉon aŭ etikedon"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
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"
@@ -1279,6 +1803,21 @@ msgstr[1] "Estas montrataj %d eventoj"
msgid "Snippets"
msgstr ""
+msgid "Something went wrong on our end."
+msgstr ""
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
+msgid "Something went wrong while fetching the projects."
+msgstr ""
+
+msgid "Something went wrong while fetching the registry list."
+msgstr ""
+
+msgid "Sort by"
+msgstr ""
+
msgid "SortOptions|Access level, ascending"
msgstr ""
@@ -1396,6 +1935,12 @@ msgstr "Kreu %{new_merge_request} kun ĉi tiuj ÅanÄoj"
msgid "Start the Runner!"
msgstr ""
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Iri al branĉo/etikedo"
@@ -1416,6 +1961,15 @@ msgstr "Cela branĉo"
msgid "Team"
msgstr ""
+msgid "Thanks! Don't show me this again"
+msgstr ""
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr ""
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+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 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."
@@ -1428,6 +1982,15 @@ 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 number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "La etapo de la disvolva ciklo."
@@ -1458,6 +2021,12 @@ msgstr "La etapo de preparo por eldono montras la tempon inter la aplikado de la
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 in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
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."
@@ -1467,12 +2036,27 @@ msgstr "La valoro, kiu troviÄas en la mezo de aro da rigardataj valoroj. Ekzemp
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "This is the author's first Merge Request to this project. Handle with care."
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
+msgid "This is a confidential issue."
+msgstr ""
+
+msgid "This is the author's first Merge Request to this project."
+msgstr ""
+
+msgid "This issue is confidential and locked."
+msgstr ""
+
+msgid "This issue is locked."
msgstr ""
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Ĉi tiu signifas, ke vi ne povos alpuÅi kodon, antaÅ­ ol vi kreos malplenan deponejon aÅ­ enportos jam ekzistantan."
+msgid "This merge request is locked."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Tempo antaÅ­ problemo estas planita por ellabori"
@@ -1551,9 +2135,6 @@ msgstr "antaÅ­ unu monato"
msgid "Timeago|a week ago"
msgstr "antaÅ­ unu semajno"
-msgid "Timeago|a while"
-msgstr "antaÅ­ iom da tempo"
-
msgid "Timeago|a year ago"
msgstr "antaÅ­ unu jaro"
@@ -1605,6 +2186,9 @@ msgstr "post 1 semajno"
msgid "Timeago|in 1 year"
msgstr "post 1 jaro"
+msgid "Timeago|in a while"
+msgstr ""
+
msgid "Timeago|less than a minute ago"
msgstr "antaÅ­ malpli ol minuto"
@@ -1627,9 +2211,36 @@ msgstr "Totala tempo"
msgid "Total test time for all commits/merges"
msgstr "Totala tempo por la testado de ĉiuj enmetadoj/kunfandoj"
+msgid "Track activity with Contribution Analytics."
+msgstr ""
+
+msgid "Unlock"
+msgstr ""
+
+msgid "Unlocked"
+msgstr ""
+
msgid "Unstar"
msgstr "Malsteligi"
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr ""
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr ""
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr ""
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr ""
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr ""
+
msgid "Upload New File"
msgstr "AlÅuti novan dosieron"
@@ -1672,9 +2283,120 @@ msgstr "Ĉu vi volas vidi la datenojn? Bonvolu peti atingeblon de administranto.
msgid "We don't have enough data to show this stage."
msgstr "Ne estas sufiĉe da datenoj por montri ĉi tiun etapon."
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr ""
+
+msgid "Weight"
+msgstr ""
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
+msgid "WikiClone|Clone your wiki"
+msgstr ""
+
+msgid "WikiClone|Git Access"
+msgstr ""
+
+msgid "WikiClone|Install Gollum"
+msgstr ""
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr ""
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr ""
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr ""
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr ""
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr ""
+
+msgid "WikiHistoricalPage|history"
+msgstr ""
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr ""
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr ""
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr ""
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr ""
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr ""
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr ""
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr ""
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr ""
+
+msgid "WikiPageConflictMessage|the page"
+msgstr ""
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr ""
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr ""
+
+msgid "WikiPage|Page slug"
+msgstr ""
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr ""
+
+msgid "Wiki|Create Page"
+msgstr ""
+
+msgid "Wiki|Create page"
+msgstr ""
+
+msgid "Wiki|Edit Page"
+msgstr ""
+
+msgid "Wiki|Empty page"
+msgstr ""
+
+msgid "Wiki|More Pages"
+msgstr ""
+
+msgid "Wiki|New page"
+msgstr ""
+
+msgid "Wiki|Page history"
+msgstr ""
+
+msgid "Wiki|Page version"
+msgstr ""
+
+msgid "Wiki|Pages"
+msgstr ""
+
+msgid "Wiki|Wiki Pages"
+msgstr ""
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr ""
+
msgid "Withdraw Access Request"
msgstr "Nuligi la peton pri atingeblo"
@@ -1688,11 +2410,23 @@ msgid "You are going to remove the fork relationship to source project %{forked_
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?"
+msgstr "Vi estas transigonta „%{project_name_with_namespace}“ al alia posedanto. Ĉu vi estas ABSOLUTE certa?"
+
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
msgid "You can only add files when you are on a branch"
msgstr "Oni povas aldoni dosierojn nur kiam oni estas en branĉo"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Vi ne povas krei pliajn projektojn"
@@ -1723,6 +2457,12 @@ msgstr "Vi ne povos eltiri aÅ­ alpuÅi kodon per %{protocol} antaÅ­ ol vi %{set_
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 comment will not be visible to the public."
+msgstr ""
+
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "Via nomo"
@@ -1748,3 +2488,15 @@ msgid_plural "parents"
msgstr[0] "patro"
msgstr[1] "patroj"
+msgid "password"
+msgstr ""
+
+msgid "personal access token"
+msgstr ""
+
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po
index 29a010f9428..31ff4e08592 100644
--- a/locale/es/gitlab.po
+++ b/locale/es/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:43-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:31-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -21,6 +21,11 @@ msgid_plural "%d commits"
msgstr[0] "%d cambio"
msgstr[1] "%d cambios"
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] "%s cambio adicional ha sido omitido para evitar problemas de rendimiento."
@@ -29,6 +34,11 @@ 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 "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -49,6 +59,12 @@ msgstr[1] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -57,6 +73,9 @@ msgstr[1] ""
msgid "1st contribution!"
msgstr ""
+msgid "2FA enabled"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Una colección de gráficos sobre Integración Continua"
@@ -81,12 +100,18 @@ msgstr "Activo"
msgid "Activity"
msgstr "Actividad"
+msgid "Add"
+msgstr ""
+
msgid "Add Changelog"
msgstr "Agregar Changelog"
msgid "Add Contribution guide"
msgstr "Agregar guía de contribución"
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Add License"
msgstr "Agregar Licencia"
@@ -96,9 +121,18 @@ 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 "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr ""
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -114,6 +148,9 @@ 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 leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -132,25 +169,34 @@ msgstr "Adjunte un archivo arrastrando &amp; soltando o %{upload_link}"
msgid "Authentication Log"
msgstr ""
-msgid "Auto DevOps (Beta)"
+msgid "Author"
msgstr ""
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto DevOps documentation"
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgid "AutoDevOps|Auto DevOps documentation"
+msgstr ""
+
+msgid "AutoDevOps|Enable in settings"
+msgstr ""
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
msgstr ""
-msgid "AutoDevOps|Learn more in the"
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
+msgstr ""
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
msgid "Billing"
@@ -168,6 +214,9 @@ msgstr ""
msgid "BillingPlans|Customer Support"
msgstr ""
+msgid "BillingPlans|Downgrade"
+msgstr ""
+
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
msgstr ""
@@ -204,12 +253,6 @@ msgstr ""
msgid "BillingPlans|per user"
msgstr ""
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
-
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "Rama"
@@ -218,6 +261,9 @@ 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 "Branch has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Buscar ramas"
@@ -332,12 +378,18 @@ msgstr ""
msgid "CI configuration"
msgstr "Configuración de CI"
+msgid "CICD|Jobs"
+msgstr ""
+
msgid "Cancel"
msgstr "Cancelar"
msgid "Cancel edit"
msgstr ""
+msgid "Change Weight"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Escoger en la rama"
@@ -365,6 +417,9 @@ msgstr "Escoger este cambio"
msgid "Cherry-pick this merge request"
msgstr "Escoger esta solicitud de fusión"
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr ""
+
msgid "CiStatusLabel|canceled"
msgstr "cancelado"
@@ -419,6 +474,147 @@ msgstr "omitido"
msgid "CiStatus|running"
msgstr "en ejecución"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
+msgid "Clone repository"
+msgstr ""
+
+msgid "Close"
+msgstr ""
+
+msgid "Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Create cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr ""
+
+msgid "ClusterIntegration|Machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr ""
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
+msgstr ""
+
+msgid "ClusterIntegration|See your projects"
+msgstr ""
+
+msgid "ClusterIntegration|See zones"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr ""
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|cluster"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -427,6 +623,14 @@ msgid_plural "Commits"
msgstr[0] "Cambio"
msgstr[1] "Cambios"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Commit Message"
+msgstr ""
+
msgid "Commit duration in minutes for last 30 commits"
msgstr "Duración de los cambios en minutos para los últimos 30"
@@ -454,12 +658,63 @@ msgstr "Enviado por"
msgid "Compare"
msgstr "Comparar"
+msgid "Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Created"
+msgstr ""
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr ""
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr ""
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Learn more about"
+msgstr ""
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr ""
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr ""
+
+msgid "ContainerRegistry|Remove repository"
+msgstr ""
+
+msgid "ContainerRegistry|Remove tag"
+msgstr ""
+
+msgid "ContainerRegistry|Size"
+msgstr ""
+
+msgid "ContainerRegistry|Tag"
+msgstr ""
+
+msgid "ContainerRegistry|Tag ID"
+msgstr ""
+
+msgid "ContainerRegistry|Use different image names"
+msgstr ""
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr ""
+
msgid "Contribution guide"
msgstr "Guía de contribución"
msgid "Contributors"
msgstr "Contribuidores"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -472,9 +727,6 @@ 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}."
@@ -484,9 +736,21 @@ msgstr "Crear directorio"
msgid "Create empty bare repository"
msgstr "Crear repositorio vacío"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "Crear solicitud de fusión"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "Crear nuevo..."
@@ -538,6 +802,12 @@ msgstr "Puesta en escena"
msgid "CycleAnalyticsStage|Test"
msgstr "Pruebas"
+msgid "DashboardProjects|All"
+msgstr ""
+
+msgid "DashboardProjects|Personal"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "Definir un patrón personalizado con la sintaxis de cron"
@@ -555,6 +825,9 @@ msgstr ""
msgid "Description"
msgstr "Descripción"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr ""
+
msgid "Details"
msgstr ""
@@ -564,6 +837,12 @@ msgstr "Nombre del directorio"
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Dismiss Merge Request promotion"
+msgstr ""
+
msgid "Don't show again"
msgstr "No mostrar de nuevo"
@@ -603,9 +882,6 @@ msgstr "Editar Programación del Pipeline %{id}"
msgid "Emails"
msgstr ""
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -636,12 +912,18 @@ msgstr "Todas las semanas (domingos a las 4:00 am)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
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 "File name"
+msgstr ""
+
msgid "Files"
msgstr "Archivos"
@@ -668,6 +950,12 @@ msgstr[1] "Bifurcaciones"
msgid "ForkedFromProjectPath|Forked from"
msgstr "Bifurcado de"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr ""
+
+msgid "Format"
+msgstr ""
+
msgid "From issue creation until deploy to production"
msgstr "Desde la creación de la incidencia hasta el despliegue a producción"
@@ -680,6 +968,18 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
+msgid "Geo|Groups to replicate"
+msgstr ""
+
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
+msgid "Geo|Select groups to replicate."
+msgstr ""
+
msgid "Git storage health information has been reset"
msgstr ""
@@ -692,6 +992,9 @@ msgstr "Ir a tu bifurcación"
msgid "GoToYourFork|Fork"
msgstr "Bifurcación"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -716,6 +1019,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -734,28 +1082,60 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
+msgid "History"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "Servicio de limpieza iniciado con éxito"
msgid "Import repository"
msgstr "Importar repositorio"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Patrón de intervalo"
msgid "Introducing Cycle Analytics"
msgstr "Introducción a Cycle Analytics"
+msgid "Issue board focus mode"
+msgstr ""
+
+msgid "Issue boards with milestones"
+msgstr ""
+
msgid "Issue events"
msgstr ""
-msgid "Issues"
+msgid "IssueBoards|Board"
msgstr ""
-msgid "Jobs"
+msgid "IssueBoards|Boards"
+msgstr ""
+
+msgid "Issues"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -775,12 +1155,21 @@ 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 "Last edited %{date}"
+msgstr ""
+
+msgid "Last edited by %{name}"
+msgstr ""
+
+msgid "Last update"
+msgstr ""
+
+msgid "Last updated"
+msgstr ""
+
msgid "LastPushEvent|You pushed to"
msgstr ""
@@ -793,6 +1182,9 @@ 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"
+msgstr ""
+
msgid "Leave group"
msgstr "Abandonar grupo"
@@ -807,9 +1199,21 @@ 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 "Lock"
+msgstr ""
+
+msgid "Locked"
+msgstr ""
+
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "Mediana"
@@ -837,6 +1241,12 @@ msgstr ""
msgid "More information is available|here"
msgstr ""
+msgid "Multiple issue boards"
+msgstr ""
+
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nueva incidencia"
@@ -854,27 +1264,42 @@ msgstr "Nuevo directorio"
msgid "New file"
msgstr "Nuevo archivo"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "Nueva incidencia"
msgid "New merge request"
msgstr "Nueva solicitud de fusión"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "Nueva programación"
msgid "New snippet"
msgstr "Nuevo fragmento de código"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "Nueva etiqueta"
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr ""
+
msgid "No repository"
msgstr "No hay repositorio"
msgid "No schedules"
msgstr "No hay programaciones"
+msgid "None"
+msgstr ""
+
msgid "Not available"
msgstr "No disponible"
@@ -938,12 +1363,24 @@ msgstr "Vigilancia"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrar"
+msgid "Only project members can comment."
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Abierto"
+msgid "Opens in a new window"
+msgstr ""
+
msgid "Options"
msgstr "Opciones"
@@ -968,6 +1405,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr ""
+
msgid "Pipeline"
msgstr ""
@@ -1070,9 +1510,54 @@ msgstr "con etapas"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Proyecto ‘%{project_name}’ en cola para eliminación."
@@ -1082,9 +1567,6 @@ 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."
@@ -1127,6 +1609,27 @@ msgstr "Etapa"
msgid "ProjectNetworkGraph|Graph"
msgstr "Historial gráfico"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr ""
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr ""
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1148,12 +1651,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr ""
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Leer más"
@@ -1217,6 +1729,12 @@ msgstr "Revertir esta solicitud de fusión"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
+msgid "Save changes"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "Guardar programación del pipeline"
@@ -1232,15 +1750,21 @@ msgstr "Programación de Pipelines"
msgid "Search branches and tags"
msgstr "Buscar ramas y etiquetas"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
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"
@@ -1279,6 +1803,21 @@ msgstr[1] "Mostrando %d eventos"
msgid "Snippets"
msgstr ""
+msgid "Something went wrong on our end."
+msgstr ""
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
+msgid "Something went wrong while fetching the projects."
+msgstr ""
+
+msgid "Something went wrong while fetching the registry list."
+msgstr ""
+
+msgid "Sort by"
+msgstr ""
+
msgid "SortOptions|Access level, ascending"
msgstr ""
@@ -1396,6 +1935,12 @@ msgstr "Iniciar una %{new_merge_request} con estos cambios"
msgid "Start the Runner!"
msgstr ""
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Cambiar rama/etiqueta"
@@ -1416,6 +1961,15 @@ msgstr "Rama de destino"
msgid "Team"
msgstr ""
+msgid "Thanks! Don't show me this again"
+msgstr ""
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr ""
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+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."
@@ -1428,6 +1982,15 @@ 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 number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "La etapa del ciclo de vida de desarrollo."
@@ -1458,6 +2021,12 @@ msgstr "La etapa de puesta en escena muestra el tiempo entre la fusión y el des
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 in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
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."
@@ -1467,12 +2036,27 @@ msgstr "El valor en el punto medio de una serie de valores observados. Por ejemp
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "This is the author's first Merge Request to this project. Handle with care."
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
+msgid "This is a confidential issue."
+msgstr ""
+
+msgid "This is the author's first Merge Request to this project."
+msgstr ""
+
+msgid "This issue is confidential and locked."
+msgstr ""
+
+msgid "This issue is locked."
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."
+msgid "This merge request is locked."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Tiempo antes de que una incidencia sea programada"
@@ -1551,9 +2135,6 @@ 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"
@@ -1605,6 +2186,9 @@ msgstr "en 1 semana"
msgid "Timeago|in 1 year"
msgstr "en 1 año"
+msgid "Timeago|in a while"
+msgstr ""
+
msgid "Timeago|less than a minute ago"
msgstr "hace menos de 1 minuto"
@@ -1627,9 +2211,36 @@ msgstr "Tiempo Total"
msgid "Total test time for all commits/merges"
msgstr "Tiempo total de pruebas para todos los cambios o integraciones"
+msgid "Track activity with Contribution Analytics."
+msgstr ""
+
+msgid "Unlock"
+msgstr ""
+
+msgid "Unlocked"
+msgstr ""
+
msgid "Unstar"
msgstr "No Destacar"
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr ""
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr ""
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr ""
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr ""
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr ""
+
msgid "Upload New File"
msgstr "Subir nuevo archivo"
@@ -1672,9 +2283,120 @@ 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 "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr ""
+
+msgid "Weight"
+msgstr ""
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
+msgid "WikiClone|Clone your wiki"
+msgstr ""
+
+msgid "WikiClone|Git Access"
+msgstr ""
+
+msgid "WikiClone|Install Gollum"
+msgstr ""
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr ""
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr ""
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr ""
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr ""
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr ""
+
+msgid "WikiHistoricalPage|history"
+msgstr ""
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr ""
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr ""
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr ""
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr ""
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr ""
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr ""
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr ""
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr ""
+
+msgid "WikiPageConflictMessage|the page"
+msgstr ""
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr ""
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr ""
+
+msgid "WikiPage|Page slug"
+msgstr ""
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr ""
+
+msgid "Wiki|Create Page"
+msgstr ""
+
+msgid "Wiki|Create page"
+msgstr ""
+
+msgid "Wiki|Edit Page"
+msgstr ""
+
+msgid "Wiki|Empty page"
+msgstr ""
+
+msgid "Wiki|More Pages"
+msgstr ""
+
+msgid "Wiki|New page"
+msgstr ""
+
+msgid "Wiki|Page history"
+msgstr ""
+
+msgid "Wiki|Page version"
+msgstr ""
+
+msgid "Wiki|Pages"
+msgstr ""
+
+msgid "Wiki|Wiki Pages"
+msgstr ""
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr ""
+
msgid "Withdraw Access Request"
msgstr "Retirar Solicitud de Acceso"
@@ -1690,9 +2412,21 @@ msgstr "Vas a eliminar el enlace de la bifurcación con el proyecto original %{f
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 are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
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 cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Has alcanzado el límite de tu proyecto"
@@ -1723,6 +2457,12 @@ msgstr "No podrás actualizar o enviar código al proyecto a través de %{protoc
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 comment will not be visible to the public."
+msgstr ""
+
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "Tu nombre"
@@ -1748,3 +2488,15 @@ msgid_plural "parents"
msgstr[0] "padre"
msgstr[1] "padres"
+msgid "password"
+msgstr ""
+
+msgid "personal access token"
+msgstr ""
+
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index 28d9c6a3e56..a4f9a34b13b 100644
--- a/locale/fr/gitlab.po
+++ b/locale/fr/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:45-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:30-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -21,6 +21,11 @@ msgid_plural "%d commits"
msgstr[0] "%d validation"
msgstr[1] "%d validations"
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] "%d couche"
+msgstr[1] "%d couches"
+
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."
@@ -29,8 +34,13 @@ msgstr[1] "%s validations supplémentaires ont été masquées afin d'éviter de
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} a validé %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
-msgstr ""
+msgstr "%{number_commits_behind} validations de retard sur %{default_branch}, %{number_commits_ahead} validations d'avance"
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr "%{number_of_failures} sur %{maximum_failures} tentative(s). GitLab va vous permettre d'accéder à la prochaine tentative."
@@ -49,19 +59,28 @@ msgstr[1] "%{storage_name} : %{failed_attempts} tentatives d’accès au stocka
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(Lisez %{link} pour savoir comment l'installer)."
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 pipeline"
msgstr[1] "%d pipelines"
msgid "1st contribution!"
-msgstr ""
+msgstr "1ère contribution !"
+
+msgid "2FA enabled"
+msgstr "2FA activé"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Un ensemble de graphiques concernant l’Intégration Continue (CI)"
msgid "About auto deploy"
-msgstr "A propos de l'auto-déploiement"
+msgstr "À propos de l'auto-déploiement"
msgid "Abuse Reports"
msgstr "Rapports d’abus"
@@ -81,12 +100,18 @@ msgstr "Actif"
msgid "Activity"
msgstr "Activité"
+msgid "Add"
+msgstr "Ajouter"
+
msgid "Add Changelog"
msgstr "Ajouter un journal des modifications"
msgid "Add Contribution guide"
msgstr "Ajouter un guide de contribution"
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr "Ajouter des Webhooks de groupe et GitLab Enterprise Edition."
+
msgid "Add License"
msgstr "Ajouter une licence"
@@ -96,11 +121,20 @@ msgstr "Ajoutez une clef SSH à votre profil pour pouvoir récupérer et pousser
msgid "Add new directory"
msgstr "Ajouter un nouveau dossier"
+msgid "AdminHealthPageLink|health page"
+msgstr "État des services"
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr "Tous"
+msgid "An error occurred. Please try again."
+msgstr "Une erreur est survenue. Merci de réessayer."
+
msgid "Appearance"
-msgstr ""
+msgstr "Apparence"
msgid "Applications"
msgstr "Applications"
@@ -109,13 +143,16 @@ 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é"
+msgstr "Êtes-vous sûr·e de vouloir supprimer ce pipeline programmé ?"
msgid "Are you sure you want to discard your changes?"
-msgstr "Êtes-vous sûr de vouloir annuler vos modifications ?"
+msgstr "Êtes-vous sûr·e de vouloir annuler vos modifications ?"
+
+msgid "Are you sure you want to leave this group?"
+msgstr "Êtes-vous sûr·e de vouloir quitter ce groupe ?"
msgid "Are you sure you want to reset registration token?"
-msgstr "Êtes-vous sûr de vouloir réinitialiser le jeton d’inscription ?"
+msgstr "Êtes-vous sûr·e de vouloir réinitialiser le jeton d’inscription ?"
msgid "Are you sure you want to reset the health check token?"
msgstr "Êtes-vous sûr de vouloir réinitialiser le jeton de bilan de santé ?"
@@ -124,91 +161,97 @@ msgid "Are you sure?"
msgstr "Êtes-vous certain ?"
msgid "Artifacts"
-msgstr ""
+msgstr "Artéfacts"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Attachez un fichier par glisser &amp; déposer ou %{upload_link}"
msgid "Authentication Log"
-msgstr ""
+msgstr "Journal d'authentification"
-msgid "Auto DevOps (Beta)"
-msgstr ""
-
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
-msgid "Auto DevOps documentation"
-msgstr ""
+msgid "Author"
+msgstr "Auteur"
msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Les applications de révision automatique et le déploiement automatique requièrent un nom de domaine et un %{kubernetes} pour fonctionner correctement."
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
-msgstr ""
+msgstr "Les applications de révision automatique et de déploiement automatique requièrent un nom de domaine pour fonctionner correctement."
msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Les applications de révision automatique et de déploiement automatique requièrent un %{kubernetes} pour fonctionner correctement."
-msgid "AutoDevOps|Learn more in the"
-msgstr ""
+msgid "AutoDevOps|Auto DevOps (Beta)"
+msgstr "Auto DevOps (Béta)"
+
+msgid "AutoDevOps|Auto DevOps documentation"
+msgstr "Documentation Auto DevOps"
+
+msgid "AutoDevOps|Enable in settings"
+msgstr "Activer dans les paramètres"
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr "AutoDevOps vous permet de construire, tester et déployer automatiquement votre application à partir d'une configuration CI/CD prédéfinie."
+
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
+msgstr "En savoir plus dans %{link_to_documentation}"
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr "Vous pouvez activer %{link_to_settings} pour ce projet."
msgid "Billing"
-msgstr ""
+msgstr "Facturation"
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr ""
+msgstr "%{group_name} est actuellement abonné au forfait %{plan_link}."
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr ""
+msgstr "La mise à niveau de certains abonnements n’est actuellement pas disponible."
msgid "BillingPlans|Current plan"
-msgstr ""
+msgstr "Abonnement actuel"
msgid "BillingPlans|Customer Support"
-msgstr ""
+msgstr "Support client"
+
+msgid "BillingPlans|Downgrade"
+msgstr "Retour à un forfait inférieur"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr ""
+msgstr "En savoir plus sur chaque abonnement en lisant nos %{faq_link}."
msgid "BillingPlans|Manage plan"
-msgstr ""
+msgstr "Gérer l'abonnement"
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr ""
+msgstr "Merci de contacter %{customer_support_link} à ce sujet."
msgid "BillingPlans|See all %{plan_name} features"
-msgstr ""
+msgstr "Voir toutes les fonctionnalités de l’abonnement %{plan_name}"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr ""
+msgstr "Ce groupe utilise l’abonnement associé à son groupe parent."
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr ""
+msgstr "Pour gérer le plan de ce groupe, visitez la section facturation de %{parent_billing_page_link}."
msgid "BillingPlans|Upgrade"
-msgstr ""
+msgstr "Mise à niveau"
msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr ""
+msgstr "Vous êtes actuellement abonné·e au forfait %{plan_link}."
msgid "BillingPlans|frequently asked questions"
-msgstr ""
+msgstr "Foire aux questions"
msgid "BillingPlans|monthly"
-msgstr ""
+msgstr "Mensuellement"
msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr ""
+msgstr "au prix annuel de %{price_per_year}"
msgid "BillingPlans|per user"
-msgstr ""
-
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
+msgstr "par utilisateur"
msgid "Branch"
msgid_plural "Branches"
@@ -216,7 +259,10 @@ msgstr[0] "Branche"
msgstr[1] "Branches"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
-msgstr "La branche <strong>%{branch_name}</strong> a été crée. Pour mettre en place le déploiement automatisé, sélectionnez un modèle de fichier Yaml pour l'intégration continue (CI) de GitLab, et validez les modifications. %{link_to_autodeploy_doc}"
+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 has changed"
+msgstr "La branche a été modifiée"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Rechercher la branche"
@@ -228,88 +274,88 @@ msgid "Branches"
msgstr "Branches"
msgid "Branches|Cant find HEAD commit for this branch"
-msgstr ""
+msgstr "Impossible de trouver la validation HEAD pour cette branche"
msgid "Branches|Compare"
-msgstr ""
+msgstr "Comparer"
msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
-msgstr ""
+msgstr "Supprimer toutes les branches qui ont été fusionnées dans '%{default_branch}'"
msgid "Branches|Delete branch"
-msgstr ""
+msgstr "Supprimer cette branche"
msgid "Branches|Delete merged branches"
-msgstr ""
+msgstr "Supprimer les branches fusionnées"
msgid "Branches|Delete protected branch"
-msgstr ""
+msgstr "Supprimer cette branche protégée"
msgid "Branches|Delete protected branch '%{branch_name}'?"
-msgstr ""
+msgstr "Supprimer la branche protégée '%{branch_name}' ?"
msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
-msgstr ""
+msgstr "La suppression de la branche '%{branch_name}' ne peut être annulée. Êtes-vous sûr ?"
msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
-msgstr ""
+msgstr "La suppression des branches fusionnées ne peut être annulée. Êtes-vous sûr ?"
msgid "Branches|Filter by branch name"
-msgstr ""
+msgstr "Filtrer par nom de branche"
msgid "Branches|Merged into %{default_branch}"
-msgstr ""
+msgstr "Fusionnée dans %{default_branch}"
msgid "Branches|New branch"
-msgstr ""
+msgstr "Nouvelle branche"
msgid "Branches|No branches to show"
-msgstr ""
+msgstr "Aucune branche à afficher"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
-msgstr ""
+msgstr "Une fois que vous aurez confirmé et cliqué sur %{delete_protected_branch}, cette action ne pourra pas être annulée ou restaurée."
msgid "Branches|Only a project master or owner can delete a protected branch"
-msgstr ""
+msgstr "Seulement un maître ou un propriétaire du projet peut supprimer une branche protégée"
msgid "Branches|Protected branches can be managed in %{project_settings_link}"
-msgstr ""
+msgstr "Les branches protégées peuvent être gérées dans %{project_settings_link}"
msgid "Branches|Sort by"
-msgstr ""
+msgstr "Trier par"
msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
+msgstr "Cette branche ne peut pas être mise à jour automatiquement car elle a dévié par rapport à son dépôt en amont."
msgid "Branches|The default branch cannot be deleted"
-msgstr ""
+msgstr "La branche par défaut ne peut pas être supprimée"
msgid "Branches|This branch hasn’t been merged into %{default_branch}."
-msgstr ""
+msgstr "Cette branche n'a pas été fusionnée dans %{default_branch}."
msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
-msgstr ""
+msgstr "Afin d'éviter de perdre des données, il est conseillé de fusionner cette branche avant de la supprimer."
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
-msgstr ""
+msgstr "Pour confirmer, veuillez saisir %{branch_name_confirmation} :"
msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
+msgstr "Pour rejeter les changements locaux et écraser la branche avec la version du dépôt en amont, veuillez la supprimer ici puis cliquez ci-dessus sur 'Mettre à jour maintenant'."
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
-msgstr ""
+msgstr "Vous êtes sur le point de supprimer définitivement la branche protégée %{branch_name}."
msgid "Branches|diverged from upstream"
-msgstr ""
+msgstr "a dévié du dépôt en amont"
msgid "Branches|merged"
-msgstr ""
+msgstr "fusionnée"
msgid "Branches|project settings"
-msgstr ""
+msgstr "paramètres du projet"
msgid "Branches|protected"
-msgstr ""
+msgstr "protégée"
msgid "Browse Directory"
msgstr "Parcourir le dossier"
@@ -332,20 +378,26 @@ msgstr "Intégration continu / Déploiement continu"
msgid "CI configuration"
msgstr "Configuration de l'intégration continue (CI)"
+msgid "CICD|Jobs"
+msgstr "Tâches"
+
msgid "Cancel"
msgstr "Annuler"
msgid "Cancel edit"
msgstr "Annuler modification"
+msgid "Change Weight"
+msgstr "Changer le poids"
+
msgid "ChangeTypeActionLabel|Pick into branch"
-msgstr "Sélectionner dans la branche"
+msgstr "Picorer dans la branche"
msgid "ChangeTypeActionLabel|Revert in branch"
msgstr "Défaire dans la branche"
msgid "ChangeTypeAction|Cherry-pick"
-msgstr "Sélectionner"
+msgstr "Picorer"
msgid "ChangeTypeAction|Revert"
msgstr "Défaire"
@@ -354,16 +406,19 @@ msgid "Changelog"
msgstr "Journal des modifications"
msgid "Charts"
-msgstr "Graphiques"
+msgstr "Statistiques"
msgid "Chat"
msgstr "Chat"
msgid "Cherry-pick this commit"
-msgstr "Sélectionner cette validation"
+msgstr "Picorer cette validation"
msgid "Cherry-pick this merge request"
-msgstr "Sélectionner cette demande de fusion"
+msgstr "Picorer cette demande de fusion"
+
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr "Choisissez quels groupes vous souhaitez répliquer sur le nœud secondaire. Laissez vide pour tous les répliquer."
msgid "CiStatusLabel|canceled"
msgstr "annulé"
@@ -419,6 +474,147 @@ msgstr "ignoré"
msgid "CiStatus|running"
msgstr "en cours"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr "CircuitBreaker API"
+
+msgid "Clone repository"
+msgstr "Cloner le dépôt"
+
+msgid "Close"
+msgstr "Fermer"
+
+msgid "Cluster"
+msgstr "Cluster"
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr "Un %{link_to_container_project} doit avoir été créé pour ce compte"
+
+msgid "ClusterIntegration|Cluster details"
+msgstr "Détails du cluster"
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr "Intégration du cluster"
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr "L'intégration du cluster est désactivée pour ce projet."
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr "L'intégration du cluster est activée pour ce projet."
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr "L'intégration de cluster est activée pour ce projet. La désactivation de cette intégration n’affectera pas votre cluster, il coupera temporairement la connexion de GitLab à celui-ci."
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr "Le cluster est en cours de création sur Google Container Engine…"
+
+msgid "ClusterIntegration|Cluster name"
+msgstr "Nom du cluster"
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr "Le cluster a été correctement créé sur Google Container Engine"
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr "Copier le nom du cluster"
+
+msgid "ClusterIntegration|Create cluster"
+msgstr "Créer le cluster"
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr "Créer un nouveau cluster sur Google Container Engine"
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr "Activer l’intégration du cluster"
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr "ID de projet Google Cloud Platform"
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr "Google Container Engine"
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr "Google Container Engine"
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr "En savoir plus sur %{link_to_documentation}"
+
+msgid "ClusterIntegration|Machine type"
+msgstr "Type de machine"
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr "Assurez-vous que votre compte %{link_to_requirements} pour créer des clusters"
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr "Gérer l’intégration du cluster sur votre projet GitLab"
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr "Gérer votre cluster en visitant le lien %{link_gke}"
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr "Nombre de nœuds"
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr "Veuillez vous assurer que votre compte Google répond aux exigences suivantes : "
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr "Espace de noms du projet (facultatif, unique)"
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr "Lire notre %{link_to_help_page} sur l’intégration d’un cluster."
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr "Retirer l’intégration du cluster"
+
+msgid "ClusterIntegration|Remove integration"
+msgstr "Retirer l’intégration"
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr "Supprimer l'intégration du cluster supprimera sa configuration que vous avez ajoutée pour ce projet. Cela ne supprimera pas votre projet."
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr "Voir et modifier les détails de votre cluster"
+
+msgid "ClusterIntegration|See machine types"
+msgstr "Voir les types de machine"
+
+msgid "ClusterIntegration|See your projects"
+msgstr "Voir vos projets"
+
+msgid "ClusterIntegration|See zones"
+msgstr "Voir les zones"
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr "Un problème est survenu de notre côté."
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr "Un problème est survenu lors de la création de votre cluster sur Google Container Engine."
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr "Activer/désactiver le cluster"
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr "Avec un cluster associé à ce projet, vous pouvez utiliser des applications de revue, déployer vos applications, exécuter vos pipelines et bien plus encore, de manière très simple."
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr "Votre compte doit disposer de %{link_to_container_engine}"
+
+msgid "ClusterIntegration|Zone"
+msgstr "Zone"
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr "Accéder à Google Container Engine"
+
+msgid "ClusterIntegration|cluster"
+msgstr "cluster"
+
+msgid "ClusterIntegration|help page"
+msgstr "page d’aide"
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr "répond aux exigences"
+
+msgid "ClusterIntegration|properly configured"
+msgstr "correctement configuré"
+
msgid "Comments"
msgstr "Commentaires"
@@ -427,6 +623,14 @@ msgid_plural "Commits"
msgstr[0] "Validation"
msgstr[1] "Validations"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Commit Message"
+msgstr "Message de validation"
+
msgid "Commit duration in minutes for last 30 commits"
msgstr "Durée des 30 derniers pipelines en minutes"
@@ -454,14 +658,65 @@ msgstr "Validé par"
msgid "Compare"
msgstr "Comparer"
+msgid "Container Registry"
+msgstr "Registre de conteneur"
+
+msgid "ContainerRegistry|Created"
+msgstr "Créé"
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr "D’abord inscrivez-vous au registre de conteneur GitLab à l’aide de votre nom d’utilisateur GitLab et de votre mot de passe. Si vous avez %{link_2fa} vous devrez utiliser un %{link_token} :"
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr "GitLab prend en charge jusqu'à 3 niveaux de noms d’image. Les exemples d’images suivants sont valides pour votre projet :"
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr "Comment utiliser le registre de conteneur"
+
+msgid "ContainerRegistry|Learn more about"
+msgstr "En savoir plus sur"
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr "Aucune étiquettes dans le registre de conteneur pour l’image de ce conteneur."
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr "Une fois identifié·e, vous êtes libre de créer et télécharger une image de conteneur en utilisant les commandes courantes de %{build} et %{push}"
+
+msgid "ContainerRegistry|Remove repository"
+msgstr "Supprimer le dépôt"
+
+msgid "ContainerRegistry|Remove tag"
+msgstr "Supprimer l’étiquette"
+
+msgid "ContainerRegistry|Size"
+msgstr "Taille"
+
+msgid "ContainerRegistry|Tag"
+msgstr "Étiquette"
+
+msgid "ContainerRegistry|Tag ID"
+msgstr "ID d‘étiquette"
+
+msgid "ContainerRegistry|Use different image names"
+msgstr "Utilisez des noms d’images différents"
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr "Avec le registre de conteneur Docker intégré à GitLab, chaque projet peut avoir son propre espace pour stocker ses images Docker."
+
msgid "Contribution guide"
msgstr "Guilde de contribution"
msgid "Contributors"
msgstr "Contributeurs"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr "Contrôler la concurrence maximale des remplacements de fichier-joint LFS pour ce nœud secondaire"
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr "Contrôler la concurrence maximale des remplacements de dépôt pour ce nœud secondaire"
+
msgid "Copy SSH public key to clipboard"
-msgstr ""
+msgstr "Copier la clé publique SSH dans le presse-papier"
msgid "Copy URL to clipboard"
msgstr "Copier l'URL dans le presse-papier"
@@ -472,9 +727,6 @@ msgstr "Copier le SHA de la validation"
msgid "Create New Directory"
msgstr "Créer un nouveau dossier"
-msgid "Create a new branch"
-msgstr "Créer une nouvelle branche"
-
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "Créer un jeton d’accès personnel pour votre compte afin de récupérer ou pousser par %{protocol}."
@@ -484,9 +736,21 @@ msgstr "Créer un dossier"
msgid "Create empty bare repository"
msgstr "Créer un dépôt vide"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "Créer une demande de fusion"
+msgid "Create new branch"
+msgstr "Créer une nouvelle branche"
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "Créer nouveau..."
@@ -494,7 +758,7 @@ msgid "CreateNewFork|Fork"
msgstr "Fourcher"
msgid "CreateTag|Tag"
-msgstr "Étiquette"
+msgstr "Tag"
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "Créer un jeton d'accès personnel"
@@ -521,7 +785,7 @@ msgid "CycleAnalyticsStage|Code"
msgstr "Code"
msgid "CycleAnalyticsStage|Issue"
-msgstr "Incident"
+msgstr "Ticket"
msgid "CycleAnalyticsStage|Plan"
msgstr "Planification"
@@ -538,6 +802,12 @@ msgstr "Pré-production"
msgid "CycleAnalyticsStage|Test"
msgstr "Test"
+msgid "DashboardProjects|All"
+msgstr "Tous"
+
+msgid "DashboardProjects|Personal"
+msgstr "Personnels"
+
msgid "Define a custom pattern with cron syntax"
msgstr "Définir un schéma personnalisé avec une syntaxe Cron"
@@ -555,6 +825,9 @@ msgstr "Clés de déploiement"
msgid "Description"
msgstr "Description"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr "Les modèles de description permettent de définir des modèles spécifiques au contexte pour les champs de description de tickets et demandes de fusion pour votre projet."
+
msgid "Details"
msgstr "Détails"
@@ -564,6 +837,12 @@ msgstr "Nom du dossier"
msgid "Discard changes"
msgstr "Supprimer les modifications"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr "Passer l’introduction Cycle Analytics"
+
+msgid "Dismiss Merge Request promotion"
+msgstr "Rejeter la promotion de la demande de fusion"
+
msgid "Don't show again"
msgstr "Ne plus montrer"
@@ -603,9 +882,6 @@ msgstr "Éditer le pipeline programmé %{id}"
msgid "Emails"
msgstr "Courriels"
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr "Aucun filtre"
@@ -613,7 +889,7 @@ msgid "EventFilterBy|Filter by comments"
msgstr "Filtrer par commentaires"
msgid "EventFilterBy|Filter by issue events"
-msgstr "Filtrer par événements d'incident"
+msgstr "Filtrer par événements de ticket"
msgid "EventFilterBy|Filter by merge events"
msgstr "Filtrer par événements de fusion"
@@ -625,16 +901,19 @@ msgid "EventFilterBy|Filter by team"
msgstr "Filtrer par équipe"
msgid "Every day (at 4:00am)"
-msgstr "Chaque jour (à 4:00 du matin)"
+msgstr "Chaque jour (à 4h00 du matin)"
msgid "Every month (on the 1st at 4:00am)"
msgstr "Chaque mois (le 1er à 4:00 du matin)"
msgid "Every week (Sundays at 4:00am)"
-msgstr "Chaque semaine (dimanche à 4:00 du matin)"
+msgstr "Chaque semaine (dimanche à 4h00 du matin)"
msgid "Explore projects"
-msgstr ""
+msgstr "Explorer les projets"
+
+msgid "Explore public groups"
+msgstr "Explorer les groupes publics"
msgid "Failed to change the owner"
msgstr "Échec du changement de propriétaire"
@@ -642,6 +921,9 @@ msgstr "Échec du changement de propriétaire"
msgid "Failed to remove the pipeline schedule"
msgstr "Échec de la suppression du pipeline programmé"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "Fichiers"
@@ -668,8 +950,14 @@ msgstr[1] "Fourches"
msgid "ForkedFromProjectPath|Forked from"
msgstr "Fourché depuis"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr "Fourché depuis %{project_name} (supprimé)"
+
+msgid "Format"
+msgstr "Format"
+
msgid "From issue creation until deploy to production"
-msgstr "Depuis la création de l'incident jusqu'au déploiement en production"
+msgstr "Depuis la création du ticket 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"
@@ -678,13 +966,25 @@ msgid "GPG Keys"
msgstr "Clés GPG"
msgid "Geo Nodes"
-msgstr ""
+msgstr "NÅ“uds Geo"
+
+msgid "Geo|File sync capacity"
+msgstr "Capacité de synchronisation de fichier"
+
+msgid "Geo|Groups to replicate"
+msgstr "Groupes à répliquer"
+
+msgid "Geo|Repository sync capacity"
+msgstr "Capacité de synchronisation du dépôt"
+
+msgid "Geo|Select groups to replicate."
+msgstr "Sélectionner les groupes à répliquer."
msgid "Git storage health information has been reset"
msgstr "Les informations de santé du stockage Git ont été réinitialisées"
msgid "GitLab Runner section"
-msgstr "Section de Runner GitLab"
+msgstr "Section de l'Exécuteur GitLab"
msgid "Go to your fork"
msgstr "Aller à votre fourche"
@@ -692,35 +992,83 @@ msgstr "Aller à votre fourche"
msgid "GoToYourFork|Fork"
msgstr "Fourche"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr "L’authentification Google n’est pas %{link_to_documentation}. Demandez à votre administrateur GitLab si vous souhaitez utiliser ce service."
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
-msgstr ""
+msgstr "Empêcher le partage d'un projet de %{group} avec d'autres groupes"
msgid "GroupSettings|Share with group lock"
-msgstr ""
+msgstr "Verrou de partage avec un groupe"
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
-msgstr ""
+msgstr "Ce paramètre est appliqué sur %{ancestor_group} et a été modifié dans ce sous-groupe."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Ce paramètre est appliqué sur %{ancestor_group}. Pour partager des projets dans ce groupe avec un autre groupe, demander au propriétaire de modifier le paramètre ou %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Ce paramètre s’applique sur %{ancestor_group}. Vous pouvez modifier le paramètre ou le %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
-msgstr ""
+msgstr "Ce paramètre s’appliquera à tous les sous-groupes, sauf s’il est modifié par un propriétaire du groupe. Les groupes déjà liés au projet continueront d’y avoir accès à moins d’être retirés manuellement."
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
-msgstr ""
+msgstr "ne peut pas être désactivé lorsque le groupe parent « Verrou de partage avec un groupe » est activé, sauf pour le propriétaire du groupe parent"
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
-msgstr ""
+msgstr "supprimer le verrou de partage avec un groupe pour %{ancestor_group_name}"
+
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr "Un groupe est une collection de plusieurs projets."
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr "Si vous organisez vos projets au sein d’un groupe, il fonctionnera comme un dossier."
+
+msgid "GroupsEmptyState|No groups found"
+msgstr "Aucun groupe trouvé"
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr "Vous pouvez gérer les autorisations des membres de votre groupe et accéder à chacun de ses projets."
+
+msgid "GroupsTreeRole|as"
+msgstr "comme"
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr "Êtes-vous sûr·e de vouloir quitter le groupe « ${this.group.fullName} » ?"
+
+msgid "GroupsTree|Create a project in this group."
+msgstr "Créez un projet dans ce groupe."
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr "Créer un sous-groupe de ce groupe."
+
+msgid "GroupsTree|Edit group"
+msgstr "Modifier le groupe"
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr "Impossible de quitter le groupe. Veuillez vous assurer que vous n’êtes pas seul·e propriétaire."
+
+msgid "GroupsTree|Filter by name..."
+msgstr "Filtrer par nom…"
+
+msgid "GroupsTree|Leave this group"
+msgstr "Quitter ce groupe"
+
+msgid "GroupsTree|Loading groups"
+msgstr "Chargement des groupes"
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr "Désolé, aucun groupe ne correspond à vos critères de recherche"
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr "Désolé, aucun groupe ni projet ne correspond à vos critères de recherche"
msgid "Health Check"
-msgstr "Bilan de santé"
+msgstr "État des services"
msgid "Health information can be retrieved from the following endpoints. More information is available"
-msgstr "Des informations de santé peuvent être récupérées depuis les adresses suivantes. Plus d’informations"
+msgstr "L’état des service peut être récupéré depuis les adresses suivantes. Plus d’information disponible."
msgid "HealthCheck|Access token is"
msgstr "Le jeton d’accès est"
@@ -734,14 +1082,37 @@ msgstr "Aucun problème détecté"
msgid "HealthCheck|Unhealthy"
msgstr "En mauvaise santé"
+msgid "History"
+msgstr "Historique"
+
msgid "Housekeeping successfully started"
msgstr "Maintenance démarrée avec succès"
msgid "Import repository"
msgstr "Importer un dépôt"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr "Améliorer le tableau de tickets avec GitLab Entreprise Edition."
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr "Améliorer la gestion des tickets avec les poids de ticket et GitLab Entreprise Edition."
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr "Améliorer la recherche avec la Recherche Globale Avancée et GitLab Enterprise Edition."
+
msgid "Install a Runner compatible with GitLab CI"
-msgstr "Installez un Runner compatible avec l'intégration continue de GitLab"
+msgstr "Installez un Exécuteur compatible avec l'intégration continue de GitLab"
+
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] "Instance"
+msgstr[1] "Instances"
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr "Interne - Le groupe ainsi que tous les projets internes sont accessibles pour n’importe quel·le utilisa·teur·trice connecté·e."
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr "Interne - Le projet est accessible pour n’importe quel·le utilisa·teur·trice connecté·e."
msgid "Interval Pattern"
msgstr "Schéma d’intervalle"
@@ -749,14 +1120,23 @@ msgstr "Schéma d’intervalle"
msgid "Introducing Cycle Analytics"
msgstr "Introduction à l'analyseur de cycle"
+msgid "Issue board focus mode"
+msgstr "Mode focus du tableau de tickets"
+
+msgid "Issue boards with milestones"
+msgstr "Tableaux des tickets avec leurs jalons"
+
msgid "Issue events"
-msgstr "Événements de l'incident"
+msgstr "Événements du ticket"
-msgid "Issues"
-msgstr "Incidents"
+msgid "IssueBoards|Board"
+msgstr "Tableau"
-msgid "Jobs"
-msgstr ""
+msgid "IssueBoards|Boards"
+msgstr "Tableaux"
+
+msgid "Issues"
+msgstr "Tickets"
msgid "LFSStatus|Disabled"
msgstr "Désactivé"
@@ -765,7 +1145,7 @@ msgid "LFSStatus|Enabled"
msgstr "Activé"
msgid "Labels"
-msgstr ""
+msgstr "Labels"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -775,12 +1155,21 @@ msgstr[1] "Les derniers %d jours"
msgid "Last Pipeline"
msgstr "Dernier pipeline"
-msgid "Last Update"
-msgstr "Dernière mise à jour"
-
msgid "Last commit"
msgstr "Dernière validation"
+msgid "Last edited %{date}"
+msgstr "Dernière modification le %{date}"
+
+msgid "Last edited by %{name}"
+msgstr "Dernière modification par %{name}"
+
+msgid "Last update"
+msgstr "Dernière mise à jour"
+
+msgid "Last updated"
+msgstr "Dernière mise à jour"
+
msgid "LastPushEvent|You pushed to"
msgstr "Vous avez poussé sur"
@@ -793,6 +1182,9 @@ msgstr "En apprendre plus dans le"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "documentation concernant la programmation de pipeline"
+msgid "Leave"
+msgstr "Quitter"
+
msgid "Leave group"
msgstr "Quitter le groupe"
@@ -800,15 +1192,27 @@ msgid "Leave project"
msgstr "Quitter le projet"
msgid "License"
-msgstr ""
+msgstr "Licence"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "Limiter l'affichage au plus à %d évènement"
msgstr[1] "Limiter l'affichage au plus à %d évènements"
+msgid "Lock"
+msgstr "Verrouiller"
+
+msgid "Locked"
+msgstr "Verrouillé"
+
msgid "Locked Files"
-msgstr ""
+msgstr "Fichiers verrouillés"
+
+msgid "Login"
+msgstr "Se connecter"
+
+msgid "Maximum git storage failures"
+msgstr "Nombre maximum d’échecs du stockage git"
msgid "Median"
msgstr "Médian"
@@ -823,13 +1227,13 @@ msgid "Merge events"
msgstr "Événements de fusion"
msgid "Merge request"
-msgstr ""
+msgstr "Demande de fusion"
msgid "Messages"
msgstr "Messages"
msgid "MissingSSHKeyWarningLink|add an SSH key"
-msgstr "ajouter une clef SSH"
+msgstr "ajouter une clé SSH"
msgid "Monitoring"
msgstr "Surveillance"
@@ -837,10 +1241,16 @@ msgstr "Surveillance"
msgid "More information is available|here"
msgstr "ici"
+msgid "Multiple issue boards"
+msgstr "Multiple tableaux de tickets"
+
+msgid "New Cluster"
+msgstr "Nouveau cluster"
+
msgid "New Issue"
msgid_plural "New Issues"
-msgstr[0] "Nouvel incident"
-msgstr[1] "Nouveaux incidents"
+msgstr[0] "Nouveau ticket"
+msgstr[1] "Nouveaux tickets"
msgid "New Pipeline Schedule"
msgstr "Nouveau pipeline programmé"
@@ -852,22 +1262,34 @@ msgid "New directory"
msgstr "Nouveau dossier"
msgid "New file"
-msgstr "Nouveau Fichier"
+msgstr "Nouveau fichier"
+
+msgid "New group"
+msgstr "Nouveau groupe"
msgid "New issue"
-msgstr "Nouvel incident"
+msgstr "Nouveau ticket"
msgid "New merge request"
msgstr "Nouvelle demande de fusion"
+msgid "New project"
+msgstr "Nouveau projet"
+
msgid "New schedule"
msgstr "Nouveau programme"
msgid "New snippet"
msgstr "Nouvel extrait de code"
+msgid "New subgroup"
+msgstr "Nouveau sous-groupe"
+
msgid "New tag"
-msgstr "Nouvelle étiquette"
+msgstr "Nouveau tag"
+
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr "Aucune image de conteneur stockée pour ce projet. Ajoutez en une en suivant les instructions ci-dessous."
msgid "No repository"
msgstr "Pas de dépôt"
@@ -875,6 +1297,9 @@ msgstr "Pas de dépôt"
msgid "No schedules"
msgstr "Aucun programme"
+msgid "None"
+msgstr "Aucun·e"
+
msgid "Not available"
msgstr "Indisponible"
@@ -885,7 +1310,7 @@ msgid "Notification events"
msgstr "Événement de notifications"
msgid "NotificationEvent|Close issue"
-msgstr "Clore l'incident"
+msgstr "Clore le ticket"
msgid "NotificationEvent|Close merge request"
msgstr "Clore la demande de fusion"
@@ -897,7 +1322,7 @@ msgid "NotificationEvent|Merge merge request"
msgstr "Fusionner le demande de fusion"
msgid "NotificationEvent|New issue"
-msgstr "Nouvel incident"
+msgstr "Nouveau ticket"
msgid "NotificationEvent|New merge request"
msgstr "Nouvelle demande de fusion"
@@ -906,13 +1331,13 @@ msgid "NotificationEvent|New note"
msgstr "Nouvelle note"
msgid "NotificationEvent|Reassign issue"
-msgstr "Réassigner l'incident"
+msgstr "Réassigner le ticket"
msgid "NotificationEvent|Reassign merge request"
msgstr "Réassigner la demande de fusion"
msgid "NotificationEvent|Reopen issue"
-msgstr "Ré-ouvrir l'incident"
+msgstr "Ré-ouvrir le ticket"
msgid "NotificationEvent|Successful pipeline"
msgstr "Pipeline réussi"
@@ -938,12 +1363,24 @@ msgstr "Surveillé"
msgid "Notifications"
msgstr "Notifications"
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtre"
+msgid "Only project members can comment."
+msgstr "Seuls les membres du projet peuvent commenter."
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Ouvert"
+msgid "Opens in a new window"
+msgstr "Ouvrir dans une nouvelle fenêtre"
+
msgid "Options"
msgstr "Paramètres"
@@ -954,20 +1391,23 @@ msgid "Owner"
msgstr "Propriétaire"
msgid "Pagination|Last »"
-msgstr ""
+msgstr "Dernière »"
msgid "Pagination|Next"
-msgstr ""
+msgstr "Suivante"
msgid "Pagination|Prev"
-msgstr ""
+msgstr "Précédente"
msgid "Pagination|« First"
-msgstr ""
+msgstr "« Première"
msgid "Password"
msgstr "Mot de Passe"
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr "Les personnes sans autorisation ne recevront jamais de notifications et ne pourront pas commenter."
+
msgid "Pipeline"
msgstr "Pipeline"
@@ -981,7 +1421,7 @@ msgid "Pipeline Schedules"
msgstr "Programmations de pipeline"
msgid "Pipeline quota"
-msgstr ""
+msgstr "Quota de pipeline"
msgid "PipelineCharts|Failed:"
msgstr "Échecs : "
@@ -1047,13 +1487,13 @@ msgid "Pipelines charts"
msgstr "Graphique des pipelines"
msgid "Pipelines for last month"
-msgstr "Pipelines pour le dernier mois"
+msgstr "Pipelines du dernier mois"
msgid "Pipelines for last week"
-msgstr "Pipelines pour la dernière semaine"
+msgstr "Pipelines de la semaine dernière"
msgid "Pipelines for last year"
-msgstr "Pipelines pour la dernière année"
+msgstr "Pipelines de l’année dernière"
msgid "Pipeline|all"
msgstr "Tous"
@@ -1070,8 +1510,53 @@ msgstr "avec les étapes"
msgid "Preferences"
msgstr "Préférences"
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr "Privé - L’accès au projet doit être autorisé explicitement pour chaque utilisa·teur·trice."
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr "Privé - Le groupe ainsi que ses projets ne sont accessibles qu’à ses membres."
+
msgid "Profile"
-msgstr ""
+msgstr "Profil"
+
+msgid "Profiles|Account scheduled for removal."
+msgstr "Compte programmé pour sa suppression."
+
+msgid "Profiles|Delete Account"
+msgstr "Supprimer le compte"
+
+msgid "Profiles|Delete account"
+msgstr "Supprimer le compte"
+
+msgid "Profiles|Delete your account?"
+msgstr "Supprimer votre compte ?"
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr "Supprimer un compte aura les conséquences suivantes :"
+
+msgid "Profiles|Invalid password"
+msgstr "Mot de passe incorrect"
+
+msgid "Profiles|Invalid username"
+msgstr "Nom d’utilisateur incorrect"
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr "Saisissez votre %{confirmationValue} pour confirmer :"
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr "Vous n’avez pas les autorisations suffisantes pour supprimer cet·te utilisa·teur·trice."
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr "Vous devez transférer la propriété ou supprimer ces groupes avant de pouvoir supprimer votre compte."
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr "Votre compte est actuellement propriétaire des groupes suivants :"
+
+msgid "Profiles|your account"
+msgstr "votre compte"
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr "Le projet “%{project_name}†est en train d’être supprimé."
msgid "Project '%{project_name}' queued for deletion."
msgstr "Projet '%{project_name}' en attente de suppression."
@@ -1082,9 +1567,6 @@ msgstr "Projet '%{project_name}' créé avec succès."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Projet '%{project_name}' mis à jour avec succès."
-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."
@@ -1125,11 +1607,32 @@ msgid "ProjectLifecycle|Stage"
msgstr "Étape"
msgid "ProjectNetworkGraph|Graph"
-msgstr "Graphique "
+msgstr "Graphes"
-msgid "ProjectsDropdown|Frequently visited"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr "Contactez un administrateur pour modifier ce paramètre."
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr "Seules les validations signées peuvent être poussées sur ce dépôt."
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr "Ce paramètre est appliqué au niveau du serveur et peut être modifié par un administrateur."
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr "Ce paramètre est appliqué au niveau du serveur mais il a été modifié pour ce projet."
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr "Ce paramètre s’appliquera à tous les projets à moins qu’un administrateur ne le modifie."
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
msgstr ""
+msgid "Projects"
+msgstr "Projets"
+
+msgid "ProjectsDropdown|Frequently visited"
+msgstr "Fréquemment visité"
+
msgid "ProjectsDropdown|Loading projects"
msgstr "Chargement des projets"
@@ -1140,7 +1643,7 @@ msgid "ProjectsDropdown|Search your projects"
msgstr "Chercher dans vos projets"
msgid "ProjectsDropdown|Something went wrong on our end."
-msgstr ""
+msgstr "Un problème est survenu de notre côté."
msgid "ProjectsDropdown|Sorry, no projects matched your search"
msgstr "Désolé, aucun projet ne correspond à votre recherche"
@@ -1148,12 +1651,21 @@ msgstr "Désolé, aucun projet ne correspond à votre recherche"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Cette fonctionnalité requiert le support du localStorage par votre navigateur"
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr "Public - Le groupe ainsi que n’importe quel projet public est accessible sans authentification."
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr "Public - Le projet est accessible sans aucune authentification."
+
msgid "Push Rules"
-msgstr ""
+msgstr "Règles de poussée"
msgid "Push events"
msgstr "Évènements de poussée"
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Lire plus"
@@ -1164,19 +1676,19 @@ msgid "RefSwitcher|Branches"
msgstr "Branches"
msgid "RefSwitcher|Tags"
-msgstr "Étiquettes"
+msgstr "Tags"
msgid "Registry"
-msgstr ""
+msgstr "Registre"
msgid "Related Commits"
msgstr "Validations liés"
msgid "Related Deployed Jobs"
-msgstr "Tâches de déploiement liés"
+msgstr "Tâches de déploiement liées"
msgid "Related Issues"
-msgstr "Incidents liés"
+msgstr "Tickets liés"
msgid "Related Jobs"
msgstr "Tâches liées"
@@ -1206,17 +1718,23 @@ msgid "Reset health check access token"
msgstr "Réinitialiser le jeton d’accès au bilan de santé"
msgid "Reset runners registration token"
-msgstr "Réinitialiser le jeton d’inscription des Runners"
+msgstr "Réinitialiser le jeton d’inscription des exécuteurs"
msgid "Revert this commit"
-msgstr "Annuler cette validation"
+msgstr "Défaire cette validation"
msgid "Revert this merge request"
-msgstr "Annuler cette demande de fusion"
+msgstr "Défaire cette demande de fusion"
msgid "SSH Keys"
msgstr "Clés SSH"
+msgid "Save"
+msgstr ""
+
+msgid "Save changes"
+msgstr "Enregistrer les modifications"
+
msgid "Save pipeline schedule"
msgstr "Sauvegarder le pipeline programmé"
@@ -1224,7 +1742,7 @@ msgid "Schedule a new pipeline"
msgstr "Programmer un nouveau pipeline"
msgid "Schedules"
-msgstr ""
+msgstr "Programmes"
msgid "Scheduling Pipelines"
msgstr "Programmer des pipelines"
@@ -1232,15 +1750,21 @@ msgstr "Programmer des pipelines"
msgid "Search branches and tags"
msgstr "Rechercher dans les branches et les étiquettes"
+msgid "Seconds before reseting failure information"
+msgstr "Nombre de secondes avant de réinitialiser les informations d’échec"
+
+msgid "Seconds to wait after a storage failure"
+msgstr "Nombre de secondes d'attente après un échec de stockage"
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr "Nombre de secondes d’attente pour un essai d'accès au stockage"
+
msgid "Select Archive Format"
msgstr "Sélectionnez le format de l'archive"
msgid "Select a timezone"
msgstr "Sélectionnez un fuseau horaire"
-msgid "Select existing branch"
-msgstr "Sélectionnez une branche existante"
-
msgid "Select target branch"
msgstr "Sélectionnez une branche cible"
@@ -1266,10 +1790,10 @@ msgid "Settings"
msgstr "Paramètres"
msgid "Show parent pages"
-msgstr ""
+msgstr "Afficher les pages parentes"
msgid "Show parent subgroups"
-msgstr ""
+msgstr "Afficher les sous-groupes parents"
msgid "Showing %d event"
msgid_plural "Showing %d events"
@@ -1279,101 +1803,116 @@ msgstr[1] "Affichage de %d évènements"
msgid "Snippets"
msgstr "Extraits de code"
+msgid "Something went wrong on our end."
+msgstr "Une erreur est survenue de notre côté."
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr "Quelque chose ne s'est pas bien passé en essayant de changer l’état de verrouillage de cet ${this.issuableDisplayName(this.issuableType)}"
+
+msgid "Something went wrong while fetching the projects."
+msgstr "Une erreur s'est produite lors de la récupération des projets."
+
+msgid "Something went wrong while fetching the registry list."
+msgstr "Une erreur s'est produite lors de la récupération de la liste du registre."
+
+msgid "Sort by"
+msgstr "Trier par"
+
msgid "SortOptions|Access level, ascending"
-msgstr ""
+msgstr "Niveau d’accès, croissant"
msgid "SortOptions|Access level, descending"
-msgstr ""
+msgstr "Niveau d’accès, decroissant"
msgid "SortOptions|Created date"
-msgstr ""
+msgstr "Date de création"
msgid "SortOptions|Due date"
-msgstr ""
+msgstr "Date d'échéance"
msgid "SortOptions|Due later"
-msgstr ""
+msgstr "Échéance lointaine"
msgid "SortOptions|Due soon"
-msgstr ""
+msgstr "Échéance proche"
msgid "SortOptions|Label priority"
-msgstr ""
+msgstr "Priorité du label"
msgid "SortOptions|Largest group"
-msgstr ""
+msgstr "Taille de groupe"
msgid "SortOptions|Largest repository"
-msgstr ""
+msgstr "Taille de dépôt"
msgid "SortOptions|Last created"
-msgstr ""
+msgstr "Créé récemment"
msgid "SortOptions|Last joined"
-msgstr ""
+msgstr "Rejoint récemment"
msgid "SortOptions|Last updated"
-msgstr ""
+msgstr "Mise à jour récemment"
msgid "SortOptions|Least popular"
-msgstr ""
+msgstr "Moins populaire"
msgid "SortOptions|Less weight"
-msgstr ""
+msgstr "Poids croissant"
msgid "SortOptions|Milestone"
-msgstr ""
+msgstr "Jalon"
msgid "SortOptions|Milestone due later"
-msgstr ""
+msgstr "Jalon avec une échéance lointaine"
msgid "SortOptions|Milestone due soon"
-msgstr ""
+msgstr "Jalon avec une échéance proche"
msgid "SortOptions|More weight"
-msgstr ""
+msgstr "Poids décroissant"
msgid "SortOptions|Most popular"
-msgstr ""
+msgstr "Populaire"
msgid "SortOptions|Name"
-msgstr ""
+msgstr "Nom"
msgid "SortOptions|Name, ascending"
-msgstr ""
+msgstr "Nom, par ordre croissant"
msgid "SortOptions|Name, descending"
-msgstr ""
+msgstr "Nom, par ordre décroissant"
msgid "SortOptions|Oldest created"
-msgstr ""
+msgstr "Créé depuis longtemps"
msgid "SortOptions|Oldest joined"
-msgstr ""
+msgstr "Rejoint depuis longtemps"
msgid "SortOptions|Oldest sign in"
-msgstr ""
+msgstr "Authentifié·e depuis longtemps"
msgid "SortOptions|Oldest updated"
-msgstr ""
+msgstr "Mise à jour depuis longtemps"
msgid "SortOptions|Popularity"
-msgstr ""
+msgstr "Popularité"
msgid "SortOptions|Priority"
-msgstr ""
+msgstr "Priorité"
msgid "SortOptions|Recent sign in"
-msgstr ""
+msgstr "Authentifié·e récemment"
msgid "SortOptions|Start later"
-msgstr ""
+msgstr "Commence plus tard"
msgid "SortOptions|Start soon"
-msgstr ""
+msgstr "Commence bientôt"
msgid "SortOptions|Weight"
-msgstr ""
+msgstr "Poids"
msgid "Source code"
msgstr "Code source"
@@ -1382,33 +1921,39 @@ msgid "Spam Logs"
msgstr "Journaux des messages indésirables"
msgid "Specify the following URL during the Runner setup:"
-msgstr "Spécifiez l’URL suivante lors de la configuration du Runner :"
+msgstr "Spécifiez l’URL suivante lors de la configuration de l'Exécuteur :"
msgid "StarProject|Star"
-msgstr "S'abonner"
+msgstr "Mettre en favori"
msgid "Starred projects"
-msgstr ""
+msgstr "Projets favoris"
msgid "Start a %{new_merge_request} with these changes"
msgstr "Créer une %{new_merge_request} avec ces changements"
msgid "Start the Runner!"
-msgstr "Démarrer le Runner !"
+msgstr "Démarrer l'Exécuteur !"
+
+msgid "Subgroups"
+msgstr "Sous-groupes"
+
+msgid "Subscribe"
+msgstr ""
msgid "Switch branch/tag"
msgstr "Changer de branche / d'étiquette"
msgid "System Hooks"
-msgstr ""
+msgstr "Crochets système"
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "Étiquette"
-msgstr[1] "Étiquettes"
+msgstr[1] "Tags"
msgid "Tags"
-msgstr "Étiquettes"
+msgstr "Tags"
msgid "Target Branch"
msgstr "Branche cible"
@@ -1416,6 +1961,15 @@ msgstr "Branche cible"
msgid "Team"
msgstr "Équipe"
+msgid "Thanks! Don't show me this again"
+msgstr "Merci de ne plus afficher ce message"
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr "La Recherche Globale Avancée de GitLab est un outils qui vous fait gagner du temps. Au lieu de créer du code similaire et perdre du temps, vous pouvez maintenant chercher dans le code d'autres équipes pour vous aider sur votre projet."
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+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 "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."
@@ -1426,22 +1980,31 @@ 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."
+msgstr "L'étape des tickets montre le temps nécessaire entre la création d'un ticket et son assignation à un jalon, ou son ajout à une liste d'un tableau de tickets. Commencez par créer des tickets pour voir des données pour cette étape."
+
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr "Nombre d’échecs avant que GitLab n’empêche tout accès au stockage. Ce nombre d’échecs peut être réinitialisé dans l’interface d’administration : %{link_to_health_page} ou en suivant le %{api_documentation_link}."
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é."
+msgstr "Les pipelines programmés exécutent des pipelines dans le futur, de façon répétée, pour les branches et tags spécifiées. Ces pipelines programmés héritent d’un accès partiel au projet basé sur l’utilisa·teur·trice qui leurs est associé·e."
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."
+msgstr "L’étape de mise en production montre le temps nécessaire entre la création d’un ticket 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 utilisa·teur·trice authentifié·e."
msgid "The project can be accessed without any authentication."
msgstr "Votre projet peut être accédé sans aucune authentification."
@@ -1458,6 +2021,12 @@ msgstr "L’étape de pré-production indique le temps entre la fusion de la DF
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 in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr "Délai en secondes pendant lequel GitLab gardera les informations d’échec. Si aucun échec ne survient pendant ce délai, les informations de ce montage seront réinitialisées."
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr "Temps en secondes pendant lequel GitLab essaiera d’accéder au stockage. Après ce délai, une erreur d’expiration d’attente sera déclenchée."
+
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."
@@ -1467,17 +2036,32 @@ msgstr "La valeur située au point médian d’une série de valeur observée. C
msgid "There are problems accessing Git storage: "
msgstr "Il y a des difficultés à accéder aux données Git : "
-msgid "This is the author's first Merge Request to this project. Handle with care."
-msgstr ""
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr "Cette branche a changé depuis le début de l’édition. Souhaitez-vous créer une nouvelle branche ?"
+
+msgid "This is a confidential issue."
+msgstr "Ce ticket est confidentiel."
+
+msgid "This is the author's first Merge Request to this project."
+msgstr "C’est la première demande de fusion de cet auteur pour ce projet."
+
+msgid "This issue is confidential and locked."
+msgstr "Ce ticket est confidentiel et verrouillé."
+
+msgid "This issue is locked."
+msgstr "Ce ticket est verrouillé."
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."
+msgstr "Cela signifie que vous ne pouvez pas pousser du code tant que vous n’avez pas créé un dépôt vide, ou que vous n’avez pas importé un dépôt existant."
+
+msgid "This merge request is locked."
+msgstr "Cette demande de fusion est verrouillée."
msgid "Time before an issue gets scheduled"
-msgstr "Temps avant qu’un incident ne soit planifié"
+msgstr "Temps avant qu’un ticket ne soit planifié"
msgid "Time before an issue starts implementation"
-msgstr "Temps avant que résolution ne débute"
+msgstr "Temps avant que la résolution du ticket ne débute"
msgid "Time between merge request creation and merge/close"
msgstr "Temps entre la création d'une demande de fusion et sa fusion/clôture"
@@ -1486,7 +2070,7 @@ msgid "Time until first merge request"
msgstr "Temps jusqu’à la première demande de fusion"
msgid "Timeago|%s days ago"
-msgstr "Il y a %s jours"
+msgstr "il y a %s jours"
msgid "Timeago|%s days remaining"
msgstr "Il reste %s jours"
@@ -1516,7 +2100,7 @@ msgid "Timeago|%s weeks remaining"
msgstr "Il reste %s semaines"
msgid "Timeago|%s years ago"
-msgstr "Il y a %s ans"
+msgstr "il y a %s ans"
msgid "Timeago|%s years remaining"
msgstr "Il reste %s ans"
@@ -1551,9 +2135,6 @@ msgstr "Il y a un mois"
msgid "Timeago|a week ago"
msgstr "Il y a une semaine"
-msgid "Timeago|a while"
-msgstr "Il y a un moment"
-
msgid "Timeago|a year ago"
msgstr "Il y a un an"
@@ -1605,6 +2186,9 @@ msgstr "Dans 1 semaine"
msgid "Timeago|in 1 year"
msgstr "Dans 1 an"
+msgid "Timeago|in a while"
+msgstr "il y a longtemps"
+
msgid "Timeago|less than a minute ago"
msgstr "il y a moins d'une minute"
@@ -1627,8 +2211,35 @@ msgstr "Temps total"
msgid "Total test time for all commits/merges"
msgstr "Temps total de test pour toutes les validations/fusions"
+msgid "Track activity with Contribution Analytics."
+msgstr "Suivre l’activité avec Contribution Analytics."
+
+msgid "Unlock"
+msgstr "Déverrouiller"
+
+msgid "Unlocked"
+msgstr "Déverrouillé"
+
msgid "Unstar"
-msgstr "Se désabonner"
+msgstr "Supprimer des favoris"
+
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr "Mettez à jour votre abonnement pour activer la Recherche Globale Avancée."
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr "Mettez à jour votre abonnement pour activer Contribution Analytics."
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr "Mettez à jour votre abonnement pour activer les Webhooks de groupe."
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr "Mettez à niveau votre abonnement pour activer les poids de ticket."
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr "Mettez à niveau votre abonnement pour améliorer les tableaux de tickets."
msgid "Upload New File"
msgstr "Téléverser un nouveau fichier"
@@ -1646,13 +2257,13 @@ msgid "Use your global notification setting"
msgstr "Utiliser vos paramètres de notification globaux"
msgid "View file @ "
-msgstr ""
+msgstr "Voir le fichier @ "
msgid "View open merge request"
msgstr "Afficher la demande de fusion"
msgid "View replaced file @ "
-msgstr ""
+msgstr "Voir le fichier remplacé @ "
msgid "VisibilityLevel|Internal"
msgstr "Interne"
@@ -1672,32 +2283,155 @@ msgstr "Vous voulez voir les données ? Merci de contacter un administrateur pou
msgid "We don't have enough data to show this stage."
msgstr "Nous n'avons pas suffisamment de données pour afficher cette étape."
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr "Les webhooks vous permettent d’appeler une URL si, par exemple, du nouveau code est poussé ou un nouveau ticket est créé. Vous pouvez configurer les webhooks pour écouter les événements spécifiques comme des poussées de code, des tickets ou des demandes de fusion. Les webhooks de groupes s’appliqueront à tous les projets dans un groupe, ce qui vous permet de normaliser la fonctionnalité du webhook dans votre groupe entier."
+
+msgid "Weight"
+msgstr "Poids"
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr "Si l’accès à un stockage échoue, GitLab empêchera l’accès au stockage pendant la durée spécifiée ici. Cela permettra au système de fichiers de récupérer. Les dépôts présents sur les secteurs en erreur seront temporairement indisponibles."
+
msgid "Wiki"
msgstr "Wiki"
+msgid "WikiClone|Clone your wiki"
+msgstr "Clonez votre wiki"
+
+msgid "WikiClone|Git Access"
+msgstr "Accès git"
+
+msgid "WikiClone|Install Gollum"
+msgstr "Installer Gollum"
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr "Il est recommandé d’installer %{markdown} pour que les spécificités de GFM s’affichent en local :"
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr "Démarrer Gollum et modifier localement"
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr "Vous n’êtes pas autorisé·e à créer des pages wiki"
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr "Il s’agit d’une ancienne version de la page."
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr "Vous pouvez consulter le %{most_recent_link} ou parcourir l’%{history_link}."
+
+msgid "WikiHistoricalPage|history"
+msgstr "historique"
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr "version la plus récente"
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr "D’autres exemples se trouvent dans la %{docs_link}"
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr "documentation"
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr "Pour créer un lien vers une (nouvelle) page, il suffit de saisir %{link_example}"
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr "consignes-installation"
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr "Astuce : vous pouvez saisir le chemin d’accès complet du nouveau fichier. Nous créerons automatiquement les répertoires manquants."
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr "Nouvelle Page Wiki"
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr "Êtes-vous sûr·e de vouloir supprimer cette page ?"
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr "Quelqu’un a modifié la page en même temps que vous. Veuillez consulter %{page_link} et vérifiez que vos modifications ne suppriment pas involontairement les siennes."
+
+msgid "WikiPageConflictMessage|the page"
+msgstr "la page"
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr "Créer %{page_title}"
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr "Mettre à jour %{page_title}"
+
+msgid "WikiPage|Page slug"
+msgstr "Slug de la page"
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr "Écrivez du contenu ou faîtes glisser des fichiers ici..."
+
+msgid "Wiki|Create Page"
+msgstr "Créer une Page"
+
+msgid "Wiki|Create page"
+msgstr "Créer la page"
+
+msgid "Wiki|Edit Page"
+msgstr "Modifier cette Page"
+
+msgid "Wiki|Empty page"
+msgstr "Page vide"
+
+msgid "Wiki|More Pages"
+msgstr "Plus de Pages"
+
+msgid "Wiki|New page"
+msgstr "Nouvelle page"
+
+msgid "Wiki|Page history"
+msgstr "Historique de la page"
+
+msgid "Wiki|Page version"
+msgstr "Version de la page"
+
+msgid "Wiki|Pages"
+msgstr "Pages"
+
+msgid "Wiki|Wiki Pages"
+msgstr "Pages du Wiki"
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr "Avec Contribution Analytics vous pouvez avoir un aperçu de l’activité des tickets, demandes de fusion et des événements de poussée de votre organisation et de ses membres."
+
msgid "Withdraw Access Request"
msgstr "Retirer la demande d'accès"
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
-msgstr "Vous êtes sur le point de supprimer %{group_name}. Les groupes supprimés NE PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr ?"
+msgstr "Vous êtes sur le point de supprimer %{group_name}. Les groupes supprimés NE PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr·e ?"
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 ?"
+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·e ?"
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."
+msgstr "Vous allez supprimer la relation de fourche avec le projet source %{forked_from_project}. Êtes-vous ABSOLUMENT sûr·e ?"
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 ?"
+msgstr "Vous allez transférer %{project_name_with_namespace} à un nouveau propriétaire. Êtes vous ABSOLUMENT sûr·e ?"
+
+msgid "You are on a read-only GitLab instance."
+msgstr "Vous êtes sur une instance GitLab en lecture seule."
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr "Vous êtes sur une instance GitLab en lecture seule. Si vous souhaitez apporter des modifications, vous devez aller sur %{link_to_primary_node}."
msgid "You can only add files when you are on a branch"
msgstr "Vous ne pouvez ajouter de fichier que dans une branche"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr "Vous ne pouvez pas écrire sur une instance GitLab Geo secondaire en lecture-seule. Veuillez utiliser le %{link_to_primary_node} à la place."
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr "Vous ne pouvez pas écrire sur cette instance GitLab en lecture-seule."
+
msgid "You have reached your project limit"
msgstr "Vous avez atteint votre limite de projet"
msgid "You must sign in to star a project"
-msgstr "Vous devez vous identifier pour vous abonner à un projet"
+msgstr "Vous devez vous connecter pour mettre un projet en favori"
msgid "You need permission."
msgstr "Vous avez besoin d’une autorisation."
@@ -1723,14 +2457,20 @@ msgstr "Vous ne pourrez pas récupérer ou pousser de code par %{protocol} tant
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 comment will not be visible to the public."
+msgstr "Votre commentaire ne sera pas visible publiquement."
+
+msgid "Your groups"
+msgstr "Vos groupes"
+
msgid "Your name"
msgstr "Votre nom"
msgid "Your projects"
-msgstr ""
+msgstr "Vos projets"
msgid "commit"
-msgstr ""
+msgstr "validation"
msgid "day"
msgid_plural "days"
@@ -1748,3 +2488,15 @@ msgid_plural "parents"
msgstr[0] "parent"
msgstr[1] "parents"
+msgid "password"
+msgstr "mot de passe"
+
+msgid "personal access token"
+msgstr "jeton d’accès personnel"
+
+msgid "to help your contributors communicate effectively!"
+msgstr "pour aider vos contributeurs à communiquer efficacement !"
+
+msgid "username"
+msgstr "nom d’utilisateur"
+
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c73e582e608..32afb7b06e4 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-06 18:33+0200\n"
-"PO-Revision-Date: 2017-10-06 18:33+0200\n"
+"POT-Creation-Date: 2017-10-22 16:40+0300\n"
+"PO-Revision-Date: 2017-10-22 16:40+0300\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -106,9 +106,18 @@ msgstr ""
msgid "Add new directory"
msgstr ""
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr ""
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -124,6 +133,9 @@ msgstr ""
msgid "Are you sure you want to discard your changes?"
msgstr ""
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -157,18 +169,21 @@ msgstr ""
msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
msgid "AutoDevOps|Enable in settings"
msgstr ""
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] ""
@@ -177,6 +192,9 @@ 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 "Branch has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr ""
@@ -372,10 +390,19 @@ msgstr ""
msgid "CiStatus|running"
msgstr ""
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
msgid "Clone repository"
msgstr ""
-msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgid "Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster details"
msgstr ""
msgid "ClusterIntegration|Cluster integration"
@@ -420,37 +447,46 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
-msgstr ""
-
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
-msgid "ClusterIntegration|See machine types"
+msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
msgid "ClusterIntegration|Remove cluster integration"
msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Container Engine."
+msgstr ""
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
msgstr ""
-msgid "ClusterIntegration|Save changes"
+msgid "ClusterIntegration|See machine types"
msgstr ""
msgid "ClusterIntegration|See your projects"
@@ -462,18 +498,12 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine."
-msgstr ""
-
-msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
-msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
-msgstr ""
-
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -608,6 +638,9 @@ msgstr ""
msgid "Create merge request"
msgstr ""
+msgid "Create new branch"
+msgstr ""
+
msgid "Create new..."
msgstr ""
@@ -691,6 +724,9 @@ msgstr ""
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
msgid "Don't show again"
msgstr ""
@@ -760,6 +796,9 @@ msgstr ""
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr ""
@@ -846,6 +885,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -876,6 +960,12 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr ""
@@ -935,6 +1025,9 @@ msgstr ""
msgid "Learn more in the|pipeline schedules documentation"
msgstr ""
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr ""
@@ -946,6 +1039,18 @@ msgid_plural "Limited to showing %d events at most"
msgstr[0] ""
msgstr[1] ""
+msgid "Lock"
+msgstr ""
+
+msgid "Locked"
+msgstr ""
+
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr ""
@@ -973,6 +1078,9 @@ msgstr ""
msgid "More information is available|here"
msgstr ""
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] ""
@@ -990,18 +1098,27 @@ msgstr ""
msgid "New file"
msgstr ""
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr ""
msgid "New merge request"
msgstr ""
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr ""
msgid "New snippet"
msgstr ""
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr ""
@@ -1080,6 +1197,9 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
+msgid "Only project members can comment."
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr ""
@@ -1110,6 +1230,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr ""
+
msgid "Pipeline"
msgstr ""
@@ -1209,9 +1332,54 @@ msgstr ""
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr ""
@@ -1221,9 +1389,6 @@ 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 ""
@@ -1266,6 +1431,9 @@ msgstr ""
msgid "ProjectNetworkGraph|Graph"
msgstr ""
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1287,6 +1455,12 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push events"
msgstr ""
@@ -1350,6 +1524,9 @@ msgstr ""
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr ""
@@ -1368,6 +1545,15 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr ""
@@ -1415,12 +1601,18 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
+msgid "Sort by"
+msgstr ""
+
msgid "SortOptions|Access level, ascending"
msgstr ""
@@ -1529,6 +1721,9 @@ msgstr ""
msgid "Start the Runner!"
msgstr ""
+msgid "Subgroups"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr ""
@@ -1561,6 +1756,9 @@ 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 number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr ""
@@ -1591,6 +1789,12 @@ msgstr ""
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr ""
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr ""
@@ -1600,12 +1804,27 @@ msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
+msgid "This is a confidential issue."
+msgstr ""
+
msgid "This is the author's first Merge Request to this project."
msgstr ""
+msgid "This issue is confidential and locked."
+msgstr ""
+
+msgid "This issue is locked."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr ""
+msgid "This merge request is locked."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr ""
@@ -1760,6 +1979,12 @@ msgstr ""
msgid "Total test time for all commits/merges"
msgstr ""
+msgid "Unlock"
+msgstr ""
+
+msgid "Unlocked"
+msgstr ""
+
msgid "Unstar"
msgstr ""
@@ -1805,6 +2030,9 @@ msgstr ""
msgid "We don't have enough data to show this stage."
msgstr ""
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -1922,9 +2150,15 @@ msgstr ""
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr ""
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr ""
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr ""
@@ -1955,6 +2189,12 @@ 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 comment will not be visible to the public."
+msgstr ""
+
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr ""
@@ -1977,5 +2217,11 @@ msgid_plural "parents"
msgstr[0] ""
msgstr[1] ""
+msgid "password"
+msgstr ""
+
msgid "personal access token"
msgstr ""
+
+msgid "username"
+msgstr ""
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index 804817e96e9..4ac7676ca18 100644
--- a/locale/it/gitlab.po
+++ b/locale/it/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:44-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:31-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -21,6 +21,11 @@ msgid_plural "%d commits"
msgstr[0] ""
msgstr[1] ""
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] "%s commit aggiuntivo è stato omesso per evitare degradi di prestazioni negli issues."
@@ -29,6 +34,11 @@ msgstr[1] "%s commit aggiuntivi sono stati omessi per evitare degradi di prestaz
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} ha committato %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -49,6 +59,12 @@ msgstr[1] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -57,6 +73,9 @@ msgstr[1] ""
msgid "1st contribution!"
msgstr ""
+msgid "2FA enabled"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Un insieme di grafici riguardo la Continuous Integration"
@@ -81,12 +100,18 @@ msgstr "Attivo"
msgid "Activity"
msgstr "Attività"
+msgid "Add"
+msgstr ""
+
msgid "Add Changelog"
msgstr "Aggiungi Changelog"
msgid "Add Contribution guide"
msgstr "Aggiungi Guida per contribuire"
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Add License"
msgstr "Aggiungi Licenza"
@@ -96,9 +121,18 @@ msgstr "Aggiungi una chiave SSH al tuo profilo per eseguire pull o push tramite
msgid "Add new directory"
msgstr "Aggiungi una directory (cartella)"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr ""
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -114,6 +148,9 @@ msgstr "Sei sicuro di voler cancellare questa pipeline programmata?"
msgid "Are you sure you want to discard your changes?"
msgstr ""
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -132,25 +169,34 @@ msgstr "Aggiungi un file tramite trascina &amp; rilascia ( drag &amp; drop) o %{
msgid "Authentication Log"
msgstr ""
-msgid "Auto DevOps (Beta)"
+msgid "Author"
msgstr ""
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto DevOps documentation"
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgid "AutoDevOps|Auto DevOps documentation"
+msgstr ""
+
+msgid "AutoDevOps|Enable in settings"
+msgstr ""
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
msgstr ""
-msgid "AutoDevOps|Learn more in the"
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
+msgstr ""
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
msgid "Billing"
@@ -168,6 +214,9 @@ msgstr ""
msgid "BillingPlans|Customer Support"
msgstr ""
+msgid "BillingPlans|Downgrade"
+msgstr ""
+
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
msgstr ""
@@ -204,12 +253,6 @@ msgstr ""
msgid "BillingPlans|per user"
msgstr ""
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
-
msgid "Branch"
msgid_plural "Branches"
msgstr[0] ""
@@ -218,6 +261,9 @@ 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 has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Cerca branches"
@@ -332,12 +378,18 @@ msgstr ""
msgid "CI configuration"
msgstr "Configurazione CI (Integrazione Continua)"
+msgid "CICD|Jobs"
+msgstr ""
+
msgid "Cancel"
msgstr "Cancella"
msgid "Cancel edit"
msgstr ""
+msgid "Change Weight"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Preleva nella branch"
@@ -365,6 +417,9 @@ msgstr ""
msgid "Cherry-pick this merge request"
msgstr "Cherry-pick questa richiesta di merge"
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr ""
+
msgid "CiStatusLabel|canceled"
msgstr "cancellato"
@@ -419,6 +474,147 @@ msgstr "saltata"
msgid "CiStatus|running"
msgstr "in corso"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
+msgid "Clone repository"
+msgstr ""
+
+msgid "Close"
+msgstr ""
+
+msgid "Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Create cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr ""
+
+msgid "ClusterIntegration|Machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr ""
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
+msgstr ""
+
+msgid "ClusterIntegration|See your projects"
+msgstr ""
+
+msgid "ClusterIntegration|See zones"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr ""
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|cluster"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -427,6 +623,14 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Commit Message"
+msgstr ""
+
msgid "Commit duration in minutes for last 30 commits"
msgstr "Durata del commit (in minuti) per gli ultimi 30 commit"
@@ -454,12 +658,63 @@ msgstr "Committato da "
msgid "Compare"
msgstr "Confronta"
+msgid "Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Created"
+msgstr ""
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr ""
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr ""
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Learn more about"
+msgstr ""
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr ""
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr ""
+
+msgid "ContainerRegistry|Remove repository"
+msgstr ""
+
+msgid "ContainerRegistry|Remove tag"
+msgstr ""
+
+msgid "ContainerRegistry|Size"
+msgstr ""
+
+msgid "ContainerRegistry|Tag"
+msgstr ""
+
+msgid "ContainerRegistry|Tag ID"
+msgstr ""
+
+msgid "ContainerRegistry|Use different image names"
+msgstr ""
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr ""
+
msgid "Contribution guide"
msgstr "Guida per contribuire"
msgid "Contributors"
msgstr "Collaboratori"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -472,9 +727,6 @@ msgstr "Copia l'SHA del commit negli appunti"
msgid "Create New Directory"
msgstr "Crea una nuova cartella"
-msgid "Create a new branch"
-msgstr ""
-
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}"
@@ -484,9 +736,21 @@ msgstr "Crea cartella"
msgid "Create empty bare repository"
msgstr "Crea una repository vuota"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "Crea una richiesta di merge"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "Crea nuovo..."
@@ -538,6 +802,12 @@ msgstr "Pre-rilascio"
msgid "CycleAnalyticsStage|Test"
msgstr "Test"
+msgid "DashboardProjects|All"
+msgstr ""
+
+msgid "DashboardProjects|Personal"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "Definisci un patter personalizzato mediante la sintassi cron"
@@ -555,6 +825,9 @@ msgstr ""
msgid "Description"
msgstr "Descrizione"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr ""
+
msgid "Details"
msgstr ""
@@ -564,6 +837,12 @@ msgstr "Nome cartella"
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Dismiss Merge Request promotion"
+msgstr ""
+
msgid "Don't show again"
msgstr "Non mostrare più"
@@ -603,9 +882,6 @@ msgstr "Cambia programmazione della pipeline %{id}"
msgid "Emails"
msgstr ""
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -636,12 +912,18 @@ msgstr "Ogni settimana (Di domenica alle 4 del mattino)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "Impossibile cambiare owner"
msgid "Failed to remove the pipeline schedule"
msgstr "Impossibile rimuovere la pipeline pianificata"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr ""
@@ -668,6 +950,12 @@ msgstr[1] ""
msgid "ForkedFromProjectPath|Forked from"
msgstr "Fork da"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr ""
+
+msgid "Format"
+msgstr ""
+
msgid "From issue creation until deploy to production"
msgstr "Dalla creazione di un issue fino al rilascio in produzione"
@@ -680,6 +968,18 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
+msgid "Geo|Groups to replicate"
+msgstr ""
+
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
+msgid "Geo|Select groups to replicate."
+msgstr ""
+
msgid "Git storage health information has been reset"
msgstr ""
@@ -692,6 +992,9 @@ msgstr "Vai il tuo fork"
msgid "GoToYourFork|Fork"
msgstr "Fork"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -716,6 +1019,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -734,28 +1082,60 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
+msgid "History"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "Housekeeping iniziato con successo"
msgid "Import repository"
msgstr "Importa repository"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Intervallo di Pattern"
msgid "Introducing Cycle Analytics"
msgstr "Introduzione delle Analisi Cicliche"
+msgid "Issue board focus mode"
+msgstr ""
+
+msgid "Issue boards with milestones"
+msgstr ""
+
msgid "Issue events"
msgstr ""
-msgid "Issues"
+msgid "IssueBoards|Board"
msgstr ""
-msgid "Jobs"
+msgid "IssueBoards|Boards"
+msgstr ""
+
+msgid "Issues"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -775,12 +1155,21 @@ msgstr[1] "Gli ultimi %d giorni"
msgid "Last Pipeline"
msgstr "Ultima Pipeline"
-msgid "Last Update"
-msgstr "Ultimo Aggiornamento"
-
msgid "Last commit"
msgstr "Ultimo Commit"
+msgid "Last edited %{date}"
+msgstr ""
+
+msgid "Last edited by %{name}"
+msgstr ""
+
+msgid "Last update"
+msgstr ""
+
+msgid "Last updated"
+msgstr ""
+
msgid "LastPushEvent|You pushed to"
msgstr ""
@@ -793,6 +1182,9 @@ msgstr "Leggi di più su"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "documentazione sulla pianificazione delle pipelines"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "Abbandona il gruppo"
@@ -807,9 +1199,21 @@ msgid_plural "Limited to showing %d events at most"
msgstr[0] "Limita visualizzazione %d d'evento"
msgstr[1] "Limita visualizzazione %d di eventi"
+msgid "Lock"
+msgstr ""
+
+msgid "Locked"
+msgstr ""
+
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "Mediano"
@@ -837,6 +1241,12 @@ msgstr ""
msgid "More information is available|here"
msgstr ""
+msgid "Multiple issue boards"
+msgstr ""
+
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nuovo Issue"
@@ -854,27 +1264,42 @@ msgstr "Nuova directory"
msgid "New file"
msgstr "Nuovo file"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "Nuovo Issue"
msgid "New merge request"
msgstr "Nuova richiesta di merge"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "Nuova pianficazione"
msgid "New snippet"
msgstr "Nuovo snippet"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "Nuovo tag"
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr ""
+
msgid "No repository"
msgstr "Nessuna Repository"
msgid "No schedules"
msgstr "Nessuna pianificazione"
+msgid "None"
+msgstr ""
+
msgid "Not available"
msgstr "Non disponibile"
@@ -938,12 +1363,24 @@ msgstr "Osserva"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtra"
+msgid "Only project members can comment."
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Aperto"
+msgid "Opens in a new window"
+msgstr ""
+
msgid "Options"
msgstr "Opzioni"
@@ -968,6 +1405,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr ""
+
msgid "Pipeline"
msgstr ""
@@ -1070,9 +1510,54 @@ msgstr "con più stadi"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Il Progetto '%{project_name}' in coda di eliminazione."
@@ -1082,9 +1567,6 @@ msgstr "Il Progetto '%{project_name}' è stato creato con successo."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Il Progetto '%{project_name}' è stato aggiornato con successo."
-msgid "Project '%{project_name}' will be deleted."
-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"
@@ -1127,6 +1609,27 @@ msgstr "Stadio"
msgid "ProjectNetworkGraph|Graph"
msgstr "Grafico"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr ""
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr ""
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1148,12 +1651,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr ""
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Vedi altro"
@@ -1217,6 +1729,12 @@ msgstr "Ripristina questa richiesta di merge"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
+msgid "Save changes"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "Salva pianificazione pipeline"
@@ -1232,15 +1750,21 @@ msgstr "Pianificazione pipelines"
msgid "Search branches and tags"
msgstr "Ricerca branches e tags"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
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"
@@ -1279,6 +1803,21 @@ msgstr[1] "Visualizza %d eventi"
msgid "Snippets"
msgstr ""
+msgid "Something went wrong on our end."
+msgstr ""
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
+msgid "Something went wrong while fetching the projects."
+msgstr ""
+
+msgid "Something went wrong while fetching the registry list."
+msgstr ""
+
+msgid "Sort by"
+msgstr ""
+
msgid "SortOptions|Access level, ascending"
msgstr ""
@@ -1396,6 +1935,12 @@ msgstr "inizia una %{new_merge_request} con queste modifiche"
msgid "Start the Runner!"
msgstr ""
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Cambia branch/tag"
@@ -1416,6 +1961,15 @@ msgstr "Branch di destinazione"
msgid "Team"
msgstr ""
+msgid "Thanks! Don't show me this again"
+msgstr ""
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr ""
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+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 "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."
@@ -1428,6 +1982,15 @@ 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 number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "Il ciclo vitale della fase di sviluppo."
@@ -1458,6 +2021,12 @@ msgstr "Lo stadio di pre-rilascio mostra il tempo che trascorre da una MR (Richi
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 in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "Il tempo aggregato relativo eventi/data entry raccolto in quello stadio."
@@ -1467,12 +2036,27 @@ msgstr "Il valore falsato nel mezzo di una serie di dati osservati. ES: tra 3,5,
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "This is the author's first Merge Request to this project. Handle with care."
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
+msgid "This is a confidential issue."
+msgstr ""
+
+msgid "This is the author's first Merge Request to this project."
+msgstr ""
+
+msgid "This issue is confidential and locked."
+msgstr ""
+
+msgid "This issue is locked."
msgstr ""
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Questo significa che non è possibile effettuare push di codice fino a che non crei una repository vuota o ne importi una esistente"
+msgid "This merge request is locked."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "Il tempo che impiega un issue per esser pianificato"
@@ -1551,9 +2135,6 @@ msgstr "un mese fa"
msgid "Timeago|a week ago"
msgstr "una settimana fa"
-msgid "Timeago|a while"
-msgstr "poco fa"
-
msgid "Timeago|a year ago"
msgstr "un anno fa"
@@ -1605,6 +2186,9 @@ msgstr "in 1 settimana"
msgid "Timeago|in 1 year"
msgstr "in 1 anno"
+msgid "Timeago|in a while"
+msgstr ""
+
msgid "Timeago|less than a minute ago"
msgstr "meno di un minuto fa"
@@ -1627,9 +2211,36 @@ msgstr "Tempo Totale"
msgid "Total test time for all commits/merges"
msgstr "Tempo totale di test per tutti i commits/merges"
+msgid "Track activity with Contribution Analytics."
+msgstr ""
+
+msgid "Unlock"
+msgstr ""
+
+msgid "Unlocked"
+msgstr ""
+
msgid "Unstar"
msgstr ""
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr ""
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr ""
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr ""
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr ""
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr ""
+
msgid "Upload New File"
msgstr "Carica un nuovo file"
@@ -1672,9 +2283,120 @@ msgstr "Vuoi visualizzare i dati? Richiedi l'accesso ad un amministratore, grazi
msgid "We don't have enough data to show this stage."
msgstr "Non ci sono sufficienti dati da mostrare su questo stadio"
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr ""
+
+msgid "Weight"
+msgstr ""
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
+msgid "WikiClone|Clone your wiki"
+msgstr ""
+
+msgid "WikiClone|Git Access"
+msgstr ""
+
+msgid "WikiClone|Install Gollum"
+msgstr ""
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr ""
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr ""
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr ""
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr ""
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr ""
+
+msgid "WikiHistoricalPage|history"
+msgstr ""
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr ""
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr ""
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr ""
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr ""
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr ""
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr ""
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr ""
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr ""
+
+msgid "WikiPageConflictMessage|the page"
+msgstr ""
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr ""
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr ""
+
+msgid "WikiPage|Page slug"
+msgstr ""
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr ""
+
+msgid "Wiki|Create Page"
+msgstr ""
+
+msgid "Wiki|Create page"
+msgstr ""
+
+msgid "Wiki|Edit Page"
+msgstr ""
+
+msgid "Wiki|Empty page"
+msgstr ""
+
+msgid "Wiki|More Pages"
+msgstr ""
+
+msgid "Wiki|New page"
+msgstr ""
+
+msgid "Wiki|Page history"
+msgstr ""
+
+msgid "Wiki|Page version"
+msgstr ""
+
+msgid "Wiki|Pages"
+msgstr ""
+
+msgid "Wiki|Wiki Pages"
+msgstr ""
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr ""
+
msgid "Withdraw Access Request"
msgstr "Ritira richiesta d'accesso"
@@ -1690,9 +2412,21 @@ msgstr "Stai per rimuovere la relazione con il progetto sorgente %{forked_from_p
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 on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "Puoi aggiungere files solo quando sei in una branch"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "Hai raggiunto il tuo limite di progetto"
@@ -1723,6 +2457,12 @@ msgstr "Non sarai in grado di eseguire pull o push di codice tramite %{protocol}
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 comment will not be visible to the public."
+msgstr ""
+
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "Il tuo nome"
@@ -1748,3 +2488,15 @@ msgid_plural "parents"
msgstr[0] ""
msgstr[1] ""
+msgid "password"
+msgstr ""
+
+msgid "personal access token"
+msgstr ""
+
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index 2a08abda7ce..9045ae26c4a 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:44-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:31-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Japanese\n"
"Language: ja_JP\n"
@@ -20,6 +20,10 @@ msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d個ã®ã‚³ãƒŸãƒƒãƒˆ"
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] ""
+
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 個ã®ã‚³ãƒŸãƒƒãƒˆã‚’çœç•¥ã—ã¾ã—ãŸã€‚"
@@ -27,6 +31,10 @@ msgstr[0] "パフォーマンス低下をé¿ã‘ã‚‹ãŸã‚ %s 個ã®ã‚³ãƒŸãƒƒãƒˆã‚
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_timeago}ã«%{commit_author_link}ãŒã‚³ãƒŸãƒƒãƒˆã—ã¾ã—ãŸã€‚"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -46,6 +54,12 @@ msgstr[0] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d 個ã®ãƒ‘イプライン"
@@ -53,6 +67,9 @@ msgstr[0] "%d 個ã®ãƒ‘イプライン"
msgid "1st contribution!"
msgstr ""
+msgid "2FA enabled"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "CIã«ã¤ã„ã¦ã®ã‚°ãƒ©ãƒ•"
@@ -77,12 +94,18 @@ msgstr "有効"
msgid "Activity"
msgstr "アクティビティー"
+msgid "Add"
+msgstr ""
+
msgid "Add Changelog"
msgstr "変更履歴を追加"
msgid "Add Contribution guide"
msgstr "貢献者å‘ã‘ガイドを追加"
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Add License"
msgstr "ライセンスを追加"
@@ -92,9 +115,18 @@ msgstr "SSHã§ãƒ—ルやプッシュã™ã‚‹å ´åˆã¯ã€ãƒ—ロフィールã«SSHéµ
msgid "Add new directory"
msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’追加"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr ""
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -110,6 +142,9 @@ msgstr "ã“ã®ãƒ‘イプラインスケジュールを削除ã—ã¾ã™ã‹ï¼Ÿ"
msgid "Are you sure you want to discard your changes?"
msgstr ""
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -128,25 +163,34 @@ msgstr "ドラッグ&ドロップã¾ãŸã¯ %{upload_link} ã§ãƒ•ã‚¡ã‚¤ãƒ«ã‚’æ·
msgid "Authentication Log"
msgstr ""
-msgid "Auto DevOps (Beta)"
+msgid "Author"
msgstr ""
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto DevOps documentation"
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgid "AutoDevOps|Auto DevOps documentation"
+msgstr ""
+
+msgid "AutoDevOps|Enable in settings"
+msgstr ""
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
-msgid "AutoDevOps|Learn more in the"
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
msgid "Billing"
@@ -164,6 +208,9 @@ msgstr ""
msgid "BillingPlans|Customer Support"
msgstr ""
+msgid "BillingPlans|Downgrade"
+msgstr ""
+
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
msgstr ""
@@ -200,12 +247,6 @@ msgstr ""
msgid "BillingPlans|per user"
msgstr ""
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
-
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "ブランãƒ"
@@ -213,6 +254,9 @@ 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 has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "ブランãƒã‚’検索"
@@ -327,12 +371,18 @@ msgstr ""
msgid "CI configuration"
msgstr "CI 設定"
+msgid "CICD|Jobs"
+msgstr ""
+
msgid "Cancel"
msgstr "キャンセル"
msgid "Cancel edit"
msgstr ""
+msgid "Change Weight"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "ピック先ブランãƒ:"
@@ -360,6 +410,9 @@ msgstr "ã“ã®ã‚³ãƒŸãƒƒãƒˆã‚’ãƒã‚§ãƒªãƒ¼ãƒ”ック"
msgid "Cherry-pick this merge request"
msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’ãƒã‚§ãƒªãƒ¼ãƒ”ック"
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr ""
+
msgid "CiStatusLabel|canceled"
msgstr "キャンセル"
@@ -414,6 +467,147 @@ msgstr "スキップ済ã¿"
msgid "CiStatus|running"
msgstr "実行中"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
+msgid "Clone repository"
+msgstr ""
+
+msgid "Close"
+msgstr ""
+
+msgid "Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Create cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr ""
+
+msgid "ClusterIntegration|Machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr ""
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
+msgstr ""
+
+msgid "ClusterIntegration|See your projects"
+msgstr ""
+
+msgid "ClusterIntegration|See zones"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr ""
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|cluster"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -421,6 +615,13 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "コミット"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+
+msgid "Commit Message"
+msgstr ""
+
msgid "Commit duration in minutes for last 30 commits"
msgstr "ç›´è¿‘30コミットã®æ‰€è¦æ™‚é–“(分)"
@@ -448,12 +649,63 @@ msgstr "コミット担当者: "
msgid "Compare"
msgstr "比較"
+msgid "Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Created"
+msgstr ""
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr ""
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr ""
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Learn more about"
+msgstr ""
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr ""
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr ""
+
+msgid "ContainerRegistry|Remove repository"
+msgstr ""
+
+msgid "ContainerRegistry|Remove tag"
+msgstr ""
+
+msgid "ContainerRegistry|Size"
+msgstr ""
+
+msgid "ContainerRegistry|Tag"
+msgstr ""
+
+msgid "ContainerRegistry|Tag ID"
+msgstr ""
+
+msgid "ContainerRegistry|Use different image names"
+msgstr ""
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr ""
+
msgid "Contribution guide"
msgstr "貢献者å‘ã‘ガイド"
msgid "Contributors"
msgstr "貢献者"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -466,9 +718,6 @@ msgstr "コミットã®SHAをクリップボードã«ã‚³ãƒ”ー"
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 "%{protocol} ã§ãƒ—ッシュやプルã™ã‚‹ãŸã‚ã®ã‚ãªãŸå€‹äººç”¨ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’作æˆ"
@@ -478,9 +727,21 @@ msgstr "ディレクトリを作æˆ"
msgid "Create empty bare repository"
msgstr "空ã®bareレãƒã‚¸ãƒˆãƒªãƒ¼ã‚’作æˆ"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "マージリクエストを作æˆ"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "æ–°è¦ä½œæˆ"
@@ -532,6 +793,12 @@ msgstr "ステージング"
msgid "CycleAnalyticsStage|Test"
msgstr "テスト"
+msgid "DashboardProjects|All"
+msgstr ""
+
+msgid "DashboardProjects|Personal"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "Cron 構文ã§ã‚«ã‚¹ã‚¿ãƒ ãªãƒ‘ターンを指定ã™ã‚‹"
@@ -548,6 +815,9 @@ msgstr ""
msgid "Description"
msgstr "説明"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr ""
+
msgid "Details"
msgstr ""
@@ -557,6 +827,12 @@ msgstr "ディレクトリå"
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Dismiss Merge Request promotion"
+msgstr ""
+
msgid "Don't show again"
msgstr "次回ã‹ã‚‰è¡¨ç¤ºã—ãªã„"
@@ -596,9 +872,6 @@ msgstr "パイプラインスケジュール %{id} を編集"
msgid "Emails"
msgstr ""
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -629,12 +902,18 @@ msgstr "毎週 (日曜日ã®åˆå‰4:00)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "オーナーを変更ã§ãã¾ã›ã‚“ã§ã—ãŸ"
msgid "Failed to remove the pipeline schedule"
msgstr "パイプラインスケジュールを削除ã§ãã¾ã›ã‚“ã§ã—ãŸ"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "ファイル"
@@ -660,6 +939,12 @@ msgstr[0] "フォーク"
msgid "ForkedFromProjectPath|Forked from"
msgstr "フォーク元"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr ""
+
+msgid "Format"
+msgstr ""
+
msgid "From issue creation until deploy to production"
msgstr "課題ãŒç™»éŒ²ã•ã‚Œã¦ã‹ã‚‰ãƒ—ロダクションã«ãƒ‡ãƒ—ロイã•ã‚Œã‚‹ã¾ã§"
@@ -672,6 +957,18 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
+msgid "Geo|Groups to replicate"
+msgstr ""
+
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
+msgid "Geo|Select groups to replicate."
+msgstr ""
+
msgid "Git storage health information has been reset"
msgstr ""
@@ -684,6 +981,9 @@ msgstr "自分ã®ãƒ•ã‚©ãƒ¼ã‚¯ã¸ç§»å‹•"
msgid "GoToYourFork|Fork"
msgstr "フォーク"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -708,6 +1008,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -726,28 +1071,59 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
+msgid "History"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "ãƒã‚¦ã‚¹ã‚­ãƒ¼ãƒ”ングã¯æ­£å¸¸ã«èµ·å‹•ã—ã¾ã—ãŸã€‚"
msgid "Import repository"
msgstr "レãƒã‚¸ãƒˆãƒªãƒ¼ã‚’インãƒãƒ¼ãƒˆ"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] ""
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "é–“éš”ã®ãƒ‘ターン"
msgid "Introducing Cycle Analytics"
msgstr "サイクル分æžã®ã”紹介"
+msgid "Issue board focus mode"
+msgstr ""
+
+msgid "Issue boards with milestones"
+msgstr ""
+
msgid "Issue events"
msgstr ""
-msgid "Issues"
+msgid "IssueBoards|Board"
msgstr ""
-msgid "Jobs"
+msgid "IssueBoards|Boards"
+msgstr ""
+
+msgid "Issues"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -766,12 +1142,21 @@ msgstr[0] "éŽåŽ»%d日間"
msgid "Last Pipeline"
msgstr "最新パイプライン"
-msgid "Last Update"
-msgstr "最新アップデート"
-
msgid "Last commit"
msgstr "最新コミット"
+msgid "Last edited %{date}"
+msgstr ""
+
+msgid "Last edited by %{name}"
+msgstr ""
+
+msgid "Last update"
+msgstr ""
+
+msgid "Last updated"
+msgstr ""
+
msgid "LastPushEvent|You pushed to"
msgstr ""
@@ -784,6 +1169,9 @@ msgstr "詳ã—ã見る:"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "詳ã—ãã¯ãƒ‘イプラインスケジュールã®ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆã‚’å‚ç…§"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "グループを離脱"
@@ -797,9 +1185,21 @@ msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "イベント表示数を最大 %d 個ã«åˆ¶é™"
+msgid "Lock"
+msgstr ""
+
+msgid "Locked"
+msgstr ""
+
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "中央値"
@@ -827,6 +1227,12 @@ msgstr ""
msgid "More information is available|here"
msgstr ""
+msgid "Multiple issue boards"
+msgstr ""
+
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "æ–°è¦èª²é¡Œ"
@@ -843,27 +1249,42 @@ msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒª"
msgid "New file"
msgstr "æ–°è¦ãƒ•ã‚¡ã‚¤ãƒ«"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "æ–°è¦èª²é¡Œ"
msgid "New merge request"
msgstr "æ–°è¦ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "æ–°è¦ã‚¹ã‚±ã‚¸ãƒ¥ãƒ¼ãƒ«"
msgid "New snippet"
msgstr "æ–°è¦ã‚¹ãƒ‹ãƒšãƒƒãƒˆ"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "æ–°è¦ã‚¿ã‚°"
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr ""
+
msgid "No repository"
msgstr "レãƒã‚¸ãƒˆãƒªãƒ¼ã¯ã‚ã‚Šã¾ã›ã‚“"
msgid "No schedules"
msgstr "スケジュールãªã—"
+msgid "None"
+msgstr ""
+
msgid "Not available"
msgstr "利用ã§ãã¾ã›ã‚“"
@@ -927,12 +1348,24 @@ msgstr "ã™ã¹ã¦é€šçŸ¥"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "フィルター"
+msgid "Only project members can comment."
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "オープンã•ã‚ŒãŸã®ã¯"
+msgid "Opens in a new window"
+msgstr ""
+
msgid "Options"
msgstr "オプション"
@@ -957,6 +1390,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr ""
+
msgid "Pipeline"
msgstr "パイプライン"
@@ -1059,9 +1495,54 @@ msgstr "ステージã‚ã‚Š"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "'%{project_name}' プロジェクトã¯å‰Šé™¤å‡¦ç†å¾…ã¡ã§ã™ã€‚"
@@ -1071,9 +1552,6 @@ msgstr "'%{project_name}' プロジェクトã¯æ­£å¸¸ã«ä½œæˆã•ã‚Œã¾ã—ãŸã€‚
msgid "Project '%{project_name}' was successfully updated."
msgstr "'%{project_name}' プロジェクトã¯æ­£å¸¸ã«æ›´æ–°ã•ã‚Œã¾ã—ãŸã€‚"
-msgid "Project '%{project_name}' will be deleted."
-msgstr "'%{project_name}' プロジェクトã¯å‰Šé™¤ã•ã‚Œã¾ã™ã€‚"
-
msgid "Project access must be granted explicitly to each user."
msgstr "ユーザーã”ã¨ã«ãƒ—ロジェクトアクセスã®æ¨©é™ã‚’指定ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。"
@@ -1116,6 +1594,27 @@ msgstr "ステージ"
msgid "ProjectNetworkGraph|Graph"
msgstr "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã‚°ãƒ©ãƒ•"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr ""
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr ""
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1137,12 +1636,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr ""
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "続ãを読む"
@@ -1206,6 +1714,12 @@ msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’リãƒãƒ¼ãƒˆ"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
+msgid "Save changes"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "パイプラインスケジュールをä¿å­˜"
@@ -1221,15 +1735,21 @@ msgstr "パイプラインスケジューリング"
msgid "Search branches and tags"
msgstr "ブランãƒã¾ãŸã¯ã‚¿ã‚°ã‚’検索"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "アーカイブã®ãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆã‚’é¸æŠž"
msgid "Select a timezone"
msgstr "タイムゾーンをé¸æŠž"
-msgid "Select existing branch"
-msgstr ""
-
msgid "Select target branch"
msgstr "ターゲットブランãƒã‚’é¸æŠž"
@@ -1267,6 +1787,21 @@ msgstr[0] "%d ã®ã‚¤ãƒ™ãƒ³ãƒˆã‚’表示中"
msgid "Snippets"
msgstr ""
+msgid "Something went wrong on our end."
+msgstr ""
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
+msgid "Something went wrong while fetching the projects."
+msgstr ""
+
+msgid "Something went wrong while fetching the registry list."
+msgstr ""
+
+msgid "Sort by"
+msgstr ""
+
msgid "SortOptions|Access level, ascending"
msgstr ""
@@ -1384,6 +1919,12 @@ msgstr "ã“ã®å¤‰æ›´ã§ %{new_merge_request} を作æˆã™ã‚‹"
msgid "Start the Runner!"
msgstr ""
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "ブランãƒãƒ»ã‚¿ã‚°åˆ‡ã‚Šæ›¿ãˆ"
@@ -1403,6 +1944,15 @@ msgstr "ターゲットブランãƒ"
msgid "Team"
msgstr ""
+msgid "Thanks! Don't show me this again"
+msgstr ""
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr ""
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+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 "コーディングステージã§ã¯ã€æœ€åˆã®ã‚³ãƒŸãƒƒãƒˆã‹ã‚‰ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒä½œæˆã•ã‚Œã‚‹ã¾ã§ã®æ™‚é–“ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®ãƒ‡ãƒ¼ã‚¿ã¯æœ€åˆã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒä½œæˆã•ã‚ŒãŸã¨ãã«è‡ªå‹•çš„ã«è¿½åŠ ã•ã‚Œã¾ã™ã€‚"
@@ -1415,6 +1965,15 @@ 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 number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "開発ライフサイクルã®æ®µéšŽ"
@@ -1445,6 +2004,12 @@ 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 time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "ã“ã®ã‚¹ãƒ†ãƒ¼ã‚¸ã«åŽé›†ã•ã‚ŒãŸãƒ‡ãƒ¼ã‚¿æ¯Žã®æ™‚é–“"
@@ -1454,12 +2019,27 @@ msgstr "得られãŸä¸€é€£ã®ãƒ‡ãƒ¼ã‚¿ã‚’å°ã•ã„é †ã«ä¸¦ã¹ãŸã¨ãã«ä¸­å¤®
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "This is the author's first Merge Request to this project. Handle with care."
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
+msgid "This is a confidential issue."
+msgstr ""
+
+msgid "This is the author's first Merge Request to this project."
+msgstr ""
+
+msgid "This issue is confidential and locked."
+msgstr ""
+
+msgid "This issue is locked."
msgstr ""
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "空レãƒã‚¸ãƒˆãƒªãƒ¼ã‚’作æˆã¾ãŸã¯æ—¢å­˜ãƒ¬ãƒã‚¸ãƒˆãƒªãƒ¼ã‚’インãƒãƒ¼ãƒˆã‚’ã—ãªã‘ã‚Œã°ã€ã‚³ãƒ¼ãƒ‰ã®ãƒ—ッシュã¯ã§ãã¾ã›ã‚“。"
+msgid "This merge request is locked."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "課題ãŒè¨ˆç”»ã•ã‚Œã‚‹ã¾ã§ã®æ™‚é–“"
@@ -1538,9 +2118,6 @@ msgstr "1ヶ月å‰"
msgid "Timeago|a week ago"
msgstr "1週間å‰"
-msgid "Timeago|a while"
-msgstr "ã—ã°ã‚‰ãå‰"
-
msgid "Timeago|a year ago"
msgstr "1å¹´å‰"
@@ -1592,6 +2169,9 @@ msgstr "1週間以内"
msgid "Timeago|in 1 year"
msgstr "1年以内"
+msgid "Timeago|in a while"
+msgstr ""
+
msgid "Timeago|less than a minute ago"
msgstr "1分未満"
@@ -1612,9 +2192,36 @@ msgstr "åˆè¨ˆæ™‚é–“"
msgid "Total test time for all commits/merges"
msgstr "ã™ã¹ã¦ã®ã‚³ãƒŸãƒƒãƒˆ/マージã®åˆè¨ˆãƒ†ã‚¹ãƒˆæ™‚é–“"
+msgid "Track activity with Contribution Analytics."
+msgstr ""
+
+msgid "Unlock"
+msgstr ""
+
+msgid "Unlocked"
+msgstr ""
+
msgid "Unstar"
msgstr "スターを外ã™"
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr ""
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr ""
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr ""
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr ""
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr ""
+
msgid "Upload New File"
msgstr "æ–°è¦ãƒ•ã‚¡ã‚¤ãƒ«ã‚’アップロード"
@@ -1657,9 +2264,120 @@ msgstr "ã“ã®ãƒ‡ãƒ¼ã‚¿ã‚’å‚ç…§ã—ãŸã„ã§ã™ã‹ï¼Ÿã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã«ã¯ç®¡
msgid "We don't have enough data to show this stage."
msgstr "データä¸è¶³ã®ãŸã‚ã€ã“ã®ã‚¹ãƒ†ãƒ¼ã‚¸ã®è¡¨ç¤ºã¯ã§ãã¾ã›ã‚“。"
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr ""
+
+msgid "Weight"
+msgstr ""
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
+msgid "WikiClone|Clone your wiki"
+msgstr ""
+
+msgid "WikiClone|Git Access"
+msgstr ""
+
+msgid "WikiClone|Install Gollum"
+msgstr ""
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr ""
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr ""
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr ""
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr ""
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr ""
+
+msgid "WikiHistoricalPage|history"
+msgstr ""
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr ""
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr ""
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr ""
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr ""
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr ""
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr ""
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr ""
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr ""
+
+msgid "WikiPageConflictMessage|the page"
+msgstr ""
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr ""
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr ""
+
+msgid "WikiPage|Page slug"
+msgstr ""
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr ""
+
+msgid "Wiki|Create Page"
+msgstr ""
+
+msgid "Wiki|Create page"
+msgstr ""
+
+msgid "Wiki|Edit Page"
+msgstr ""
+
+msgid "Wiki|Empty page"
+msgstr ""
+
+msgid "Wiki|More Pages"
+msgstr ""
+
+msgid "Wiki|New page"
+msgstr ""
+
+msgid "Wiki|Page history"
+msgstr ""
+
+msgid "Wiki|Page version"
+msgstr ""
+
+msgid "Wiki|Pages"
+msgstr ""
+
+msgid "Wiki|Wiki Pages"
+msgstr ""
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr ""
+
msgid "Withdraw Access Request"
msgstr "アクセスリクエストをå–り消ã™"
@@ -1675,9 +2393,21 @@ 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 on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "ファイルを追加ã™ã‚‹ã«ã¯ã€ã©ã“ã‹ã®ãƒ–ランãƒã«ã„ãªã‘ã‚Œã°ã„ã‘ã¾ã›ã‚“"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "プロジェクト数ã®ä¸Šé™ã«é”ã—ã¦ã„ã¾ã™"
@@ -1708,6 +2438,12 @@ msgstr "%{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 "%{add_ssh_key_link} をプロファイルã«è¿½åŠ ã—ã¦ã„ãªã„ã®ã§ã€ãƒ—ロジェクトã«ã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã‚’プッシュã€ãƒ—ルã§ãã¾ã›ã‚“"
+msgid "Your comment will not be visible to the public."
+msgstr ""
+
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "åå‰"
@@ -1731,3 +2467,15 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "親"
+msgid "password"
+msgstr ""
+
+msgid "personal access token"
+msgstr ""
+
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
index de4a13d3765..ab74d4cbeae 100644
--- a/locale/ko/gitlab.po
+++ b/locale/ko/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:43-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:31-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Korean\n"
"Language: ko_KR\n"
@@ -20,6 +20,10 @@ msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d 커밋"
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] ""
+
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 추가 ì»¤ë°‹ì€ ì„±ëŠ¥ ì´ìŠˆë¥¼ 방지하기 위해 ìƒëžµë˜ì—ˆìŠµë‹ˆë‹¤."
@@ -27,6 +31,10 @@ msgstr[0] "%s 추가 ì»¤ë°‹ì€ ì„±ëŠ¥ ì´ìŠˆë¥¼ 방지하기 위해 ìƒëžµë˜ì—ˆ
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_timeago} ì— %{commit_author_link} ë‹˜ì´ ì»¤ë°‹í•˜ì˜€ìŠµë‹ˆë‹¤. "
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -46,6 +54,12 @@ msgstr[0] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr "설치 ë°©ë²•ì— ëŒ€í•œ 정보를 얻기 위해 %{link} 를 ì²´í¬ì•„웃하세요."
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d 파ì´í”„ë¼ì¸"
@@ -53,6 +67,9 @@ msgstr[0] "%d 파ì´í”„ë¼ì¸"
msgid "1st contribution!"
msgstr ""
+msgid "2FA enabled"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "지ì†ì ì¸ í†µí•©ì— ê´€í•œ 그래프 모ìŒ"
@@ -77,12 +94,18 @@ msgstr "활성"
msgid "Activity"
msgstr "활ë™"
+msgid "Add"
+msgstr ""
+
msgid "Add Changelog"
msgstr "변경 로그 추가"
msgid "Add Contribution guide"
msgstr "기여 ê°€ì´ë“œ 추가"
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Add License"
msgstr "ë¼ì´ì„ ìŠ¤ 추가"
@@ -92,9 +115,18 @@ msgstr "í”„ë¡œí•„ì— SSH 키를 추가하여 SSH를 통해 Pull 하거나 Pushí•
msgid "Add new directory"
msgstr "새 디렉토리 추가"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr "ì „ì²´"
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -110,6 +142,9 @@ msgstr "ì´ íŒŒì´í”„ë¼ì¸ ìŠ¤ì¼€ì¥´ì„ ì‚­ì œ 하시겠습니까?"
msgid "Are you sure you want to discard your changes?"
msgstr "변경 ë‚´ìš©ì„ ì·¨ì†Œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?"
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "ë“±ë¡ í† í°ì„ 초기화 하시겠습니까?"
@@ -128,25 +163,34 @@ msgstr "드래그 &amp; 드롭 ë˜ëŠ” %{upload_link}"
msgid "Authentication Log"
msgstr ""
-msgid "Auto DevOps (Beta)"
+msgid "Author"
msgstr ""
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto DevOps documentation"
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgid "AutoDevOps|Auto DevOps documentation"
+msgstr ""
+
+msgid "AutoDevOps|Enable in settings"
+msgstr ""
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
-msgid "AutoDevOps|Learn more in the"
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
msgid "Billing"
@@ -164,6 +208,9 @@ msgstr ""
msgid "BillingPlans|Customer Support"
msgstr ""
+msgid "BillingPlans|Downgrade"
+msgstr ""
+
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
msgstr ""
@@ -200,12 +247,6 @@ msgstr ""
msgid "BillingPlans|per user"
msgstr ""
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
-
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "브랜치"
@@ -213,6 +254,9 @@ 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 has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "브랜치 검색"
@@ -327,12 +371,18 @@ msgstr ""
msgid "CI configuration"
msgstr "CI 설정"
+msgid "CICD|Jobs"
+msgstr ""
+
msgid "Cancel"
msgstr "취소"
msgid "Cancel edit"
msgstr ""
+msgid "Change Weight"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "브랜치ì—ì„œ Pick"
@@ -360,6 +410,9 @@ msgstr "ì´ ì»¤ë°‹ì„ Cherry-pick"
msgid "Cherry-pick this merge request"
msgstr "ì´ ë¨¸ì§€ 리퀘스트를 Cherry-pick"
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr ""
+
msgid "CiStatusLabel|canceled"
msgstr "취소ë¨"
@@ -414,6 +467,147 @@ msgstr "건너 뜀"
msgid "CiStatus|running"
msgstr "실행 중"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
+msgid "Clone repository"
+msgstr ""
+
+msgid "Close"
+msgstr ""
+
+msgid "Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Create cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr ""
+
+msgid "ClusterIntegration|Machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr ""
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
+msgstr ""
+
+msgid "ClusterIntegration|See your projects"
+msgstr ""
+
+msgid "ClusterIntegration|See zones"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr ""
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|cluster"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -421,6 +615,13 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "커밋"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+
+msgid "Commit Message"
+msgstr ""
+
msgid "Commit duration in minutes for last 30 commits"
msgstr "최근 30 ê±´ì˜ ì»¤ë°‹ 소요시간 (분)"
@@ -448,12 +649,63 @@ msgstr "커밋한 사용ìž"
msgid "Compare"
msgstr "비êµ"
+msgid "Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Created"
+msgstr ""
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr ""
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr ""
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Learn more about"
+msgstr ""
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr ""
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr ""
+
+msgid "ContainerRegistry|Remove repository"
+msgstr ""
+
+msgid "ContainerRegistry|Remove tag"
+msgstr ""
+
+msgid "ContainerRegistry|Size"
+msgstr ""
+
+msgid "ContainerRegistry|Tag"
+msgstr ""
+
+msgid "ContainerRegistry|Tag ID"
+msgstr ""
+
+msgid "ContainerRegistry|Use different image names"
+msgstr ""
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr ""
+
msgid "Contribution guide"
msgstr "ê¸°ì—¬ì— ëŒ€í•œ 안내"
msgid "Contributors"
msgstr "기여해 주신 분들"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -466,9 +718,6 @@ msgstr "ì»¤ë°‹ì˜ SHA를 í´ë¦½ë³´ë“œë¡œ 복사합니다"
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 "%{protocol}ì„ (를) 통해 Pull 하거나 Push í•  ê°œì¸ ì•¡ì„¸ìŠ¤ 토í°ì„ 만드십시오."
@@ -478,9 +727,21 @@ msgstr "디렉토리 만들기"
msgid "Create empty bare repository"
msgstr "빈 bare 저장소 만들기"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "머지 리퀘스트 만들기"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "새로 만들기 ..."
@@ -532,6 +793,12 @@ msgstr "스테ì´ì§•"
msgid "CycleAnalyticsStage|Test"
msgstr "테스트"
+msgid "DashboardProjects|All"
+msgstr ""
+
+msgid "DashboardProjects|Personal"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "cron êµ¬ë¬¸ì„ ì‚¬ìš©í•˜ì—¬ ì‚¬ìš©ìž ì •ì˜ íŒ¨í„´ ì •ì˜"
@@ -548,6 +815,9 @@ msgstr ""
msgid "Description"
msgstr "설명"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr ""
+
msgid "Details"
msgstr "ìƒì„¸"
@@ -557,6 +827,12 @@ msgstr "디렉토리 ì´ë¦„"
msgid "Discard changes"
msgstr "변경 내용 취소"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Dismiss Merge Request promotion"
+msgstr ""
+
msgid "Don't show again"
msgstr "다시 표시하지 ì•ŠìŒ"
@@ -596,9 +872,6 @@ msgstr "파ì´í”„ë¼ì¸ 스케줄 편집 %{id}"
msgid "Emails"
msgstr ""
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr "모든 ê°’ì„ ê¸°ì¤€ìœ¼ë¡œ í•„í„°"
@@ -629,12 +902,18 @@ msgstr "매주 (ì¼ìš”ì¼ ì˜¤ì „ 4ì‹œì—)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "소유ìžë¥¼ 변경하지 못했습니다"
msgid "Failed to remove the pipeline schedule"
msgstr "파ì´í”„ë¼ì¸ ìŠ¤ì¼€ì¤„ì„ ì œê±°í•˜ì§€ 못했습니다."
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "파ì¼"
@@ -660,6 +939,12 @@ msgstr[0] "í¬í¬"
msgid "ForkedFromProjectPath|Forked from"
msgstr "í¬í¬í•œ 사용ìž"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr ""
+
+msgid "Format"
+msgstr ""
+
msgid "From issue creation until deploy to production"
msgstr "ì´ìŠˆ ìƒì„±ì—ì„œ 프로ë•ì…˜ ë°°í¬ê¹Œì§€"
@@ -672,6 +957,18 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
+msgid "Geo|Groups to replicate"
+msgstr ""
+
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
+msgid "Geo|Select groups to replicate."
+msgstr ""
+
msgid "Git storage health information has been reset"
msgstr "git storage ìƒíƒœ ì •ë³´ê°€ 초기화ë˜ì—ˆìŠµë‹ˆë‹¤."
@@ -684,6 +981,9 @@ msgstr "ë‹¹ì‹ ì˜ í¬í¬ë¡œ ì´ë™í•˜ì„¸ìš”"
msgid "GoToYourFork|Fork"
msgstr "í¬í¬"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -708,6 +1008,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr "헬스 ì²´í¬"
@@ -726,28 +1071,59 @@ msgstr " 헬스 문제가 발견ë˜ì§€ 않았습니다."
msgid "HealthCheck|Unhealthy"
msgstr "비정ìƒ"
+msgid "History"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "Housekeepingì´ ì„±ê³µì ìœ¼ë¡œ 시작ë˜ì—ˆìŠµë‹ˆë‹¤"
msgid "Import repository"
msgstr "저장소 가져 오기"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Install a Runner compatible with GitLab CI"
msgstr "GitLab CI 와 호환ë˜ëŠ” Runner 설치"
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] ""
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "주기 패턴"
msgid "Introducing Cycle Analytics"
msgstr "Cycle Analytics 소개"
+msgid "Issue board focus mode"
+msgstr ""
+
+msgid "Issue boards with milestones"
+msgstr ""
+
msgid "Issue events"
msgstr "ì´ìŠˆ ì´ë²¤íŠ¸"
-msgid "Issues"
+msgid "IssueBoards|Board"
msgstr ""
-msgid "Jobs"
+msgid "IssueBoards|Boards"
+msgstr ""
+
+msgid "Issues"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -766,12 +1142,21 @@ msgstr[0] "최근 %d ì¼"
msgid "Last Pipeline"
msgstr "최근 파ì´í”„ë¼ì¸"
-msgid "Last Update"
-msgstr "최근 ì—…ë°ì´íŠ¸"
-
msgid "Last commit"
msgstr "최근 커밋"
+msgid "Last edited %{date}"
+msgstr ""
+
+msgid "Last edited by %{name}"
+msgstr ""
+
+msgid "Last update"
+msgstr ""
+
+msgid "Last updated"
+msgstr ""
+
msgid "LastPushEvent|You pushed to"
msgstr "푸쉬: "
@@ -784,6 +1169,9 @@ msgstr "ë” ìžì„¸ížˆ 알아보기"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "파ì´í”„ë¼ì¸ 스케쥴 문서로부터 ë” ì•Œì•„ë³´ê¸°"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "그룹 떠나기"
@@ -797,9 +1185,21 @@ msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "최대 %d ì´ë²¤íŠ¸ 만 표시하는 것으로 제한ë©ë‹ˆë‹¤."
+msgid "Lock"
+msgstr ""
+
+msgid "Locked"
+msgstr ""
+
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "중앙값"
@@ -827,6 +1227,12 @@ msgstr ""
msgid "More information is available|here"
msgstr "여기"
+msgid "Multiple issue boards"
+msgstr ""
+
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "새 ì´ìŠˆ"
@@ -843,27 +1249,42 @@ msgstr "새 디렉토리"
msgid "New file"
msgstr "새 파ì¼"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "새 ì´ìŠˆ"
msgid "New merge request"
msgstr "새 머지 리퀘스트"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "새 ì¼ì •"
msgid "New snippet"
msgstr "새 스니펫"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "새 태그 "
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr ""
+
msgid "No repository"
msgstr "저장소 ì—†ìŒ"
msgid "No schedules"
msgstr "ì¼ì • ì—†ìŒ"
+msgid "None"
+msgstr ""
+
msgid "Not available"
msgstr "사용할 수 ì—†ìŒ"
@@ -927,12 +1348,24 @@ msgstr "Watch"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "í•„í„°"
+msgid "Only project members can comment."
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "열린"
+msgid "Opens in a new window"
+msgstr ""
+
msgid "Options"
msgstr "옵션 "
@@ -957,6 +1390,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr ""
+
msgid "Pipeline"
msgstr "파ì´í”„ë¼ì¸"
@@ -1059,9 +1495,54 @@ msgstr "스테ì´ì§•"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "'%{project_name}'프로ì íŠ¸ê°€ ì‚­ì œ 처리 중입니다."
@@ -1071,9 +1552,6 @@ msgstr "'%{project_name}'프로ì íŠ¸ê°€ 성공ì ìœ¼ë¡œ ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤."
msgid "Project '%{project_name}' was successfully updated."
msgstr "'%{project_name}'프로ì íŠ¸ê°€ 성공ì ìœ¼ë¡œ ì—…ë°ì´íŠ¸ë˜ì—ˆìŠµë‹ˆë‹¤."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "'%{project_name}'프로ì íŠ¸ê°€ ì‚­ì œë©ë‹ˆë‹¤."
-
msgid "Project access must be granted explicitly to each user."
msgstr "프로ì íŠ¸ 액세스는 ê° ì‚¬ìš©ìžì—게 명시ì ìœ¼ë¡œ 부여ë˜ì–´ì•¼í•©ë‹ˆë‹¤."
@@ -1116,6 +1594,27 @@ msgstr "스테ì´ì§•"
msgid "ProjectNetworkGraph|Graph"
msgstr "그래프"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr ""
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr ""
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1137,12 +1636,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr "푸쉬 ì´ë²¤íŠ¸"
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "ë” ì½ê¸°"
@@ -1206,6 +1714,12 @@ msgstr "ì´ ë¨¸ì§€ 리퀘스트 ë˜ëŒë¦¬ê¸°"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
+msgid "Save changes"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "파ì´í”„ë¼ì¸ 스케줄 저장"
@@ -1221,15 +1735,21 @@ msgstr "파ì´í”„ë¼ì¸ 스케줄ë§"
msgid "Search branches and tags"
msgstr "브랜치 ë° íƒœê·¸ 검색"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "ì•„ì¹´ì´ë¸Œ í¬ë§· ì„ íƒ"
msgid "Select a timezone"
msgstr "시간대 ì„ íƒ"
-msgid "Select existing branch"
-msgstr ""
-
msgid "Select target branch"
msgstr "ëŒ€ìƒ ë¸Œëžœì¹˜ ì„ íƒ"
@@ -1267,6 +1787,21 @@ msgstr[0] "%d ê°œì˜ ì´ë²¤íŠ¸ 표시 중"
msgid "Snippets"
msgstr ""
+msgid "Something went wrong on our end."
+msgstr ""
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
+msgid "Something went wrong while fetching the projects."
+msgstr ""
+
+msgid "Something went wrong while fetching the registry list."
+msgstr ""
+
+msgid "Sort by"
+msgstr ""
+
msgid "SortOptions|Access level, ascending"
msgstr ""
@@ -1384,6 +1919,12 @@ msgstr "ì´ ë³€ê²½ 사항으로 %{new_merge_request} ì„ ì‹œìž‘í•˜ì‹­ì‹œì˜¤."
msgid "Start the Runner!"
msgstr "Runner 시작!"
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "스위치 브랜치/태그"
@@ -1403,6 +1944,15 @@ msgstr "ëŒ€ìƒ ë¸Œëžœì¹˜"
msgid "Team"
msgstr ""
+msgid "Thanks! Don't show me this again"
+msgstr ""
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr ""
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+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 "Coding Stage는 첫 번째 커밋ì—서부터 머지 리퀘스트 ìƒì„±ê¹Œì§€ì˜ ì‹œê°„ì„ ë³´ì—¬ì¤ë‹ˆë‹¤. 첫 번째 머지 ë¦¬í€˜ìŠ¤íŠ¸ì„ ìƒì„±í•˜ë©´ ë°ì´í„°ê°€ ìžë™ìœ¼ë¡œ ì—¬ê¸°ì— ì¶”ê°€ë©ë‹ˆë‹¤."
@@ -1415,6 +1965,15 @@ 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 number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "개발 ìˆ˜ëª…ì£¼ê¸°ì˜ ë‹¨ê³„."
@@ -1445,6 +2004,12 @@ msgstr "Staging 단계ì—서는 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 time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "해당 단계ì—ì„œ 수집 í•œ ê° ë°ì´í„° ìž…ë ¥ì— ì†Œìš” ëœ ì‹œê°„"
@@ -1454,12 +2019,27 @@ msgstr "ê°’ì€ ì¼ë ¨ì˜ 관측 ê°’ 중ì ì— 있습니다. 예를 들어, 3, 5,
msgid "There are problems accessing Git storage: "
msgstr "git storageì— ì ‘ê·¼í•˜ëŠ”ë° ë¬¸ì œê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. "
-msgid "This is the author's first Merge Request to this project. Handle with care."
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
+msgid "This is a confidential issue."
+msgstr ""
+
+msgid "This is the author's first Merge Request to this project."
+msgstr ""
+
+msgid "This issue is confidential and locked."
+msgstr ""
+
+msgid "This issue is locked."
msgstr ""
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "즉, 빈 저장소를 만들거나 기존 저장소를 가져올 때까지 코드를 Push 할 수 없습니다."
+msgid "This merge request is locked."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "ì´ìŠˆê°€ 스케줄ë˜ê¸° ì „ì˜ ì‹œê°„"
@@ -1538,9 +2118,6 @@ msgstr "1 달 전"
msgid "Timeago|a week ago"
msgstr "1 ì£¼ì¼ ì „"
-msgid "Timeago|a while"
-msgstr "잠시 전"
-
msgid "Timeago|a year ago"
msgstr "1 ë…„ ì „"
@@ -1592,6 +2169,9 @@ msgstr "1 ì£¼ì¼ ì´ë‚´"
msgid "Timeago|in 1 year"
msgstr "1 ë…„ ì´ë‚´"
+msgid "Timeago|in a while"
+msgstr ""
+
msgid "Timeago|less than a minute ago"
msgstr "1 분미만"
@@ -1612,9 +2192,36 @@ msgstr "시간 합계:"
msgid "Total test time for all commits/merges"
msgstr "모든 커밋 / ë¨¸ì§€ì˜ ì´ í…ŒìŠ¤íŠ¸ 시간"
+msgid "Track activity with Contribution Analytics."
+msgstr ""
+
+msgid "Unlock"
+msgstr ""
+
+msgid "Unlocked"
+msgstr ""
+
msgid "Unstar"
msgstr "별표 제거"
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr ""
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr ""
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr ""
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr ""
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr ""
+
msgid "Upload New File"
msgstr "새 íŒŒì¼ ì—…ë¡œë“œ"
@@ -1657,9 +2264,120 @@ msgstr "ì´ ë°ì´í„°ë¥¼ ë³´ê³  싶ì€ê°€ìš”? 관리ìžì—게 액세스 권한ì
msgid "We don't have enough data to show this stage."
msgstr "ì´ ë‹¨ê³„ë¥¼ ë³´ì—¬ì£¼ê¸°ì— ì¶©ë¶„í•œ ë°ì´í„°ê°€ 없습니다."
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr ""
+
+msgid "Weight"
+msgstr ""
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
+msgid "WikiClone|Clone your wiki"
+msgstr ""
+
+msgid "WikiClone|Git Access"
+msgstr ""
+
+msgid "WikiClone|Install Gollum"
+msgstr ""
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr ""
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr ""
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr ""
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr ""
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr ""
+
+msgid "WikiHistoricalPage|history"
+msgstr ""
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr ""
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr ""
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr ""
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr ""
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr ""
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr ""
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr ""
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr ""
+
+msgid "WikiPageConflictMessage|the page"
+msgstr ""
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr ""
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr ""
+
+msgid "WikiPage|Page slug"
+msgstr ""
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr ""
+
+msgid "Wiki|Create Page"
+msgstr ""
+
+msgid "Wiki|Create page"
+msgstr ""
+
+msgid "Wiki|Edit Page"
+msgstr ""
+
+msgid "Wiki|Empty page"
+msgstr ""
+
+msgid "Wiki|More Pages"
+msgstr ""
+
+msgid "Wiki|New page"
+msgstr ""
+
+msgid "Wiki|Page history"
+msgstr ""
+
+msgid "Wiki|Page version"
+msgstr ""
+
+msgid "Wiki|Pages"
+msgstr ""
+
+msgid "Wiki|Wiki Pages"
+msgstr ""
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr ""
+
msgid "Withdraw Access Request"
msgstr "액세스 요청 철회"
@@ -1675,9 +2393,21 @@ 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 on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "ë¸Œëžœì¹˜ì— ìžˆì„ ë•Œì—만 파ì¼ì„ 추가 í•  수 있습니다."
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "프로ì íŠ¸ ìˆ«ìž í•œë„ì— ë„달했습니다."
@@ -1708,6 +2438,12 @@ 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"
msgstr "ë‹¹ì‹ ì˜ í”„ë¡œí•„ì— %{add_ssh_key_link} 를 하기 ì „ì—는 SSH를 통해 프로ì íŠ¸ 코드를 Pull 하거나 Push í•  수 없습니다"
+msgid "Your comment will not be visible to the public."
+msgstr ""
+
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "ê·€í•˜ì˜ ì´ë¦„"
@@ -1731,3 +2467,15 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "부모"
+msgid "password"
+msgstr ""
+
+msgid "personal access token"
+msgstr ""
+
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/nl_NL/gitlab.po b/locale/nl_NL/gitlab.po
index 45a444fac43..7e33af9f747 100644
--- a/locale/nl_NL/gitlab.po
+++ b/locale/nl_NL/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:43-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:31-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -21,6 +21,11 @@ msgid_plural "%d commits"
msgstr[0] "%d commit"
msgstr[1] "%d commits"
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] "%s andere commit is weggelaten om prestatieproblemen te voorkomen."
@@ -29,6 +34,11 @@ msgstr[1] "%s andere commits zijn weggelaten om prestatieproblemen te voorkomen.
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr ""
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -49,6 +59,12 @@ msgstr[1] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(bekijk de %{link} voor meer info over hoe je het kan installeren)."
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -57,6 +73,9 @@ msgstr[1] ""
msgid "1st contribution!"
msgstr ""
+msgid "2FA enabled"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr ""
@@ -81,12 +100,18 @@ msgstr "Actief"
msgid "Activity"
msgstr "Activiteit"
+msgid "Add"
+msgstr "Voeg toe"
+
msgid "Add Changelog"
msgstr "Changelog toevoegen"
msgid "Add Contribution guide"
msgstr ""
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Add License"
msgstr "Licentie toevoegen"
@@ -96,12 +121,21 @@ msgstr ""
msgid "Add new directory"
msgstr "Nieuwe map toevoegen"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr "Alles"
-msgid "Appearance"
+msgid "An error occurred. Please try again."
msgstr ""
+msgid "Appearance"
+msgstr "Uiterlijk"
+
msgid "Applications"
msgstr "Applicaties"
@@ -114,6 +148,9 @@ msgstr ""
msgid "Are you sure you want to discard your changes?"
msgstr ""
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -132,30 +169,39 @@ msgstr ""
msgid "Authentication Log"
msgstr ""
-msgid "Auto DevOps (Beta)"
+msgid "Author"
+msgstr "Auteur"
+
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "Auto DevOps documentation"
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgid "AutoDevOps|Enable in settings"
msgstr ""
-msgid "AutoDevOps|Learn more in the"
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
msgstr ""
-msgid "Billing"
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
+msgstr ""
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
+msgid "Billing"
+msgstr "Facturatie"
+
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
msgstr ""
@@ -168,6 +214,9 @@ msgstr ""
msgid "BillingPlans|Customer Support"
msgstr ""
+msgid "BillingPlans|Downgrade"
+msgstr ""
+
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
msgstr ""
@@ -204,12 +253,6 @@ msgstr ""
msgid "BillingPlans|per user"
msgstr ""
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
-
msgid "Branch"
msgid_plural "Branches"
msgstr[0] ""
@@ -218,20 +261,23 @@ 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"
+msgid "Branch has changed"
msgstr ""
+msgid "BranchSwitcherPlaceholder|Search branches"
+msgstr "BranchSwitcherPlaceholder|Zoek branches"
+
msgid "BranchSwitcherTitle|Switch branch"
-msgstr ""
+msgstr "BranchSwitcherTitle|Ga naar branch"
msgid "Branches"
msgstr "Branches"
msgid "Branches|Cant find HEAD commit for this branch"
-msgstr ""
+msgstr "Branches|Kan geen HEAD-commit vinden voor deze branch"
msgid "Branches|Compare"
-msgstr ""
+msgstr "Branches|Vergelijk"
msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
msgstr ""
@@ -332,12 +378,18 @@ msgstr "CI / CD"
msgid "CI configuration"
msgstr "CI Configuratie"
+msgid "CICD|Jobs"
+msgstr ""
+
msgid "Cancel"
msgstr "Annuleren"
msgid "Cancel edit"
msgstr "Bewerken annuleren"
+msgid "Change Weight"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr ""
@@ -365,6 +417,9 @@ msgstr "Cherry-pick deze commit"
msgid "Cherry-pick this merge request"
msgstr ""
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr ""
+
msgid "CiStatusLabel|canceled"
msgstr "geannuleerd"
@@ -419,6 +474,147 @@ msgstr "overgeslagen"
msgid "CiStatus|running"
msgstr ""
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
+msgid "Clone repository"
+msgstr ""
+
+msgid "Close"
+msgstr ""
+
+msgid "Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Create cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr ""
+
+msgid "ClusterIntegration|Machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr ""
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
+msgstr ""
+
+msgid "ClusterIntegration|See your projects"
+msgstr ""
+
+msgid "ClusterIntegration|See zones"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr ""
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|cluster"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
msgid "Comments"
msgstr "Opmerkingen"
@@ -427,6 +623,14 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Commit Message"
+msgstr ""
+
msgid "Commit duration in minutes for last 30 commits"
msgstr ""
@@ -454,12 +658,63 @@ msgstr "Gecommit door"
msgid "Compare"
msgstr "Vergelijk"
+msgid "Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Created"
+msgstr ""
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr ""
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr ""
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Learn more about"
+msgstr ""
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr ""
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr ""
+
+msgid "ContainerRegistry|Remove repository"
+msgstr ""
+
+msgid "ContainerRegistry|Remove tag"
+msgstr ""
+
+msgid "ContainerRegistry|Size"
+msgstr ""
+
+msgid "ContainerRegistry|Tag"
+msgstr ""
+
+msgid "ContainerRegistry|Tag ID"
+msgstr ""
+
+msgid "ContainerRegistry|Use different image names"
+msgstr ""
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr ""
+
msgid "Contribution guide"
msgstr ""
msgid "Contributors"
msgstr ""
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -472,9 +727,6 @@ msgstr ""
msgid "Create New Directory"
msgstr ""
-msgid "Create a new branch"
-msgstr ""
-
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr ""
@@ -484,9 +736,21 @@ msgstr "Maak map aan"
msgid "Create empty bare repository"
msgstr ""
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr ""
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr ""
@@ -518,7 +782,7 @@ msgid "Cycle Analytics gives an overview of how much time it takes to go from id
msgstr ""
msgid "CycleAnalyticsStage|Code"
-msgstr ""
+msgstr "Code"
msgid "CycleAnalyticsStage|Issue"
msgstr ""
@@ -527,7 +791,7 @@ msgid "CycleAnalyticsStage|Plan"
msgstr ""
msgid "CycleAnalyticsStage|Production"
-msgstr ""
+msgstr "Productie"
msgid "CycleAnalyticsStage|Review"
msgstr ""
@@ -538,6 +802,12 @@ msgstr ""
msgid "CycleAnalyticsStage|Test"
msgstr ""
+msgid "DashboardProjects|All"
+msgstr ""
+
+msgid "DashboardProjects|Personal"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr ""
@@ -555,6 +825,9 @@ msgstr ""
msgid "Description"
msgstr ""
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr ""
+
msgid "Details"
msgstr ""
@@ -564,6 +837,12 @@ msgstr ""
msgid "Discard changes"
msgstr ""
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Dismiss Merge Request promotion"
+msgstr ""
+
msgid "Don't show again"
msgstr ""
@@ -603,9 +882,6 @@ msgstr ""
msgid "Emails"
msgstr ""
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -636,12 +912,18 @@ msgstr ""
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr ""
msgid "Failed to remove the pipeline schedule"
msgstr ""
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr ""
@@ -668,6 +950,12 @@ msgstr[1] ""
msgid "ForkedFromProjectPath|Forked from"
msgstr ""
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr ""
+
+msgid "Format"
+msgstr ""
+
msgid "From issue creation until deploy to production"
msgstr ""
@@ -680,6 +968,18 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
+msgid "Geo|Groups to replicate"
+msgstr ""
+
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
+msgid "Geo|Select groups to replicate."
+msgstr ""
+
msgid "Git storage health information has been reset"
msgstr ""
@@ -692,6 +992,9 @@ msgstr ""
msgid "GoToYourFork|Fork"
msgstr ""
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -716,6 +1019,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr ""
@@ -734,28 +1082,60 @@ msgstr ""
msgid "HealthCheck|Unhealthy"
msgstr ""
+msgid "History"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
+msgid "Issue board focus mode"
+msgstr ""
+
+msgid "Issue boards with milestones"
+msgstr ""
+
msgid "Issue events"
msgstr ""
-msgid "Issues"
+msgid "IssueBoards|Board"
+msgstr ""
+
+msgid "IssueBoards|Boards"
msgstr ""
-msgid "Jobs"
+msgid "Issues"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -775,10 +1155,19 @@ msgstr[1] ""
msgid "Last Pipeline"
msgstr ""
-msgid "Last Update"
+msgid "Last commit"
msgstr ""
-msgid "Last commit"
+msgid "Last edited %{date}"
+msgstr ""
+
+msgid "Last edited by %{name}"
+msgstr ""
+
+msgid "Last update"
+msgstr ""
+
+msgid "Last updated"
msgstr ""
msgid "LastPushEvent|You pushed to"
@@ -793,6 +1182,9 @@ msgstr ""
msgid "Learn more in the|pipeline schedules documentation"
msgstr ""
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr ""
@@ -807,9 +1199,21 @@ msgid_plural "Limited to showing %d events at most"
msgstr[0] ""
msgstr[1] ""
+msgid "Lock"
+msgstr ""
+
+msgid "Locked"
+msgstr ""
+
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr ""
@@ -837,6 +1241,12 @@ msgstr ""
msgid "More information is available|here"
msgstr ""
+msgid "Multiple issue boards"
+msgstr ""
+
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nieuwe issue"
@@ -854,27 +1264,42 @@ msgstr ""
msgid "New file"
msgstr ""
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr ""
msgid "New merge request"
msgstr ""
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr ""
msgid "New snippet"
msgstr ""
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr ""
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr ""
+
msgid "No repository"
msgstr ""
msgid "No schedules"
msgstr ""
+msgid "None"
+msgstr ""
+
msgid "Not available"
msgstr ""
@@ -938,10 +1363,22 @@ msgstr ""
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr ""
+msgid "Only project members can comment."
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
+msgstr "Geopend"
+
+msgid "Opens in a new window"
msgstr ""
msgid "Options"
@@ -968,6 +1405,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr ""
+
msgid "Pipeline"
msgstr ""
@@ -1070,9 +1510,54 @@ msgstr ""
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr ""
@@ -1082,9 +1567,6 @@ 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 ""
@@ -1127,6 +1609,27 @@ msgstr ""
msgid "ProjectNetworkGraph|Graph"
msgstr ""
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr ""
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr ""
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1148,12 +1651,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr ""
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr ""
@@ -1217,6 +1729,12 @@ msgstr ""
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
+msgid "Save changes"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr ""
@@ -1232,13 +1750,19 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
-msgid "Select Archive Format"
+msgid "Seconds before reseting failure information"
msgstr ""
-msgid "Select a timezone"
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Select existing branch"
+msgid "Select Archive Format"
+msgstr ""
+
+msgid "Select a timezone"
msgstr ""
msgid "Select target branch"
@@ -1279,6 +1803,21 @@ msgstr[1] ""
msgid "Snippets"
msgstr ""
+msgid "Something went wrong on our end."
+msgstr ""
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
+msgid "Something went wrong while fetching the projects."
+msgstr ""
+
+msgid "Something went wrong while fetching the registry list."
+msgstr ""
+
+msgid "Sort by"
+msgstr ""
+
msgid "SortOptions|Access level, ascending"
msgstr ""
@@ -1396,6 +1935,12 @@ msgstr ""
msgid "Start the Runner!"
msgstr ""
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr ""
@@ -1416,6 +1961,15 @@ msgstr ""
msgid "Team"
msgstr ""
+msgid "Thanks! Don't show me this again"
+msgstr ""
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr ""
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+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 ""
@@ -1428,6 +1982,15 @@ 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 number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr ""
@@ -1458,6 +2021,12 @@ msgstr ""
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr ""
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr ""
@@ -1467,12 +2036,27 @@ msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "This is the author's first Merge Request to this project. Handle with care."
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
+msgid "This is a confidential issue."
+msgstr ""
+
+msgid "This is the author's first Merge Request to this project."
+msgstr ""
+
+msgid "This issue is confidential and locked."
+msgstr ""
+
+msgid "This issue is locked."
msgstr ""
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr ""
+msgid "This merge request is locked."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr ""
@@ -1551,9 +2135,6 @@ msgstr ""
msgid "Timeago|a week ago"
msgstr ""
-msgid "Timeago|a while"
-msgstr ""
-
msgid "Timeago|a year ago"
msgstr ""
@@ -1605,6 +2186,9 @@ msgstr ""
msgid "Timeago|in 1 year"
msgstr ""
+msgid "Timeago|in a while"
+msgstr ""
+
msgid "Timeago|less than a minute ago"
msgstr ""
@@ -1619,7 +2203,7 @@ msgstr[0] ""
msgstr[1] ""
msgid "Time|s"
-msgstr ""
+msgstr "s"
msgid "Total Time"
msgstr ""
@@ -1627,9 +2211,36 @@ msgstr ""
msgid "Total test time for all commits/merges"
msgstr ""
+msgid "Track activity with Contribution Analytics."
+msgstr ""
+
+msgid "Unlock"
+msgstr ""
+
+msgid "Unlocked"
+msgstr ""
+
msgid "Unstar"
msgstr ""
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr ""
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr ""
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr ""
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr ""
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr ""
+
msgid "Upload New File"
msgstr ""
@@ -1658,7 +2269,7 @@ msgid "VisibilityLevel|Internal"
msgstr ""
msgid "VisibilityLevel|Private"
-msgstr ""
+msgstr "Privé"
msgid "VisibilityLevel|Public"
msgstr ""
@@ -1672,9 +2283,120 @@ msgstr ""
msgid "We don't have enough data to show this stage."
msgstr ""
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr ""
+
+msgid "Weight"
+msgstr ""
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
+msgid "WikiClone|Clone your wiki"
+msgstr ""
+
+msgid "WikiClone|Git Access"
+msgstr ""
+
+msgid "WikiClone|Install Gollum"
+msgstr ""
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr ""
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr ""
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr ""
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr ""
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr ""
+
+msgid "WikiHistoricalPage|history"
+msgstr ""
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr ""
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr ""
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr ""
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr ""
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr ""
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr ""
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr ""
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr ""
+
+msgid "WikiPageConflictMessage|the page"
+msgstr ""
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr ""
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr ""
+
+msgid "WikiPage|Page slug"
+msgstr ""
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr ""
+
+msgid "Wiki|Create Page"
+msgstr ""
+
+msgid "Wiki|Create page"
+msgstr ""
+
+msgid "Wiki|Edit Page"
+msgstr ""
+
+msgid "Wiki|Empty page"
+msgstr ""
+
+msgid "Wiki|More Pages"
+msgstr ""
+
+msgid "Wiki|New page"
+msgstr ""
+
+msgid "Wiki|Page history"
+msgstr ""
+
+msgid "Wiki|Page version"
+msgstr ""
+
+msgid "Wiki|Pages"
+msgstr ""
+
+msgid "Wiki|Wiki Pages"
+msgstr ""
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr ""
+
msgid "Withdraw Access Request"
msgstr ""
@@ -1690,9 +2412,21 @@ msgstr ""
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr ""
+msgid "You are on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr ""
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr ""
@@ -1723,6 +2457,12 @@ 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 comment will not be visible to the public."
+msgstr ""
+
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr ""
@@ -1748,3 +2488,15 @@ msgid_plural "parents"
msgstr[0] ""
msgstr[1] ""
+msgid "password"
+msgstr ""
+
+msgid "personal access token"
+msgstr ""
+
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index 318c719c2ed..473086886ac 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:42-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-04 13:27-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\n"
@@ -18,8 +18,13 @@ msgstr ""
msgid "%d commit"
msgid_plural "%d commits"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d commit"
+msgstr[1] "%d commits"
+
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] "%d camada"
+msgstr[1] "%d camadas"
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
@@ -29,33 +34,47 @@ msgstr[1] "%s commits adicionais foram omitidos para prevenir problemas de perfo
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} fez commit %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] "%{count} participante"
+msgstr[1] "%{count} participantes"
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
-msgstr ""
+msgstr "%{number_commits_behind} commits atrás de %{default_branch}, %{number_commits_ahead} commits à frente"
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
-msgstr ""
+msgstr "%{number_of_failures} de %{maximum_failures} falhas. O GitLab permitirá o acesso na próxima tentativa."
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds."
-msgstr ""
+msgstr "%{number_of_failures} de %{maximum_failures} falhas. O GitLab irá bloquear o acesso por %{number_of_seconds} segundos."
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
-msgstr ""
+msgstr "%{number_of_failures} de %{maximum_failures} falhas. O GitLab não tentará mais automaticamente. Redefina as informações de storage quando o problema for resolvido."
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%{storage_name}: falha na tentativa de acesso ao storage no host:"
+msgstr[1] "%{storage_name}: %{failed_attempts} falhas de acesso ao storage:"
msgid "(checkout the %{link} for information on how to install it)."
-msgstr ""
+msgstr "(veja o %{link} para informações de como instalar)."
+
+msgid "+ %{moreCount} more"
+msgstr "%{moreCount} mais"
+
+msgid "- show less"
+msgstr "- exibir menos"
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "1 pipeline"
+msgstr[1] "%d pipelines"
msgid "1st contribution!"
-msgstr ""
+msgstr "1ª contribuição!"
+
+msgid "2FA enabled"
+msgstr "Autenticação de 2 passos ativada"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Uma coleção de gráficos sobre Integração Contínua"
@@ -64,16 +83,16 @@ msgid "About auto deploy"
msgstr "Sobre o deploy automático"
msgid "Abuse Reports"
-msgstr ""
+msgstr "Relatórios de abuso"
msgid "Access Tokens"
-msgstr ""
+msgstr "Tokens de acesso"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
-msgstr ""
+msgstr "Os acessos à storages com defeito foram temporariamente desabilitados para permitir a sua remontagem. Redefina as informações de armazenamento depois que o problema foi resolvido para permitir o acesso de novo."
msgid "Account"
-msgstr ""
+msgstr "Conta"
msgid "Active"
msgstr "Ativo"
@@ -81,12 +100,18 @@ msgstr "Ativo"
msgid "Activity"
msgstr "Atividade"
+msgid "Add"
+msgstr "Adicionar"
+
msgid "Add Changelog"
msgstr "Adicionar registro de mudanças"
msgid "Add Contribution guide"
msgstr "Adicionar Guia de contribuição"
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr "Adicione o grupo de Webhooks e GitLab Enterprise Edition."
+
msgid "Add License"
msgstr "Adicionar Licença"
@@ -96,14 +121,23 @@ msgstr "Adicionar chave SSH ao seu perfil para fazer pull ou push via SSH."
msgid "Add new directory"
msgstr "Adicionar novo diretório"
+msgid "AdminHealthPageLink|health page"
+msgstr "página de saúde"
+
+msgid "Advanced settings"
+msgstr "Configurações avançadas"
+
msgid "All"
-msgstr ""
+msgstr "Todos"
+
+msgid "An error occurred. Please try again."
+msgstr "Ocorreu um erro. Tente novamente."
msgid "Appearance"
-msgstr ""
+msgstr "Aparência"
msgid "Applications"
-msgstr ""
+msgstr "Aplicações"
msgid "Archived project! Repository is read-only"
msgstr "Projeto arquivado! O repositório é somente leitura"
@@ -112,112 +146,124 @@ 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 ""
+msgstr "Você tem certeza que deseja descartar suas alterações?"
+
+msgid "Are you sure you want to leave this group?"
+msgstr "Tem certeza que quer sair desse grupo?"
msgid "Are you sure you want to reset registration token?"
-msgstr ""
+msgstr "Você tem certeza que quer recriar o token de registro?"
msgid "Are you sure you want to reset the health check token?"
-msgstr ""
+msgstr "Você tem certeza que quer reiniciar o token de status de saúde?"
msgid "Are you sure?"
-msgstr ""
+msgstr "Você tem certeza?"
msgid "Artifacts"
-msgstr ""
+msgstr "Artefatos"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Para anexar arquivo, arraste e solte ou %{upload_link}"
msgid "Authentication Log"
-msgstr ""
+msgstr "Log de autenticação"
-msgid "Auto DevOps (Beta)"
-msgstr ""
-
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
-msgid "Auto DevOps documentation"
-msgstr ""
+msgid "Author"
+msgstr "Autor"
msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Apps de revisão automática e Auto Deploy precisam de um nome de domínio e o %{kubernetes} para que funcione corretamente."
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
-msgstr ""
+msgstr "Apps de revisão automática e Auto Deploy precisam de um nome de domínio para que funcione corretamente."
msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Apps de revisão automática e Auto Deploy precisam do %{kubernetes} para que funcione corretamente."
-msgid "AutoDevOps|Learn more in the"
-msgstr ""
+msgid "AutoDevOps|Auto DevOps (Beta)"
+msgstr "Auto DevOps (Beta)"
+
+msgid "AutoDevOps|Auto DevOps documentation"
+msgstr "Documentação de auto DevOps"
+
+msgid "AutoDevOps|Enable in settings"
+msgstr "Habilitar nas configurações"
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr "Ele gerará a build, testará e fará deploy de sua aplicação automaticamente com base em uma configuração predefinida do CI/CD."
+
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
+msgstr "Saiba mais em %{link_to_documentation}"
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr "Você pode ativar %{link_to_settings} para esse projeto."
msgid "Billing"
-msgstr ""
+msgstr "Cobrança"
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr ""
+msgstr "Grupo %{group_name} está atualmente no plano %{plan_link}."
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr ""
+msgstr "Downgrade e upgrade automático para alguns planos ainda não está disponível."
msgid "BillingPlans|Current plan"
-msgstr ""
+msgstr "Plano atual"
msgid "BillingPlans|Customer Support"
-msgstr ""
+msgstr "Suporte ao cliente"
+
+msgid "BillingPlans|Downgrade"
+msgstr "Downgrade"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr ""
+msgstr "Aprenda mais sobre cada plano lendo nossa %{faq_link}."
msgid "BillingPlans|Manage plan"
-msgstr ""
+msgstr "Gerenciar plano"
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr ""
+msgstr "Por favor contacte o %{customer_support_link} para resolver seu caso."
msgid "BillingPlans|See all %{plan_name} features"
-msgstr ""
+msgstr "Veja todas as funcionalidades do seu plano %{plan_name}"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr ""
+msgstr "Esse grupo utiliza o plano associado ao seu grupo pai."
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr ""
+msgstr "Para gerenciar o plano para esse grupo, visite a sessão de cobrança de %{parent_billing_page_link}."
msgid "BillingPlans|Upgrade"
-msgstr ""
+msgstr "Atualizar"
msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr ""
+msgstr "Vocês está utilizando o plano %{plan_link}."
msgid "BillingPlans|frequently asked questions"
-msgstr ""
+msgstr "perguntas frequentes"
msgid "BillingPlans|monthly"
-msgstr ""
+msgstr "mensalmente"
msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr ""
+msgstr "pago %{price_per_year} anualmente"
msgid "BillingPlans|per user"
-msgstr ""
-
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
+msgstr "por usuário"
msgid "Branch"
msgid_plural "Branches"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Branch"
+msgstr[1] "Branches"
msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr "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 has changed"
+msgstr "Branch foi alterado"
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Procurar por branches"
@@ -225,91 +271,91 @@ msgid "BranchSwitcherTitle|Switch branch"
msgstr "Mudar de branch"
msgid "Branches"
-msgstr ""
+msgstr "Branches"
msgid "Branches|Cant find HEAD commit for this branch"
-msgstr ""
+msgstr "Não foi possível encontrar o commit HEAD para essa branch"
msgid "Branches|Compare"
-msgstr ""
+msgstr "Comparar"
msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
-msgstr ""
+msgstr "Apagar todas as branches que foi feito merge em '%{default_branch}'"
msgid "Branches|Delete branch"
-msgstr ""
+msgstr "Apagar branch"
msgid "Branches|Delete merged branches"
-msgstr ""
+msgstr "Apagar branches que foram feito merge"
msgid "Branches|Delete protected branch"
-msgstr ""
+msgstr "Apagar branch protegida"
msgid "Branches|Delete protected branch '%{branch_name}'?"
-msgstr ""
+msgstr "Apagar branch protegida '%{branch_name}'?"
msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
-msgstr ""
+msgstr "Apagar a branch '%{branch_name}' não pode ser desfeito. Você tem certeza?"
msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
-msgstr ""
+msgstr "Apagar branches que foram feito merge não pode ser desfeito. Você tem certeza?"
msgid "Branches|Filter by branch name"
-msgstr ""
+msgstr "Filtrar por nome de branch"
msgid "Branches|Merged into %{default_branch}"
-msgstr ""
+msgstr "Merge feito na branch '%{default_branch}'"
msgid "Branches|New branch"
-msgstr ""
+msgstr "Nova branch"
msgid "Branches|No branches to show"
-msgstr ""
+msgstr "Nenhuma branch para mostrar"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
-msgstr ""
+msgstr "Uma vez que você confirmar e pressionar %{delete_protected_branch}, não pode ser desfeito ou recuperado."
msgid "Branches|Only a project master or owner can delete a protected branch"
-msgstr ""
+msgstr "Somente alguém master ou dono do projeto poderá apagar branches protegidas"
msgid "Branches|Protected branches can be managed in %{project_settings_link}"
-msgstr ""
+msgstr "Ramos protegidos podem ser gerenciados em %{project_settings_link}"
msgid "Branches|Sort by"
-msgstr ""
+msgstr "Ordernar por"
msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
+msgstr "A branch não pode ser atualizada automaticamente porque diverge do seu upstream."
msgid "Branches|The default branch cannot be deleted"
-msgstr ""
+msgstr "A branch padrão não pode ser apagada"
msgid "Branches|This branch hasn’t been merged into %{default_branch}."
-msgstr ""
+msgstr "Essa branch não teve o merge realizado na '%{default_branch}'."
msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
-msgstr ""
+msgstr "Para evitar perda de dados, considere fazer um merge dessa branch antes de apagá-la."
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
-msgstr ""
+msgstr "Para confirmar, digite %{branch_name_confirmation}:"
msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
+msgstr "Para descartar as mudanças locais e sobrescrever a branch com a versão de upstream, apague-o aqui e escolha 'Atualizar agora', acima."
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
-msgstr ""
+msgstr "Você irá apagar irreparavelmente a branch protegida '%{branch_name}'."
msgid "Branches|diverged from upstream"
-msgstr ""
+msgstr "divergiu do upstream"
msgid "Branches|merged"
-msgstr ""
+msgstr "merge realizado"
msgid "Branches|project settings"
-msgstr ""
+msgstr "configurações do projeto"
msgid "Branches|protected"
-msgstr ""
+msgstr "protegido"
msgid "Browse Directory"
msgstr "Navegar no Diretório"
@@ -327,16 +373,22 @@ msgid "ByAuthor|by"
msgstr "por"
msgid "CI / CD"
-msgstr ""
+msgstr "CI / CD"
msgid "CI configuration"
msgstr "Configuração da IC"
+msgid "CICD|Jobs"
+msgstr "Jobs"
+
msgid "Cancel"
msgstr "Cancelar"
msgid "Cancel edit"
-msgstr ""
+msgstr "Cancelar edição"
+
+msgid "Change Weight"
+msgstr "Mudar peso"
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Pick para um branch"
@@ -357,7 +409,7 @@ msgid "Charts"
msgstr "Gráficos"
msgid "Chat"
-msgstr ""
+msgstr "Bate-papo"
msgid "Cherry-pick this commit"
msgstr "Cherry-pick esse commit"
@@ -365,6 +417,9 @@ msgstr "Cherry-pick esse commit"
msgid "Cherry-pick this merge request"
msgstr "Cherry-pick esse merge request"
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr "Escolha quais os grupos que você deseja replicar para este nó secundário. Deixe em branco para replicar todos."
+
msgid "CiStatusLabel|canceled"
msgstr "cancelado"
@@ -419,13 +474,162 @@ msgstr "ignorado"
msgid "CiStatus|running"
msgstr "executando"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr "interruptor da api"
+
+msgid "Clone repository"
+msgstr "Clonar repositório"
+
+msgid "Close"
+msgstr "Fechar"
+
+msgid "Cluster"
+msgstr "Cluster"
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr "Um %{link_to_container_project} deve ter sido criado com essa conta"
+
+msgid "ClusterIntegration|Cluster details"
+msgstr "Detalhes do cluster"
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr "Integração do cluster"
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr "Integração do cluster está desabilitada para esse projeto."
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr "Integração do cluster está ativada nesse projeto."
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr "Integração do cluster está ativada para esse projeto. Desabilitar a integração não afetará seu cluster, mas desligará temporariamente a conexão do Gitlab com ele."
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr "O cluster está sendo criado no Google Container Engine..."
+
+msgid "ClusterIntegration|Cluster name"
+msgstr "Nome do cluster"
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr "O cluster foi criado com sucesso no Google Container Engine"
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr "Copiar nome do cluster"
+
+msgid "ClusterIntegration|Create cluster"
+msgstr "Criar cluster"
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr "Criar novo cluster no Google Container Engine"
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr "Ativar integração com o cluster"
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr "ID do projeto no Google Cloud Platform"
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr "Google Container Engine"
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr "Projeto no Google Container Engine"
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr "Leia mais sobre %{link_to_documentation}"
+
+msgid "ClusterIntegration|Machine type"
+msgstr "Tipo de máquina"
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr "Confira se sua conta %{link_to_requirements} para criar clusters"
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr "Gerenciar integração de cluster com o projeto no GitLab"
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr "Gerencie seu cluster visitando %{link_gke}"
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr "Número de nós"
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr "Por favor, tenha certeza que sua conta no Google cumpre com os requisitos:"
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr "Namespace do projeto (opcional, único)"
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr "Ler nossa %{link_to_help_page} na integração com cluster."
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr "Remover integração com cluster"
+
+msgid "ClusterIntegration|Remove integration"
+msgstr "Remover integração"
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr "Remover integração com o cluster irá apagar a configuração de cluster que você adicionou à esse projeto. Não excluirá seu projeto."
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr "Ver e editar os detalhes para seu cluster"
+
+msgid "ClusterIntegration|See machine types"
+msgstr "Ver tipos de máquina"
+
+msgid "ClusterIntegration|See your projects"
+msgstr "Ver seus projetos"
+
+msgid "ClusterIntegration|See zones"
+msgstr "Ver zonas"
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr "Alguma coisa deu errado do nosso lado."
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr "Algo deu errado ao criar seu cluster no Google Container Engine"
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr "Alternar cluster"
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr "Com um cluster associado à esse projeto, você pode usar revisão de apps, fazer deploy de suas aplicações, rodar suas pipelines e muito mais de um jeito simples."
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr "Sua conta precisa ter %{link_to_container_engine}"
+
+msgid "ClusterIntegration|Zone"
+msgstr "Zona"
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr "Acesso ao Google Container Engine"
+
+msgid "ClusterIntegration|cluster"
+msgstr "cluster"
+
+msgid "ClusterIntegration|help page"
+msgstr "ajuda"
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr "atende aos requisitos"
+
+msgid "ClusterIntegration|properly configured"
+msgstr "configurado corretamente"
+
msgid "Comments"
-msgstr ""
+msgstr "Comentários"
msgid "Commit"
msgid_plural "Commits"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Commit"
+msgstr[1] "Commits"
+
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] "Commit %d arquivo"
+msgstr[1] "Commit %d arquivos"
+
+msgid "Commit Message"
+msgstr "Mensagem de Commit"
msgid "Commit duration in minutes for last 30 commits"
msgstr "Duração do commit em minutos para os últimos 30 commits"
@@ -440,7 +644,7 @@ msgid "CommitMessage|Add %{file_name}"
msgstr "Adicionar %{file_name}"
msgid "Commits"
-msgstr ""
+msgstr "Commits"
msgid "Commits feed"
msgstr "Feed de commits"
@@ -454,14 +658,65 @@ msgstr "Commit feito por"
msgid "Compare"
msgstr "Comparar"
+msgid "Container Registry"
+msgstr "Container Registry"
+
+msgid "ContainerRegistry|Created"
+msgstr "Criado"
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr "Primeiro faça login no Container Registry do Gitlab usando seu nome de usuário e senha. Se você tiver %{link_2fa}, será necessário usar um %{link_token}:"
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr "Gitlab suporta até três níveis de nomes de imagens. Os exemplos a seguir são de imagens válidas para seu projeto:"
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr "Como usar o Container Registry"
+
+msgid "ContainerRegistry|Learn more about"
+msgstr "Leia mais sobre"
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr "Sem tags no Container Registry para essa imagem."
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr "Uma vez que você autenticar, será possível criar e fazer upload de uma imagem usando os comandos comuns de %{build} e %{push}"
+
+msgid "ContainerRegistry|Remove repository"
+msgstr "Apagar repositório"
+
+msgid "ContainerRegistry|Remove tag"
+msgstr "Apagar tag"
+
+msgid "ContainerRegistry|Size"
+msgstr "Tamanho"
+
+msgid "ContainerRegistry|Tag"
+msgstr "Tag"
+
+msgid "ContainerRegistry|Tag ID"
+msgstr "ID da Tag"
+
+msgid "ContainerRegistry|Use different image names"
+msgstr "Use nomes de imagem diferentes"
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr "Com o Container Registry do Docker integrado ao Gitlab, todo projeto pode ter seu próprio espaço para guardar suas imagens."
+
msgid "Contribution guide"
msgstr "Guia de contribuição"
msgid "Contributors"
msgstr "Contribuidores"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr "Controle a concorrência máxima de LFS/preenchimento de anexos para o nó secundário"
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr "Controle a concorrência máxima de preenchimento de repositórios para o nó secundário"
+
msgid "Copy SSH public key to clipboard"
-msgstr ""
+msgstr "Copiar chave SSH pública para área de transferência"
msgid "Copy URL to clipboard"
msgstr "Copiar URL para área de transferência"
@@ -472,9 +727,6 @@ msgstr "Copiar SHA do commit para a área de transferência"
msgid "Create New Directory"
msgstr "Criar Novo Diretório"
-msgid "Create a new branch"
-msgstr ""
-
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}."
@@ -484,9 +736,21 @@ msgstr "Criar diretório"
msgid "Create empty bare repository"
msgstr "Criar repositório bruto vazio"
+msgid "Create file"
+msgstr "Criar arquivo"
+
msgid "Create merge request"
msgstr "Criar merge request"
+msgid "Create new branch"
+msgstr "Criar novo branch"
+
+msgid "Create new directory"
+msgstr "Criar nova pasta"
+
+msgid "Create new file"
+msgstr "Criar novo arquivo"
+
msgid "Create new..."
msgstr "Criar novo..."
@@ -538,6 +802,12 @@ msgstr "Homologação"
msgid "CycleAnalyticsStage|Test"
msgstr "Teste"
+msgid "DashboardProjects|All"
+msgstr "Todos"
+
+msgid "DashboardProjects|Personal"
+msgstr "Pessoal"
+
msgid "Define a custom pattern with cron syntax"
msgstr "Defina um padrão personalizado utilizando a sintaxe do cron"
@@ -550,19 +820,28 @@ msgstr[0] "Implantação"
msgstr[1] "Implantações"
msgid "Deploy Keys"
-msgstr ""
+msgstr "Chaves para deploy"
msgid "Description"
msgstr "Descrição"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr "Modelos de descrição permitem que você defina modelos de contextos específicos para issue e descrição de merge requests para seu projeto."
+
msgid "Details"
-msgstr ""
+msgstr "Detalhes"
msgid "Directory name"
msgstr "Nome do diretório"
msgid "Discard changes"
-msgstr ""
+msgstr "Descartar alterações"
+
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr "Ignorar introdução do Cycle Analytics"
+
+msgid "Dismiss Merge Request promotion"
+msgstr "Ignorar anúncio do merge request"
msgid "Don't show again"
msgstr "Não exibir novamente"
@@ -601,28 +880,25 @@ msgid "Edit Pipeline Schedule %{id}"
msgstr "Alterar Agendamento do Pipeline %{id}"
msgid "Emails"
-msgstr ""
-
-msgid "Enable in settings"
-msgstr ""
+msgstr "Emails"
msgid "EventFilterBy|Filter by all"
-msgstr ""
+msgstr "EventFilterBy|Filtrar por tudo"
msgid "EventFilterBy|Filter by comments"
-msgstr ""
+msgstr "EventFilterBy|Filtrar por comentários"
msgid "EventFilterBy|Filter by issue events"
-msgstr ""
+msgstr "EventFilterBy|Filtrar por eventos de issue"
msgid "EventFilterBy|Filter by merge events"
-msgstr ""
+msgstr "EventFilterBy|Filtrar por eventos de merge"
msgid "EventFilterBy|Filter by push events"
-msgstr ""
+msgstr "EventFilterBy|Filtrar por eventos de push"
msgid "EventFilterBy|Filter by team"
-msgstr ""
+msgstr "EventFilterBy|Filtrar por equipe"
msgid "Every day (at 4:00am)"
msgstr "Todos os dias (às 4:00)"
@@ -634,7 +910,10 @@ msgid "Every week (Sundays at 4:00am)"
msgstr "Toda semana (domingos às 4:00)"
msgid "Explore projects"
-msgstr ""
+msgstr "Explorar projetos"
+
+msgid "Explore public groups"
+msgstr "Explorar grupos públicos"
msgid "Failed to change the owner"
msgstr "Erro ao alterar o proprietário"
@@ -642,6 +921,9 @@ msgstr "Erro ao alterar o proprietário"
msgid "Failed to remove the pipeline schedule"
msgstr "Erro ao excluir o agendamento do pipeline"
+msgid "File name"
+msgstr "Nome do arquivo"
+
msgid "Files"
msgstr "Arquivos"
@@ -662,12 +944,18 @@ msgstr "publicado por"
msgid "Fork"
msgid_plural "Forks"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Fork"
+msgstr[1] "Forks"
msgid "ForkedFromProjectPath|Forked from"
msgstr "Fork criado a partir de"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr "Fork a partir de %{project_name} (apagado)"
+
+msgid "Format"
+msgstr "Formato"
+
msgid "From issue creation until deploy to production"
msgstr "Da abertura de tarefas até a implantação para a produção"
@@ -675,16 +963,28 @@ msgid "From merge request merge until deploy to production"
msgstr "Do merge request até a implantação em produção"
msgid "GPG Keys"
-msgstr ""
+msgstr "Chaves GPG"
msgid "Geo Nodes"
-msgstr ""
+msgstr "Nós de geo"
+
+msgid "Geo|File sync capacity"
+msgstr "Capacidade de sincronização de arquivo"
+
+msgid "Geo|Groups to replicate"
+msgstr "Grupos para replicar"
+
+msgid "Geo|Repository sync capacity"
+msgstr "Capacidade de sincronização de repositório"
+
+msgid "Geo|Select groups to replicate."
+msgstr "Selecione grupos para replicar."
msgid "Git storage health information has been reset"
-msgstr ""
+msgstr "Informações sobre o status de saúde do storage Git foram reiniciadas"
msgid "GitLab Runner section"
-msgstr ""
+msgstr "Seção GitLab Runner"
msgid "Go to your fork"
msgstr "Ir para seu fork"
@@ -692,47 +992,98 @@ msgstr "Ir para seu fork"
msgid "GoToYourFork|Fork"
msgstr "Fork"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr "Autenticação do Google não está %{link_to_documentation}. Peça ao administrador do Gitlab se você deseja usar esse serviço."
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
-msgstr ""
+msgstr "Bloquear compartilhamento de projetos do grupo %{group} com outros grupos"
msgid "GroupSettings|Share with group lock"
-msgstr ""
+msgstr "Travar compartilhamento de grupo"
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
-msgstr ""
+msgstr "Essa configuração é aplicada no grupo %{ancestor_group} e foi sobrescrita nesse subgrupo."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Essa configuração é aplicada no grupo %{ancestor_group}. Para compartilhar projetos desse grupo com outro grupo, peça ao dono para alterar a configuração ou %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Essa configuração foi aplicada no grupo %{ancestor_group}. Você pode sobrescrevê-la ou %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
-msgstr ""
+msgstr "Essa configuração será aplicada à todos os subgrupos a menos que sejam sobrescritas pelo dono do grupo. Grupos que já tem acesso ao projeto continuarão a acessá-los até que sejam removidos manualmente."
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
-msgstr ""
+msgstr "não pode ser desativado quando a configuração \"Travar compartilhamento de grupo\" está ativada, a não ser pelo dono do grupo pai"
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
-msgstr ""
+msgstr "retirar a trava de compartilhamento de grupo de %{ancestor_group_name}"
+
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr "Um grupo é uma coleção de vários projetos."
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr "Se você organizar seus projetos num grupo, será como uma pasta."
+
+msgid "GroupsEmptyState|No groups found"
+msgstr "Nenhum grupo encontrado"
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr "Você pode gerenciar permissões de membros e acesso do seu grupo para cada projeto no grupo."
+
+msgid "GroupsTreeRole|as"
+msgstr "como"
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr "Você tem certeza que deseja sair do grupo \"${this.group.fullName}\"?"
+
+msgid "GroupsTree|Create a project in this group."
+msgstr "Criar um projeto nesse grupo."
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr "Criar um subgrupo nesse grupo."
+
+msgid "GroupsTree|Edit group"
+msgstr "Editar grupo"
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr "Falha ao deixar o grupo. Por favor, verifique se você é o único dono."
+
+msgid "GroupsTree|Filter by name..."
+msgstr "Filtrar por nome..."
+
+msgid "GroupsTree|Leave this group"
+msgstr "Deixar o grupo"
+
+msgid "GroupsTree|Loading groups"
+msgstr "Carregando grupos"
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr "Desculpe, nenhum grupo corresponde à sua pesquisa"
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr "Desculpe, nenhum grupo ou projeto correspondem à sua pesquisa"
msgid "Health Check"
-msgstr ""
+msgstr "Status de Saúde"
msgid "Health information can be retrieved from the following endpoints. More information is available"
-msgstr ""
+msgstr "Informações do status de saúde podem ser obtidas nos seguintes locais. Mais informação está disponível"
msgid "HealthCheck|Access token is"
-msgstr ""
+msgstr "O token de acesso é"
msgid "HealthCheck|Healthy"
-msgstr ""
+msgstr "Saudável"
msgid "HealthCheck|No Health Problems Detected"
-msgstr ""
+msgstr "Nenhum problema de saúde detectado"
msgid "HealthCheck|Unhealthy"
-msgstr ""
+msgstr "Não saudável"
+
+msgid "History"
+msgstr "Histórico"
msgid "Housekeeping successfully started"
msgstr "Manutenção iniciada com sucesso"
@@ -740,8 +1091,28 @@ msgstr "Manutenção iniciada com sucesso"
msgid "Import repository"
msgstr "Importar repositório"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr "Melhorar issue boards com o GitLab Enterprise Edition."
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr "Melhore gestão das issues com os pesos no GitLab Enterprise Edition."
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr "Encontre o que precisa mais facilmente com a pesquisa global avançada com GitLab Enterprise Edition."
+
msgid "Install a Runner compatible with GitLab CI"
-msgstr ""
+msgstr "Instalar um Runner compatível com o GitLab CI"
+
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] "Instância"
+msgstr[1] "Instâncias"
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr "Interno - O grupo e projetos internos podem ser visualizados por qualquer usuário autenticado."
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr "Interno - O projeto pode ser acessado por qualquer usuário autenticado."
msgid "Interval Pattern"
msgstr "Padrão de intervalo"
@@ -749,14 +1120,23 @@ msgstr "Padrão de intervalo"
msgid "Introducing Cycle Analytics"
msgstr "Apresentando a Análise de Ciclo"
+msgid "Issue board focus mode"
+msgstr "Modo de foco no issue board"
+
+msgid "Issue boards with milestones"
+msgstr "Issue board com milestone"
+
msgid "Issue events"
-msgstr ""
+msgstr "Eventos de issue"
-msgid "Issues"
-msgstr ""
+msgid "IssueBoards|Board"
+msgstr "Board"
-msgid "Jobs"
-msgstr ""
+msgid "IssueBoards|Boards"
+msgstr "Boards"
+
+msgid "Issues"
+msgstr "Issues"
msgid "LFSStatus|Disabled"
msgstr "Desabilitado"
@@ -765,7 +1145,7 @@ msgid "LFSStatus|Enabled"
msgstr "Habilitado"
msgid "Labels"
-msgstr ""
+msgstr "Etiquetas"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -775,17 +1155,26 @@ msgstr[1] "Últimos %d dias"
msgid "Last Pipeline"
msgstr "Último Pipeline"
-msgid "Last Update"
-msgstr "Última Atualização"
-
msgid "Last commit"
msgstr "Último commit"
+msgid "Last edited %{date}"
+msgstr "Última edição em %{date}"
+
+msgid "Last edited by %{name}"
+msgstr "Última edição por %{name}"
+
+msgid "Last update"
+msgstr "Última atualização"
+
+msgid "Last updated"
+msgstr "Último atualizado"
+
msgid "LastPushEvent|You pushed to"
-msgstr ""
+msgstr "Você fez o push para"
msgid "LastPushEvent|at"
-msgstr ""
+msgstr "em"
msgid "Learn more in the"
msgstr "Saiba mais em"
@@ -793,6 +1182,9 @@ msgstr "Saiba mais em"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "documentação de agendamento de pipeline"
+msgid "Leave"
+msgstr "Sair"
+
msgid "Leave group"
msgstr "Sair do grupo"
@@ -800,42 +1192,60 @@ msgid "Leave project"
msgstr "Sair do projeto"
msgid "License"
-msgstr ""
+msgstr "Licença"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "Limitado a mostrar %d evento, no máximo"
msgstr[1] "Limitado a mostrar %d eventos, no máximo"
+msgid "Lock"
+msgstr "Bloquear"
+
+msgid "Locked"
+msgstr "Bloqueado"
+
msgid "Locked Files"
-msgstr ""
+msgstr "Arquivos bloqueados"
+
+msgid "Login"
+msgstr "Entrar"
+
+msgid "Maximum git storage failures"
+msgstr "Máximo de falhas do git storage"
msgid "Median"
msgstr "Mediana"
msgid "Members"
-msgstr ""
+msgstr "Membros"
msgid "Merge Requests"
-msgstr ""
+msgstr "Merge Requests"
msgid "Merge events"
-msgstr ""
+msgstr "Eventos de merge"
msgid "Merge request"
-msgstr ""
+msgstr "Merge requests"
msgid "Messages"
-msgstr ""
+msgstr "Mensagens"
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "adicione uma chave SSH"
msgid "Monitoring"
-msgstr ""
+msgstr "Monitoramento"
msgid "More information is available|here"
-msgstr ""
+msgstr "Mais informações estão disponíveis|aqui"
+
+msgid "Multiple issue boards"
+msgstr "Múltiplos issue boards"
+
+msgid "New Cluster"
+msgstr "Novo cluster"
msgid "New Issue"
msgid_plural "New Issues"
@@ -854,27 +1264,42 @@ msgstr "Novo diretório"
msgid "New file"
msgstr "Novo arquivo"
+msgid "New group"
+msgstr "Novo grupo"
+
msgid "New issue"
msgstr "Nova issue"
msgid "New merge request"
msgstr "Novo merge request"
+msgid "New project"
+msgstr "Novo projeto"
+
msgid "New schedule"
msgstr "Novo agendamento"
msgid "New snippet"
msgstr "Novo snippet"
+msgid "New subgroup"
+msgstr "Novo subgrupo"
+
msgid "New tag"
msgstr "Nova tag"
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr "Nenhuma imagem gravada para esse projeto. Adiciona uma com as instruções a seguir."
+
msgid "No repository"
msgstr "Nenhum repositório"
msgid "No schedules"
msgstr "Nenhum agendamento"
+msgid "None"
+msgstr "Nenhum"
+
msgid "Not available"
msgstr "Não disponível"
@@ -936,40 +1361,55 @@ msgid "NotificationLevel|Watch"
msgstr "Observar"
msgid "Notifications"
+msgstr "Notificações"
+
+msgid "Number of access attempts"
+msgstr "Número de tentativas de acesso"
+
+msgid "Number of failures before backing off"
msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrar"
+msgid "Only project members can comment."
+msgstr "Somente membros do projeto podem comentar."
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Aberto"
+msgid "Opens in a new window"
+msgstr "Abrir em nova janela"
+
msgid "Options"
msgstr "Opções"
msgid "Overview"
-msgstr ""
+msgstr "Visão geral"
msgid "Owner"
msgstr "Proprietário"
msgid "Pagination|Last »"
-msgstr ""
+msgstr "Último >>"
msgid "Pagination|Next"
-msgstr ""
+msgstr "Próximo"
msgid "Pagination|Prev"
-msgstr ""
+msgstr "Anterior"
msgid "Pagination|« First"
-msgstr ""
+msgstr "<< Primeiro"
msgid "Password"
-msgstr ""
+msgstr "Senha"
+
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr "Pessoas sem permissão nunca receberão uma notificação e não serão capazes de comentar."
msgid "Pipeline"
-msgstr ""
+msgstr "Pipeline"
msgid "Pipeline Health"
msgstr "Saúde da Pipeline"
@@ -981,7 +1421,7 @@ msgid "Pipeline Schedules"
msgstr "Agendamentos da Pipeline"
msgid "Pipeline quota"
-msgstr ""
+msgstr "Cota de pipeline"
msgid "PipelineCharts|Failed:"
msgstr "Falhou:"
@@ -1041,19 +1481,19 @@ msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "Personalizado"
msgid "Pipelines"
-msgstr ""
+msgstr "Pipelines"
msgid "Pipelines charts"
msgstr "Gráficos de pipelines"
msgid "Pipelines for last month"
-msgstr ""
+msgstr "Pipelines para o último mês"
msgid "Pipelines for last week"
-msgstr ""
+msgstr "Pipelines para a última semana"
msgid "Pipelines for last year"
-msgstr ""
+msgstr "Pipelines para o último ano"
msgid "Pipeline|all"
msgstr "todos"
@@ -1068,10 +1508,55 @@ msgid "Pipeline|with stages"
msgstr "com etapas"
msgid "Preferences"
-msgstr ""
+msgstr "Preferências"
+
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr "Privado - O acesso ao projeto deve ser concedido explicitamente para cada usuário."
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr "Privado - O grupo e seus projetos só podem ser vistos por seus membros."
msgid "Profile"
-msgstr ""
+msgstr "Perfil"
+
+msgid "Profiles|Account scheduled for removal."
+msgstr "Conta agendada para remoção."
+
+msgid "Profiles|Delete Account"
+msgstr "Excluir conta"
+
+msgid "Profiles|Delete account"
+msgstr "Excluir conta"
+
+msgid "Profiles|Delete your account?"
+msgstr "Deseja apagar sua conta?"
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr "Apagando conta tem os seguintes efeitos:"
+
+msgid "Profiles|Invalid password"
+msgstr "Senha inválida"
+
+msgid "Profiles|Invalid username"
+msgstr "Nome de usuário inválido"
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr "Escreva %{confirmationValue} para confirmar:"
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr "Você não tem permissão para apagar esse usuário."
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr "Você precisa delegar outro usuário para ser dono ou apagar esses grupos antes de excluir sua conta."
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr "Sua conta é atualmente proprietária dos seguintes grupos:"
+
+msgid "Profiles|your account"
+msgstr "sua conta"
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr "O projeto '%{project_name}' está sendo excluído."
msgid "Project '%{project_name}' queued for deletion."
msgstr "Projeto'%{project_name}' marcado para exclusão."
@@ -1082,14 +1567,11 @@ msgstr "Projeto '%{project_name}' criado com sucesso."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Projeto '%{project_name}' atualizado com sucesso."
-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 ""
+msgstr "Detalhes do projeto"
msgid "Project export could not be deleted."
msgstr "A exportação do projeto não pôde ser excluída."
@@ -1104,7 +1586,7 @@ msgid "Project export started. A download link will be sent by email."
msgstr "Exportação do projeto iniciada. Um link para baixá-la será enviado por email."
msgid "ProjectActivityRSS|Subscribe"
-msgstr ""
+msgstr "Inscreva-se"
msgid "ProjectFeature|Disabled"
msgstr "Desabilitado"
@@ -1127,32 +1609,62 @@ msgstr "Etapa"
msgid "ProjectNetworkGraph|Graph"
msgstr "Ãrvore"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr "Fale com um administrador para mudar essa configuração."
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr "Esse repositório só aceita push de commits assinados."
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr "Essa configuração está aplicada à nivel de servidor e pode ser sobrescrita por um adminstrador."
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr "Essa configuração está aplicada à nivel de servidor mas pode ser sobrescrita para esse projeto."
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr "Essa configuração será aplicada à todos os projetos, a não ser que sejam sobrescritos pelo administrador."
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr "Nesse repositório, usuários só podem fazer push de commits verificados pelos seus e-mails."
+
+msgid "Projects"
+msgstr "Projetos"
+
msgid "ProjectsDropdown|Frequently visited"
-msgstr ""
+msgstr "Visitados frequentemente"
msgid "ProjectsDropdown|Loading projects"
-msgstr ""
+msgstr "Carregando projetos"
msgid "ProjectsDropdown|Projects you visit often will appear here"
-msgstr ""
+msgstr "Projetos que você visita frequentemente aparecerão aqui"
msgid "ProjectsDropdown|Search your projects"
-msgstr ""
+msgstr "Procure seus projetos"
msgid "ProjectsDropdown|Something went wrong on our end."
-msgstr ""
+msgstr "Algo deu errado do nosso lado."
msgid "ProjectsDropdown|Sorry, no projects matched your search"
-msgstr ""
+msgstr "Desculpe, nenhum projeto corresponde a sua pesquisa"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
-msgstr ""
+msgstr "Esta funcionalidade necessita de suporte à localStorage do navegador"
+
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr "Público - O grupo e seus projetos podem ser visualizados por todos sem autenticação."
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr "Público - O projeto pode ser acessado sem nenhuma autenticação."
msgid "Push Rules"
-msgstr ""
+msgstr "Regras de push"
msgid "Push events"
-msgstr ""
+msgstr "Eventos de push"
+
+msgid "PushRule|Committer restriction"
+msgstr "Restrição de commit"
msgid "Read more"
msgstr "Leia mais"
@@ -1167,7 +1679,7 @@ msgid "RefSwitcher|Tags"
msgstr "Tags"
msgid "Registry"
-msgstr ""
+msgstr "Registry"
msgid "Related Commits"
msgstr "Commits Relacionados"
@@ -1194,19 +1706,19 @@ msgid "Remove project"
msgstr "Remover projeto"
msgid "Repository"
-msgstr ""
+msgstr "Repositório"
msgid "Request Access"
msgstr "Solicitar acesso"
msgid "Reset git storage health information"
-msgstr ""
+msgstr "Reiniciar informações de status do storage Git"
msgid "Reset health check access token"
-msgstr ""
+msgstr "Recriar o token de status de saúde"
msgid "Reset runners registration token"
-msgstr ""
+msgstr "Recriar o token de registro de runners"
msgid "Revert this commit"
msgstr "Reverter este commit"
@@ -1215,7 +1727,13 @@ msgid "Revert this merge request"
msgstr "Reverter esse merge request"
msgid "SSH Keys"
-msgstr ""
+msgstr "Chaves SSH"
+
+msgid "Save"
+msgstr "Salvar"
+
+msgid "Save changes"
+msgstr "Salvar alterações"
msgid "Save pipeline schedule"
msgstr "Salvar agendamento da pipeline"
@@ -1224,7 +1742,7 @@ msgid "Schedule a new pipeline"
msgstr "Agendar nova pipeline"
msgid "Schedules"
-msgstr ""
+msgstr "Agendamentos"
msgid "Scheduling Pipelines"
msgstr "Agendando pipelines"
@@ -1232,20 +1750,26 @@ msgstr "Agendando pipelines"
msgid "Search branches and tags"
msgstr "Procurar branch e tags"
+msgid "Seconds before reseting failure information"
+msgstr "Segundos antes de redefinir as informações de falha"
+
+msgid "Seconds to wait after a storage failure"
+msgstr "Segundos a esperar após uma falha de armazenamento"
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr "Segundo de espera para tentativa de acesso ao storage"
+
msgid "Select Archive Format"
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 "Service Templates"
-msgstr ""
+msgstr "Modelos de serviço"
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}."
@@ -1263,13 +1787,13 @@ msgid "SetPasswordToCloneLink|set a password"
msgstr "defina uma senha"
msgid "Settings"
-msgstr ""
+msgstr "Configurações"
msgid "Show parent pages"
-msgstr ""
+msgstr "Mostrar páginas acima"
msgid "Show parent subgroups"
-msgstr ""
+msgstr "Mostrar subgrupos acima"
msgid "Showing %d event"
msgid_plural "Showing %d events"
@@ -1277,143 +1801,173 @@ msgstr[0] "Mostrando %d evento"
msgstr[1] "Mostrando %d eventos"
msgid "Snippets"
-msgstr ""
+msgstr "Snippets"
+
+msgid "Something went wrong on our end."
+msgstr "Algo deu errado do nosso lado."
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr "Algo deu errado ao tentar mudar o estado de bloqueio de ${this.issuableDisplayName(this.issuableType)}"
+
+msgid "Something went wrong while fetching the projects."
+msgstr "Algo deu errado ao recuperar os projetos."
+
+msgid "Something went wrong while fetching the registry list."
+msgstr "Algo deu errado ao recuperar a lista de registro."
+
+msgid "Sort by"
+msgstr "Ordenar por"
msgid "SortOptions|Access level, ascending"
-msgstr ""
+msgstr "Nível de acesso, ascendente"
msgid "SortOptions|Access level, descending"
-msgstr ""
+msgstr "Nível de acesso, decrescente"
msgid "SortOptions|Created date"
-msgstr ""
+msgstr "Data de criação"
msgid "SortOptions|Due date"
-msgstr ""
+msgstr "Data de vencimento"
msgid "SortOptions|Due later"
-msgstr ""
+msgstr "Data de vencimento mais longe"
msgid "SortOptions|Due soon"
-msgstr ""
+msgstr "Data de vencimento mais próxima"
msgid "SortOptions|Label priority"
-msgstr ""
+msgstr "Prioridade de label"
msgid "SortOptions|Largest group"
-msgstr ""
+msgstr "Maior grupo"
msgid "SortOptions|Largest repository"
-msgstr ""
+msgstr "Maior repositório"
msgid "SortOptions|Last created"
-msgstr ""
+msgstr "Últimos criados"
msgid "SortOptions|Last joined"
-msgstr ""
+msgstr "Últimos associados"
msgid "SortOptions|Last updated"
-msgstr ""
+msgstr "Últimos atualizados"
msgid "SortOptions|Least popular"
-msgstr ""
+msgstr "Menos populares"
msgid "SortOptions|Less weight"
-msgstr ""
+msgstr "Menor peso"
msgid "SortOptions|Milestone"
-msgstr ""
+msgstr "Milestone"
msgid "SortOptions|Milestone due later"
-msgstr ""
+msgstr "Milestone de fim mais longo"
msgid "SortOptions|Milestone due soon"
-msgstr ""
+msgstr "Milestone de fim mais próximo"
msgid "SortOptions|More weight"
-msgstr ""
+msgstr "Mais peso"
msgid "SortOptions|Most popular"
-msgstr ""
+msgstr "Mais populares"
msgid "SortOptions|Name"
-msgstr ""
+msgstr "Nome"
msgid "SortOptions|Name, ascending"
-msgstr ""
+msgstr "Nome, ascendente"
msgid "SortOptions|Name, descending"
-msgstr ""
+msgstr "Nome, decrescente"
msgid "SortOptions|Oldest created"
-msgstr ""
+msgstr "Criação mais antiga"
msgid "SortOptions|Oldest joined"
-msgstr ""
+msgstr "Primeiros associados"
msgid "SortOptions|Oldest sign in"
-msgstr ""
+msgstr "Assinados mais antigos"
msgid "SortOptions|Oldest updated"
-msgstr ""
+msgstr "Atualização mais antiga"
msgid "SortOptions|Popularity"
-msgstr ""
+msgstr "Popularidade"
msgid "SortOptions|Priority"
-msgstr ""
+msgstr "Prioridade"
msgid "SortOptions|Recent sign in"
-msgstr ""
+msgstr "Assinados mais novos"
msgid "SortOptions|Start later"
-msgstr ""
+msgstr "Iniciar mais tarde"
msgid "SortOptions|Start soon"
-msgstr ""
+msgstr "Iniciar mais próximo"
msgid "SortOptions|Weight"
-msgstr ""
+msgstr "Peso"
msgid "Source code"
msgstr "Código-fonte"
msgid "Spam Logs"
-msgstr ""
+msgstr "Logs de spam"
msgid "Specify the following URL during the Runner setup:"
-msgstr ""
+msgstr "Especifique a seguinte URL durante a configuração do Runner:"
msgid "StarProject|Star"
msgstr "Marcar"
msgid "Starred projects"
-msgstr ""
+msgstr "Projetos favoritos"
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 ""
+msgstr "Inicie o Runner!"
+
+msgid "Subgroups"
+msgstr "Subgrupos"
+
+msgid "Subscribe"
+msgstr "Assine"
msgid "Switch branch/tag"
msgstr "Trocar branch/tag"
msgid "System Hooks"
-msgstr ""
+msgstr "Hooks do sistema"
msgid "Tag"
msgid_plural "Tags"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Tag"
+msgstr[1] "Tags"
msgid "Tags"
-msgstr ""
+msgstr "Tags"
msgid "Target Branch"
msgstr "Branch de destino"
msgid "Team"
+msgstr "Equipe"
+
+msgid "Thanks! Don't show me this again"
+msgstr "Obrigado! Não mostrar novamente"
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr "A pesquisa global avançado no GitLab é um serviço poderoso de pesquisa que poupa seu tempo. Ao invés de criar código duplicado e perder seu tempo, você pode agora pesquisar por código de outros times que podem ajudar no seu próprio projeto."
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
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."
@@ -1428,6 +1982,15 @@ 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 number of attempts GitLab will make to access a storage."
+msgstr "O número de tentativas que gitlab fará para acessar um storage."
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr "O número de falhas até o GitLab começar a desabilitar temporariamente o acesso a um nó de storage em um host"
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr "O número de falhas para que o GitLab desabilite o acesso ao storage. O número de falhas pode ser redefinido na interface do administrador: %{link_to_health_page} ou %{api_documentation_link}."
+
msgid "The phase of the development lifecycle."
msgstr "A fase do ciclo de vida do desenvolvimento."
@@ -1458,6 +2021,12 @@ msgstr "A etapa de homologação mostra o tempo entre o aceite da solicitação
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 in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr "Tempo em segundos para o GitLab manter as informações de falha. Se nenhuma falha ocorrer durante este tempo, a informação sobre o ponto de montagem será redefinida."
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr "Tempo em segundos que o GitLab tentará acessar o storage. Depois desse tempo, um erro de tempo excedido será disparado."
+
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."
@@ -1465,14 +2034,29 @@ msgid "The value lying at the midpoint of a series of observed values. E.g., bet
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 "There are problems accessing Git storage: "
-msgstr ""
+msgstr "Há problemas para acessar o storage Git: "
-msgid "This is the author's first Merge Request to this project. Handle with care."
-msgstr ""
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr "Esse branch mudou desde quando você começou sua edição. Você quer criar um novo branch?"
+
+msgid "This is a confidential issue."
+msgstr "Essa issue é confidencial."
+
+msgid "This is the author's first Merge Request to this project."
+msgstr "Esse é o autor do primeiro merge request desse projeto."
+
+msgid "This issue is confidential and locked."
+msgstr "Essa issue é confidencial e está bloqueada."
+
+msgid "This issue is locked."
+msgstr "Essa issue está bloqueada."
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 "This merge request is locked."
+msgstr "Esse merge request está bloqueado."
+
msgid "Time before an issue gets scheduled"
msgstr "Tempo até que uma issue seja agendada"
@@ -1551,9 +2135,6 @@ msgstr "há um mês"
msgid "Timeago|a week ago"
msgstr "há uma semana"
-msgid "Timeago|a while"
-msgstr "há algum tempo"
-
msgid "Timeago|a year ago"
msgstr "há um ano"
@@ -1605,6 +2186,9 @@ msgstr "em 1 semana"
msgid "Timeago|in 1 year"
msgstr "em 1 ano"
+msgid "Timeago|in a while"
+msgstr "há algum tempo"
+
msgid "Timeago|less than a minute ago"
msgstr "há menos de um minuto"
@@ -1627,9 +2211,36 @@ msgstr "Tempo Total"
msgid "Total test time for all commits/merges"
msgstr "Tempo de teste total para todos os commits/merges"
+msgid "Track activity with Contribution Analytics."
+msgstr "Acompanhar atividade com o Contribution Analytics."
+
+msgid "Unlock"
+msgstr "Desbloquear"
+
+msgid "Unlocked"
+msgstr "Desbloqueado"
+
msgid "Unstar"
msgstr "Desmarcar"
+msgid "Unsubscribe"
+msgstr "Desassinar"
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr "Atualize seu plano para ativar a pesquisa global avançada."
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr "Atualize seu plano para ativar o Contribution Analytics."
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr "Atualize seu plano para ativar Webhooks de grupo."
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr "Atualize seu plano para ativar peso nas issues."
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr "Atualize seu plano para melhorar os issue boards."
+
msgid "Upload New File"
msgstr "Enviar Novo Arquivo"
@@ -1640,19 +2251,19 @@ msgid "UploadLink|click to upload"
msgstr "clique para fazer upload"
msgid "Use the following registration token during setup:"
-msgstr ""
+msgstr "Use o seguinte token de registro durante a configuração:"
msgid "Use your global notification setting"
msgstr "Utilizar configuração de notificação global"
msgid "View file @ "
-msgstr ""
+msgstr "Ver arquivo @ "
msgid "View open merge request"
msgstr "Ver merge request aberto"
msgid "View replaced file @ "
-msgstr ""
+msgstr "Ver arquivo substituído @ "
msgid "VisibilityLevel|Internal"
msgstr "Interno"
@@ -1672,8 +2283,119 @@ msgstr "Precisa visualizar os dados? Solicite acesso ao administrador."
msgid "We don't have enough data to show this stage."
msgstr "Esta etapa não possui dados suficientes para exibição."
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr "Webhooks permitem que você acione uma URL se, por exemplo, um novo código for feito push ou uma nova issue criada. Você pode configurar os webhooks para escutar eventos específicos como push, issue ou merge request. Webhooks de grupo aplicarão para todos os projetos no grupo, permitindo você padronizar o funcionamento em todo o grupo."
+
+msgid "Weight"
+msgstr "Peso"
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr "Falha ao acessar o storage. Gitlab impedirá o acesso ao storage pelo tempo especificado aqui. Isso permite que o sistema de arquivos se recupere. Repositórios que estiverem em nós com falha ficarão temporariamente indisponíveis"
+
msgid "Wiki"
-msgstr ""
+msgstr "Wiki"
+
+msgid "WikiClone|Clone your wiki"
+msgstr "Clonar sua wiki"
+
+msgid "WikiClone|Git Access"
+msgstr "Acesso Git"
+
+msgid "WikiClone|Install Gollum"
+msgstr "Instalar Gollum"
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr "É recomendado instalar %{markdown} para que as funções GFM sejam renderizadas localmente:"
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr "Inicie o Gollum e edite localmente"
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr "Você não tem permissão para criar páginas web"
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr "Essa é uma versão antiga dessa página."
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr "Você pode ver o %{most_recent_link} ou navegar o %{history_link}."
+
+msgid "WikiHistoricalPage|history"
+msgstr "histórico"
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr "versão mais recente"
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr "Mais exemplos em %{docs_link}"
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr "documentação"
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr "Para criar um link para uma (nova) página, é so digitar %{link_example}"
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr "como instalar"
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr "Dica: Você pode especificar o caminho completo para o novo arquivo. Nós vamos criar automaticamente quaisquer diretórios ainda não existentes."
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr "Nova página Wiki"
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr "Quer mesmo apagar essa página?"
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr "Alguém editou essa página ao mesmo tempo que você. Por favor olhe %{page_link} e tenha certeza de que suas mudanças não removerão as mudanças deles."
+
+msgid "WikiPageConflictMessage|the page"
+msgstr "a página"
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr "Criar %{page_title}"
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr "Atualizar %{page_title}"
+
+msgid "WikiPage|Page slug"
+msgstr "Nome amigável da página"
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr "Escreve seu conteudo ou arraste arquivos aqui..."
+
+msgid "Wiki|Create Page"
+msgstr "Criar página"
+
+msgid "Wiki|Create page"
+msgstr "Criar página"
+
+msgid "Wiki|Edit Page"
+msgstr "Ediar página"
+
+msgid "Wiki|Empty page"
+msgstr "Página vazia"
+
+msgid "Wiki|More Pages"
+msgstr "Mais páginas"
+
+msgid "Wiki|New page"
+msgstr "Nova página"
+
+msgid "Wiki|Page history"
+msgstr "Histórico da página"
+
+msgid "Wiki|Page version"
+msgstr "Versão da página"
+
+msgid "Wiki|Pages"
+msgstr "Páginas"
+
+msgid "Wiki|Wiki Pages"
+msgstr "Páginas Wiki"
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr "Com o contribution analytics você pode ter uma visão geral da atividade em issue, merge request e evento de push para sua organização e seus membros."
msgid "Withdraw Access Request"
msgstr "Remover Requisição de Acesso"
@@ -1685,14 +2407,26 @@ msgid "You are going to remove %{project_name_with_namespace}. Removed project C
msgstr "Você irá remover %{project_name_with_namespace}. O projeto removido NÃO PODE ser restaurado! Tem certeza ABSOLUTA?"
msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr ""
+msgstr "Você está prestes a remover a relação de fork do projeto original %{forked_from_project}. Você tem CERTEZA disso?"
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "Você irá transferir %{project_name_with_namespace} para outro proprietário. Tem certeza ABSOLUTA?"
+msgid "You are on a read-only GitLab instance."
+msgstr "Você está em uma instância somente-leitura do GitLab."
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr "Você está em uma instância somente-leitura do GitLab. Se você quiser fazer qualquer alteração visite %{link_to_primary_node}."
+
msgid "You can only add files when you are on a branch"
msgstr "Você somente pode adicionar arquivos quando estiver em um branch"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr "Você não pode escrever numa instância secundária de somente leitura do GitLab Geo. Por favor use %{link_to_primary_node}."
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr "Você não pode escrever nesta instância somente-leitura do GitLab."
+
msgid "You have reached your project limit"
msgstr "Você atingiu o limite de seu projeto"
@@ -1723,14 +2457,20 @@ msgstr "Você não poderá fazer pull ou push via %{protocol} até que %{set_pas
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 comment will not be visible to the public."
+msgstr "Seu comentário não estará visível ao público."
+
+msgid "Your groups"
+msgstr "Seus grupos"
+
msgid "Your name"
msgstr "Seu nome"
msgid "Your projects"
-msgstr ""
+msgstr "Seus projetos"
msgid "commit"
-msgstr ""
+msgstr "commit"
msgid "day"
msgid_plural "days"
@@ -1748,3 +2488,15 @@ msgid_plural "parents"
msgstr[0] "pai"
msgstr[1] "pais"
+msgid "password"
+msgstr "senha"
+
+msgid "personal access token"
+msgstr "token de acesso pessoal"
+
+msgid "to help your contributors communicate effectively!"
+msgstr "para ajudar seus contribuintes à se comunicar de maneira eficaz!"
+
+msgid "username"
+msgstr "nome do usuário"
+
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index 507dc187cdb..2f612f46799 100644
--- a/locale/ru/gitlab.po
+++ b/locale/ru/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:43-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-05 14:39-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -19,23 +19,35 @@ msgstr ""
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d коммит"
-msgstr[1] "%d коммитов"
+msgstr[1] "%d коммита"
msgstr[2] "%d коммитов"
+msgid "%d layer"
+msgid_plural "%d layers"
+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 добавленные коммиты были иÑключены Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñтью."
+msgstr[1] "%s добавленных коммита были иÑключены Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñтью."
+msgstr[2] "%s добавленных коммитов были иÑключены Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñтью."
msgid "%{commit_author_link} committed %{commit_timeago}"
-msgstr "%{commit_author_link} коммичено %{commit_timeago}"
+msgstr "%{commit_author_link} добавил коммит %{commit_timeago}"
+
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] "%{count} учаÑтник"
+msgstr[1] "%{count} учаÑтника"
+msgstr[2] "%{count} учаÑтников"
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
-msgstr ""
+msgstr "на %{number_commits_behind} коммитов позади %{default_branch}, на %{number_commits_ahead} коммитов впереди"
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
-msgstr "%{number_of_failures} из %{maximum_failures} возможных попыток. Ð’Ñ‹ можете попытатьÑÑ ÐµÑ‰Ðµ раз."
+msgstr "%{number_of_failures} из %{maximum_failures} возможных неудачных попыток. GitLab будет доÑтупен поÑле Ñледующей попытки."
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds."
msgstr "%{number_of_failures} из %{maximum_failures} возможных неудачных попыток. GitLab заблокирует доÑтуп на %{number_of_seconds} Ñекунд."
@@ -52,17 +64,26 @@ msgstr[2] "%{storage_name}: %{failed_attempts} - неудачные попытк
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(перейдите по ÑÑылке %{link} Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ð¸ об уÑтановке)."
+msgid "+ %{moreCount} more"
+msgstr "+ ещё %{moreCount}"
+
+msgid "- show less"
+msgstr "- Ñвернуть"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "1 конвейер"
-msgstr[1] "%d конвейеры"
-msgstr[2] "%d конвейеры"
+msgstr[0] "1 ÑÐ±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ"
+msgstr[1] "%d Ñборочных линии"
+msgstr[2] "%d Ñборочных линий"
msgid "1st contribution!"
-msgstr ""
+msgstr "Первый вклад!"
+
+msgid "2FA enabled"
+msgstr "Ð”Ð²ÑƒÑ…Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð°Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð²ÐºÐ»ÑŽÑ‡ÐµÐ½Ð°"
msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Графики отноÑительно непрерывной интеграции"
+msgstr "Графики непрерывной интеграции (CI)"
msgid "About auto deploy"
msgstr "Об автоматичеÑком развёртывании"
@@ -85,26 +106,41 @@ msgstr "Ðктивный"
msgid "Activity"
msgstr "ÐктивноÑÑ‚ÑŒ"
+msgid "Add"
+msgstr "Добавить"
+
msgid "Add Changelog"
-msgstr "Добавить журнал изменений"
+msgstr "Добавить Журнал Изменений"
msgid "Add Contribution guide"
-msgstr "Добавить руководÑтво"
+msgstr "Добавить РуководÑтво учаÑтника"
+
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr "Добавить групповые веб-обработчики и GitLab Enterprise Edition."
msgid "Add License"
-msgstr "Добавить лицензию"
+msgstr "Добавить Лицензию"
msgid "Add an SSH key to your profile to pull or push via SSH."
msgstr "Добавьте ключ SSH в Ñвой профиль, чтобы отправлÑÑ‚ÑŒ или получать код через SSH."
msgid "Add new directory"
-msgstr "Добавить каталог"
+msgstr "Добавить новый каталог"
+
+msgid "AdminHealthPageLink|health page"
+msgstr "Ñтраница работоÑпоÑобноÑти"
+
+msgid "Advanced settings"
+msgstr "РаÑширенные наÑтройки"
msgid "All"
msgstr "Ð’Ñе"
+msgid "An error occurred. Please try again."
+msgstr "Произошла ошибка. ПожалуйÑта, попробуйте Ñнова."
+
msgid "Appearance"
-msgstr ""
+msgstr "Оформление"
msgid "Applications"
msgstr "ПриложениÑ"
@@ -113,106 +149,115 @@ msgid "Archived project! Repository is read-only"
msgstr "Ðрхивный проект! Репозиторий доÑтупен только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ"
msgid "Are you sure you want to delete this pipeline schedule?"
-msgstr "Ð’Ñ‹ дейÑтвительно хотите удалить Ñто раÑпиÑание конвейера?"
+msgstr "Ð’Ñ‹ дейÑтвительно хотите удалить Ñто раÑпиÑание Ñборочной линии?"
msgid "Are you sure you want to discard your changes?"
-msgstr "Ð’Ñ‹ уверены, что Ð’Ñ‹ хотите отменить Ваши изменениÑ?"
+msgstr "Ð’Ñ‹ уверены, что хотите отменить ваши изменениÑ?"
+
+msgid "Are you sure you want to leave this group?"
+msgstr "Ð’Ñ‹ уверены, что хотите покинуть Ñту группу?"
msgid "Are you sure you want to reset registration token?"
-msgstr "Ð’Ñ‹ уверены, что Ð’Ñ‹ хотите ÑброÑить Ñтот ключ региÑтрации?"
+msgstr "Ð’Ñ‹ уверены, что хотите ÑброÑить Ñтот региÑтрационный токен?"
msgid "Are you sure you want to reset the health check token?"
-msgstr "Ð’Ñ‹ уверены, что Ð’Ñ‹ хотите ÑброÑить Ñтот ключ проверки работоÑпоÑобноÑти?"
+msgstr "Ð’Ñ‹ уверены, что хотите ÑброÑить Ñтот токен проверки работоÑпоÑобноÑти?"
msgid "Are you sure?"
msgstr "Вы уверены?"
msgid "Artifacts"
-msgstr ""
+msgstr "Ðртефакты"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Приложить файл через drag &amp; drop или %{upload_link}"
msgid "Authentication Log"
-msgstr ""
+msgstr "Журнал аутентификации"
-msgid "Auto DevOps (Beta)"
-msgstr ""
-
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
-msgid "Auto DevOps documentation"
-msgstr ""
+msgid "Author"
+msgstr "Ðвтор"
msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "ÐŸÑ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑкого ревью и автоматичеÑкого Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ñ‚Ñ€ÐµÐ±ÑƒÑŽÑ‚ ÑƒÐºÐ°Ð·Ð°Ð½Ð¸Ñ Ð¸Ð¼ÐµÐ½Ð¸ домена и %{kubernetes} Ð´Ð»Ñ ÐºÐ¾Ñ€Ñ€ÐµÐºÑ‚Ð½Ð¾Ð¹ работы."
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
-msgstr ""
+msgstr "ÐŸÑ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑкого ревью и автоматичеÑкого Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ñ‚Ñ€ÐµÐ±ÑƒÑŽÑ‚ ÑƒÐºÐ°Ð·Ð°Ð½Ð¸Ñ Ð¸Ð¼ÐµÐ½Ð¸ домена Ð´Ð»Ñ ÐºÐ¾Ñ€Ñ€ÐµÐºÑ‚Ð½Ð¾Ð¹ работы."
msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr "ÐŸÑ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑкого ревью и автоматичеÑкого Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ñ‚Ñ€ÐµÐ±ÑƒÑŽÑ‚ ÑƒÐºÐ°Ð·Ð°Ð½Ð¸Ñ %{kubernetes} Ð´Ð»Ñ ÐºÐ¾Ñ€Ñ€ÐµÐºÑ‚Ð½Ð¾Ð¹ работы."
+
+msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "AutoDevOps|Learn more in the"
+msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
+msgid "AutoDevOps|Enable in settings"
+msgstr "Включить в наÑтройках"
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr "Ð”Ð»Ñ Ñтого проекта может быть активирован автоматичеÑкий DevOps. Он будет автоматичеÑки Ñобирать, теÑтировать и разворачивать ваши Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð½Ð° оÑнове предопределенной конфигурации CI/CD."
+
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
+msgstr "Подробнее по ÑÑылке %{link_to_documentation}"
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr "Ð’Ñ‹ можете активировать %{link_to_settings} Ð´Ð»Ñ Ñтого проекта."
+
msgid "Billing"
-msgstr ""
+msgstr "Тариф"
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr ""
+msgstr "%{group_name} иÑпользует тарифный план %{plan_link}."
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr ""
+msgstr "ÐвтоматичеÑкое повышение или понижение недоÑтупно Ð´Ð»Ñ Ð½ÐµÐºÐ¾Ñ‚Ð¾Ñ€Ñ‹Ñ… тарифных планов в наÑтоÑщее времÑ."
msgid "BillingPlans|Current plan"
-msgstr ""
+msgstr "Текущий тарифный план"
msgid "BillingPlans|Customer Support"
-msgstr ""
+msgstr "Поддержка Клиентов"
+
+msgid "BillingPlans|Downgrade"
+msgstr "Понижение"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr ""
+msgstr "Узнайте больше о каждом тарифном плане прочитав наш %{faq_link}."
msgid "BillingPlans|Manage plan"
-msgstr ""
+msgstr "Управление тарифным планом"
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr ""
+msgstr "ПожалуйÑта, в Ñтом Ñлучае обратитеÑÑŒ в %{customer_support_link}."
msgid "BillingPlans|See all %{plan_name} features"
-msgstr ""
+msgstr "ПоÑмотреть возможноÑти %{plan_name} полноÑтью"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr ""
+msgstr "Эта группа иÑпользует тарифный план ÑвÑзанный Ñ Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÑкой группой."
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr ""
+msgstr "Ð”Ð»Ñ ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ñ‚Ð°Ñ€Ð¸Ñ„Ð½Ñ‹Ð¼ планом Ñтой группы поÑетите раздел тарификации %{parent_billing_page_link}."
msgid "BillingPlans|Upgrade"
-msgstr ""
+msgstr "Повышение"
msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr ""
+msgstr "Ð’ наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð²Ñ‹ иÑпользуете тарифный план %{plan_link}."
msgid "BillingPlans|frequently asked questions"
-msgstr ""
+msgstr "ЧаÑто задаваемые вопроÑÑ‹"
msgid "BillingPlans|monthly"
-msgstr ""
+msgstr "ежемеÑÑчно"
msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr ""
+msgstr "оплачиваетÑÑ ÐµÐ¶ÐµÐ³Ð¾Ð´Ð½Ð¾ в размере %{price_per_year}"
msgid "BillingPlans|per user"
-msgstr ""
-
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
+msgstr "за пользователÑ"
msgid "Branch"
msgid_plural "Branches"
@@ -221,7 +266,10 @@ 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}"
+msgstr "Ветка <strong>%{branch_name}</strong> Ñоздана. Ð”Ð»Ñ Ð½Ð°Ñтройки автоматичеÑкого Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð²Ñ‹Ð±ÐµÑ€Ð¸Ñ‚Ðµ YAML-шаблон Ð´Ð»Ñ GitLab CI и зафикÑируйте Ñвои изменениÑ. %{link_to_autodeploy_doc}"
+
+msgid "Branch has changed"
+msgstr "Ветка была изменена"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "ПоиÑк веток"
@@ -233,91 +281,91 @@ msgid "Branches"
msgstr "Ветки"
msgid "Branches|Cant find HEAD commit for this branch"
-msgstr ""
+msgstr "Ðевозможно найти HEAD-коммит Ñтой ветки"
msgid "Branches|Compare"
-msgstr ""
+msgstr "Сравнить"
msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
-msgstr ""
+msgstr "Удалить вÑе ветки, влитые в '%{default_branch}'"
msgid "Branches|Delete branch"
-msgstr ""
+msgstr "Удалить ветку"
msgid "Branches|Delete merged branches"
-msgstr ""
+msgstr "Удалить влитые ветки"
msgid "Branches|Delete protected branch"
-msgstr ""
+msgstr "Удалить защищённую ветку"
msgid "Branches|Delete protected branch '%{branch_name}'?"
-msgstr ""
+msgstr "Удалить защищённую ветку '%{branch_name}'?"
msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
-msgstr ""
+msgstr "Уделение ветки '%{branch_name}' невозможно отменить. Вы уверены?"
msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
-msgstr ""
+msgstr "Удаление влитых веток невозможно отменить. Вы уверены?"
msgid "Branches|Filter by branch name"
-msgstr ""
+msgstr "Отфильтровать по имени ветки"
msgid "Branches|Merged into %{default_branch}"
-msgstr ""
+msgstr "Влить в %{default_branch}"
msgid "Branches|New branch"
-msgstr ""
+msgstr "ÐÐ¾Ð²Ð°Ñ Ð²ÐµÑ‚ÐºÐ°"
msgid "Branches|No branches to show"
-msgstr ""
+msgstr "Ðет веток Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
-msgstr ""
+msgstr "Как только вы подтвердите и нажмёте %{delete_protected_branch}, данные будут удалены без возможноÑти воÑÑтановлениÑ."
msgid "Branches|Only a project master or owner can delete a protected branch"
-msgstr ""
+msgstr "Только маÑтер или владелец проекта может удалить защищённую ветку"
msgid "Branches|Protected branches can be managed in %{project_settings_link}"
-msgstr ""
+msgstr "Управление защищёнными ветками возможно в %{project_settings_link}"
msgid "Branches|Sort by"
-msgstr ""
+msgstr "Сортировать по"
msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
+msgstr "Ветка не может быть обновлена автоматичеÑки, потому что она имеет раÑÑ…Ð¾Ð¶Ð´ÐµÐ½Ð¸Ñ Ñ ÐµÑ‘ двойником в родительÑком репозитории."
msgid "Branches|The default branch cannot be deleted"
-msgstr ""
+msgstr "Ветка \"по умолчанию\" не может быть удалена"
msgid "Branches|This branch hasn’t been merged into %{default_branch}."
-msgstr ""
+msgstr "Эта ветка не может быть влита в %{default_branch}."
msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
-msgstr ""
+msgstr "Чтобы избежать потери данных, раÑÑмотрите возможноÑÑ‚ÑŒ ÑлиÑÐ½Ð¸Ñ Ñтой ветки перед её удалением."
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
-msgstr ""
+msgstr "Ð”Ð»Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ, введите %{branch_name_confirmation}:"
msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
+msgstr "Чтобы отменить локальные Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¸ перезапиÑать ветку верÑией из родительÑкого репозиториÑ, удалите её здеÑÑŒ и выберите \"Обновить ÑейчаÑ\" выше."
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
-msgstr ""
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ безвозвратно удалить защищённую ветку %{branch_name}."
msgid "Branches|diverged from upstream"
-msgstr ""
+msgstr "раÑходÑÑ‚ÑÑ Ñ Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÑким репозиторием"
msgid "Branches|merged"
-msgstr ""
+msgstr "влита"
msgid "Branches|project settings"
-msgstr ""
+msgstr "наÑтройках проекта"
msgid "Branches|protected"
-msgstr ""
+msgstr "защищена"
msgid "Browse Directory"
-msgstr "Обзор"
+msgstr "Обзор каталога"
msgid "Browse File"
msgstr "ПроÑмотр файла"
@@ -337,12 +385,18 @@ msgstr "CI / CD"
msgid "CI configuration"
msgstr "ÐаÑтройка CI"
+msgid "CICD|Jobs"
+msgstr "ЗаданиÑ"
+
msgid "Cancel"
msgstr "Отмена"
msgid "Cancel edit"
msgstr "Отменить редактирование"
+msgid "Change Weight"
+msgstr "Изменить ВеÑ"
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Выбрать в ветке"
@@ -368,7 +422,10 @@ msgid "Cherry-pick this commit"
msgstr "Подобрать в Ñтом коммите"
msgid "Cherry-pick this merge request"
-msgstr "Побрать в Ñтом запроÑе на ÑлиÑние"
+msgstr "Подобрать Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
+
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr "Выберите группы, которые хотите Ñкопировать на вторичный узел. ОÑтавьте пуÑтым Ð´Ð»Ñ ÐºÐ¾Ð¿Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ðµ вÑего."
msgid "CiStatusLabel|canceled"
msgstr "отменено"
@@ -398,7 +455,7 @@ msgid "CiStatusLabel|waiting for manual action"
msgstr "ожидание ручных дейÑтвий"
msgid "CiStatusText|blocked"
-msgstr "блокировано"
+msgstr "заблокировано"
msgid "CiStatusText|canceled"
msgstr "отменено"
@@ -424,6 +481,147 @@ msgstr "пропущено"
msgid "CiStatus|running"
msgstr "выполнÑетÑÑ"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr "CircuitBreaker API"
+
+msgid "Clone repository"
+msgstr "Клонировать репозиторий"
+
+msgid "Close"
+msgstr "Закрыть"
+
+msgid "Cluster"
+msgstr "КлаÑтер"
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr "%{link_to_container_project} должен быть Ñоздан под Ñтой учетной запиÑью"
+
+msgid "ClusterIntegration|Cluster details"
+msgstr "Параметры клаÑтера"
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтеров"
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтеров отключена Ð´Ð»Ñ Ñтого проекта."
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтеров включена Ð´Ð»Ñ Ñтого проекта."
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr "Ð”Ð»Ñ Ñтого проекта включена Ð¸Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтеров. Отключение интеграции не повлиÑет на клаÑтер, но Ñоединение Ñ GitLab будет временно отключено."
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr "СоздаетÑÑ ÐºÐ»Ð°Ñтер в Google Container Engine..."
+
+msgid "ClusterIntegration|Cluster name"
+msgstr "Ðазвание клаÑтера"
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr "КлаÑтер был уÑпешно Ñоздан в Google Container Engine"
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr "Копировать название клаÑтера"
+
+msgid "ClusterIntegration|Create cluster"
+msgstr "Создать клаÑтер"
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr "Создать новый клаÑтер в Google Container Engine"
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr "Включить интеграцию Ñ ÐºÐ»Ð°Ñтерами"
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr "Идентификатор проекта в Google Cloud Platform"
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr "Google Container Engine"
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr "Проект Google Container Engine"
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr "Узнайте больше на %{link_to_documentation}"
+
+msgid "ClusterIntegration|Machine type"
+msgstr "Тип машины"
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr "УбедитеÑÑŒ, что ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ %{link_to_requirements} Ð´Ð»Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ñтеров"
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr "Управление интеграцией клаÑтера на вашем проекте Gitlab"
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr "УправлÑйте клаÑтером, Ð¿ÐµÑ€ÐµÐ¹Ð´Ñ Ð¿Ð¾ ÑÑылке %{link_gke}"
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr "КоличеÑтво узлов"
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr "ПожалуйÑта, убедитеÑÑŒ, что ваш аккаунт Google отвечает Ñледующим требованиÑм:"
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr "ПроÑтранÑтво имен проекта (необÑзательное, уникальное)"
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr "Прочтите нашу документацию %{link_to_help_page} по интеграции клаÑтера."
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr "Удалить интеграцию Ñ ÐºÐ»Ð°Ñтером"
+
+msgid "ClusterIntegration|Remove integration"
+msgstr "Удалить интеграцию"
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr "При удалении интеграции Ñ ÐºÐ»Ð°Ñтером будет удалена ÐºÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтера, которую вы добавили в Ñтот проект. Данное дейÑтвие не удалит Ñам проект."
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr "ПроÑмотреть и отредактировать параметры Ð´Ð»Ñ Ð²Ð°ÑˆÐµÐ³Ð¾ клаÑтера"
+
+msgid "ClusterIntegration|See machine types"
+msgstr "См. типы машин"
+
+msgid "ClusterIntegration|See your projects"
+msgstr "См. ваши проекты"
+
+msgid "ClusterIntegration|See zones"
+msgstr "См. зоны"
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr " У Ð½Ð°Ñ Ñ‡Ñ‚Ð¾-то пошло не так."
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr "Что-то пошло не так во Ð²Ñ€ÐµÐ¼Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ñтера в Google Container Engine"
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr "Переключить КлаÑтер"
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr "ЕÑли привÑзать клаÑтер к Ñтому проекту, вы Ñ Ð»Ñ‘Ð³ÐºÐ¾Ñтью Ñможете иÑпользовать Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ€ÐµÐ²ÑŒÑŽ, развертывать ваши приложениÑ, запуÑкать Ñборочные линии и многое другое."
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ должна иметь %{link_to_container_engine}"
+
+msgid "ClusterIntegration|Zone"
+msgstr "Зона"
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr "доÑтуп к Google Container Engine"
+
+msgid "ClusterIntegration|cluster"
+msgstr "клаÑтер"
+
+msgid "ClusterIntegration|help page"
+msgstr "Ñтраница Ñправки"
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr "отвечает требованиÑм"
+
+msgid "ClusterIntegration|properly configured"
+msgstr "правильно наÑтроен"
+
msgid "Comments"
msgstr "Комментарии"
@@ -433,8 +631,17 @@ msgstr[0] "Коммит"
msgstr[1] "Коммиты"
msgstr[2] "Коммиты"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] "ЗафикÑировать %d файл"
+msgstr[1] "ЗафикÑировать %d файла"
+msgstr[2] "ЗафикÑировать %d файлов"
+
+msgid "Commit Message"
+msgstr "ОпиÑание Коммита"
+
msgid "Commit duration in minutes for last 30 commits"
-msgstr "ПродолжительноÑÑ‚ÑŒ поÑледних 30 фикÑаций(коммитов) в минутах"
+msgstr "ПродолжительноÑÑ‚ÑŒ поÑледних 30 коммитов в минутах"
msgid "Commit message"
msgstr "ОпиÑание коммита"
@@ -443,31 +650,82 @@ msgid "CommitBoxTitle|Commit"
msgstr "Коммит"
msgid "CommitMessage|Add %{file_name}"
-msgstr "Добавлен %{file_name}"
+msgstr "Добавить %{file_name}"
msgid "Commits"
msgstr "Коммиты"
msgid "Commits feed"
-msgstr "ФикÑировать подачу"
+msgstr "Лента коммитов"
msgid "Commits|History"
msgstr "ИÑториÑ"
msgid "Committed by"
-msgstr "ФикÑировано"
+msgstr "ЗафикÑировано автором"
msgid "Compare"
msgstr "Сравнить"
+msgid "Container Registry"
+msgstr "РееÑÑ‚Ñ€ Контейнеров"
+
+msgid "ContainerRegistry|Created"
+msgstr "Создан"
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr "Сначала авторизуйтеÑÑŒ в рееÑтре контейнеров GitLab, иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ Ñвои Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸ пароль. ЕÑли у Ð²Ð°Ñ ÐµÑÑ‚ÑŒ %{link_2fa}, вам необходимо иÑпользовать %{link_token}:"
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr "GitLab поддерживает до трех уровней имён образов. Следующие примеры имён образов подходÑÑ‚ Ð´Ð»Ñ Ð²Ð°ÑˆÐµÐ³Ð¾ проекта:"
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr "Как иÑпользовать РееÑÑ‚Ñ€ Контейнеров"
+
+msgid "ContainerRegistry|Learn more about"
+msgstr "Узнайте больше"
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr "Ð’ рееÑтре контейнеров нет тегов Ð´Ð»Ñ Ñтого образа."
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr "ПоÑле того, как вы вошли в ÑиÑтему, вы можете Ñоздать или загрузить образ контейнера, иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ Ð¾Ð±Ñ‰Ð¸Ðµ команды %{build} и %{push}"
+
+msgid "ContainerRegistry|Remove repository"
+msgstr "Удалить репозиторий"
+
+msgid "ContainerRegistry|Remove tag"
+msgstr "Удалить тег"
+
+msgid "ContainerRegistry|Size"
+msgstr "Размер"
+
+msgid "ContainerRegistry|Tag"
+msgstr "Тег"
+
+msgid "ContainerRegistry|Tag ID"
+msgstr "Идентификатор Тега"
+
+msgid "ContainerRegistry|Use different image names"
+msgstr "ИÑпользовать различные имена образов"
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr "Когда рееÑÑ‚Ñ€ контейнеров Docker интегрирован Ñ GitLab, каждый проект может иметь Ñвое ÑобÑтвенное проÑтранÑтво Ð´Ð»Ñ Ñ…Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ ÐµÐ³Ð¾ Docker образов."
+
msgid "Contribution guide"
msgstr "РуководÑтво учаÑтника"
msgid "Contributors"
msgstr "УчаÑтники"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr "Контролировать макÑимальное количеÑтво потоков фоновой загрузки LFS/вложений Ð´Ð»Ñ Ñтого вторичного узла"
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr "Контролировать макÑимальное количеÑтво потоков фоновой загрузки хранилища Ð´Ð»Ñ Ñтого вторичного узла"
+
msgid "Copy SSH public key to clipboard"
-msgstr ""
+msgstr "Скопировать публичный ключ SSH в буфер обмена"
msgid "Copy URL to clipboard"
msgstr "Копировать URL в буфер обмена"
@@ -476,28 +734,37 @@ msgid "Copy commit SHA to clipboard"
msgstr "Копировать SHA коммита в буфер обмена"
msgid "Create New Directory"
-msgstr "Создать директорию"
-
-msgid "Create a new branch"
-msgstr "Создать новую ветку"
+msgstr "Создать Ðовый каталог"
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "Создать личный токен на аккаунте Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ отправки через %{protocol}."
msgid "Create directory"
-msgstr "Создать директорию"
+msgstr "Создать каталог"
msgid "Create empty bare repository"
msgstr "Создать пуÑтой репозиторий"
+msgid "Create file"
+msgstr "Создать файл"
+
msgid "Create merge request"
-msgstr "Создать Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° объединение"
+msgstr "Создать Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
+
+msgid "Create new branch"
+msgstr "Создать новую ветку"
+
+msgid "Create new directory"
+msgstr "Создать новый каталог"
+
+msgid "Create new file"
+msgstr "Создать новый файл"
msgid "Create new..."
msgstr "Ðовый"
msgid "CreateNewFork|Fork"
-msgstr "Форк"
+msgstr "Ответвить"
msgid "CreateTag|Tag"
msgstr "Тег"
@@ -512,38 +779,44 @@ msgid "Cron syntax"
msgstr "СинтакÑÐ¸Ñ Cron"
msgid "Custom notification events"
-msgstr " ÐаÑтраиваемые ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¾ ÑобытиÑÑ…"
+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 "Cycle Analytics"
-msgstr "Цикл Ðналитик"
+msgstr "Ðналитика Цикла"
msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
-msgstr "Цикл Ðналитик дает предÑтавление о том, Ñколько времени требуетÑÑ, чтобы перейти от идеи к производÑтву в проекте."
+msgstr "Ðналитика Цикла дает предÑтавление о том, Ñколько времени требуетÑÑ, чтобы перейти от идеи к производÑтву в вашем проекте."
msgid "CycleAnalyticsStage|Code"
msgstr "ÐапиÑание кода"
msgid "CycleAnalyticsStage|Issue"
-msgstr "Обращение"
+msgstr "ОбÑуждение"
msgid "CycleAnalyticsStage|Plan"
msgstr "Планирование"
msgid "CycleAnalyticsStage|Production"
-msgstr "ПроизводÑтво"
+msgstr "Продуктив"
msgid "CycleAnalyticsStage|Review"
msgstr "Контроль"
msgid "CycleAnalyticsStage|Staging"
-msgstr "ПоÑтановка"
+msgstr "Приёмка"
msgid "CycleAnalyticsStage|Test"
msgstr "ТеÑтирование"
+msgid "DashboardProjects|All"
+msgstr "Ð’Ñе"
+
+msgid "DashboardProjects|Personal"
+msgstr "Личные"
+
msgid "Define a custom pattern with cron syntax"
msgstr "Определить наÑтраиваемый шаблон Ñ ÑинтакÑиÑом cron"
@@ -552,9 +825,9 @@ msgstr "Удалить"
msgid "Deploy"
msgid_plural "Deploys"
-msgstr[0] "РазмеÑтить"
-msgstr[1] "Размещение"
-msgstr[2] "Размещение"
+msgstr[0] "Развернуть"
+msgstr[1] "Развертывание"
+msgstr[2] "Развертывание"
msgid "Deploy Keys"
msgstr "Ключи РазвертываниÑ"
@@ -562,15 +835,24 @@ msgstr "Ключи РазвертываниÑ"
msgid "Description"
msgstr "ОпиÑание"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr "Шаблоны опиÑаний позволÑÑŽÑ‚ вам определить Ñпецифичные шаблоны Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð¾Ð±Ñуждений и запроÑов на ÑлиÑние в вашем проекте."
+
msgid "Details"
msgstr "ÐŸÐ¾Ð´Ñ€Ð¾Ð±Ð½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ"
msgid "Directory name"
-msgstr "Каталог"
+msgstr "Ð˜Ð¼Ñ ÐºÐ°Ñ‚Ð°Ð»Ð¾Ð³Ð°"
msgid "Discard changes"
msgstr "Отменить изменениÑ"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr "Отключить блок Ð²Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð² Ðналитику Цикла"
+
+msgid "Dismiss Merge Request promotion"
+msgstr "Отключить анонÑÑ‹ Ð´Ð»Ñ Ð—Ð°Ð¿Ñ€Ð¾Ñов на СлиÑние"
+
msgid "Don't show again"
msgstr "Ðе показывать Ñнова"
@@ -605,14 +887,11 @@ msgid "Edit"
msgstr "Редактировать"
msgid "Edit Pipeline Schedule %{id}"
-msgstr "Изменить раÑпиÑание конвейера %{id}"
+msgstr "Изменить раÑпиÑание Ñборочной линии %{id}"
msgid "Emails"
msgstr "Email-адреÑа"
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr "Фильтр по вÑему"
@@ -638,16 +917,22 @@ msgid "Every month (on the 1st at 4:00am)"
msgstr "ЕжемеÑÑчно (каждое 1-е чиÑло в 4:00)"
msgid "Every week (Sundays at 4:00am)"
-msgstr "Еженедельно (по воÑкреÑениÑми в 4:00)"
+msgstr "Еженедельно (по воÑкреÑениÑм в 4:00)"
msgid "Explore projects"
-msgstr ""
+msgstr "Обзор проектов"
+
+msgid "Explore public groups"
+msgstr "ИÑÑледовать публичные группы"
msgid "Failed to change the owner"
msgstr "Ðе удалоÑÑŒ изменить владельца"
msgid "Failed to remove the pipeline schedule"
-msgstr "Ðе удалоÑÑŒ удалить раÑпиÑание конвейера"
+msgstr "Ðе удалоÑÑŒ удалить раÑпиÑание Ñборочной линии"
+
+msgid "File name"
+msgstr "Ð˜Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð°"
msgid "Files"
msgstr "Файлы"
@@ -665,19 +950,25 @@ msgid "FirstPushedBy|First"
msgstr "Первый"
msgid "FirstPushedBy|pushed by"
-msgstr "протолкнул"
+msgstr "отправлено автором"
msgid "Fork"
msgid_plural "Forks"
-msgstr[0] "Форк"
-msgstr[1] "Форки"
-msgstr[2] "Форки"
+msgstr[0] "Ответвление"
+msgstr[1] "ОтветвлениÑ"
+msgstr[2] "ОтветвлениÑ"
msgid "ForkedFromProjectPath|Forked from"
-msgstr "Форк от "
+msgstr "Ответвлено от"
+
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr "Ответвление от %{project_name} (удалено)"
+
+msgid "Format"
+msgstr "Формат"
msgid "From issue creation until deploy to production"
-msgstr "От ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ñ‹ до Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð² рабочей Ñреде"
+msgstr "От ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð¾Ð±ÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð´Ð¾ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ñ€ÐµÐ°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ð¸ в рабочей Ñреде"
msgid "From merge request merge until deploy to production"
msgstr "От запроÑа на ÑлиÑние до Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð² рабочей Ñреде"
@@ -686,7 +977,19 @@ msgid "GPG Keys"
msgstr "GPG Ключи"
msgid "Geo Nodes"
-msgstr ""
+msgstr "ГеографичеÑкие Узлы"
+
+msgid "Geo|File sync capacity"
+msgstr "Объем хранилища Ð´Ð»Ñ Ñинхронизации файлов"
+
+msgid "Geo|Groups to replicate"
+msgstr "Группы Ð´Ð»Ñ Ñ€ÐµÐ¿Ð»Ð¸ÐºÐ°Ñ†Ð¸Ð¸"
+
+msgid "Geo|Repository sync capacity"
+msgstr "Объем хранилища Ð´Ð»Ñ Ñинхронизации репозиториÑ"
+
+msgid "Geo|Select groups to replicate."
+msgstr "Выберите группы Ð´Ð»Ñ Ñ€ÐµÐ¿Ð»Ð¸ÐºÐ°Ñ†Ð¸Ð¸."
msgid "Git storage health information has been reset"
msgstr "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ ÑтабильноÑти Git хранилища была Ñброшена"
@@ -695,34 +998,82 @@ msgid "GitLab Runner section"
msgstr "Ð¡ÐµÐºÑ†Ð¸Ñ Gitlab Runner"
msgid "Go to your fork"
-msgstr "Перейти к вашему форку"
+msgstr "Перейти к вашему ответвлению"
msgid "GoToYourFork|Fork"
-msgstr "Форк"
+msgstr "Ответвление"
+
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr "ÐÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ Google не %{link_to_documentation}. ПопроÑите Ñвоего админиÑтратора GitLab, еÑли вы хотите воÑпользоватьÑÑ Ñтим ÑервиÑом."
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
-msgstr ""
+msgstr "Запретить публикацию проектов из %{group} в других группах"
msgid "GroupSettings|Share with group lock"
-msgstr ""
+msgstr "Опубликовать Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð¾Ð²Ð¾Ð¹ блокировкой"
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
-msgstr ""
+msgstr "Эта наÑтройка применена в %{ancestor_group} и была переопределена в Ñтой подгруппе."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Эта наÑтройка применена в %{ancestor_group}. Чтобы поделитьÑÑ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð°Ð¼Ð¸ из Ñтой группы Ñ Ð´Ñ€ÑƒÐ³Ð¸Ð¼Ð¸ группами, запроÑите у владельца переопределить Ñту наÑтройку или %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Эта наÑтройка применена в %{ancestor_group}. Ð’Ñ‹ можете переопределить Ñту наÑтройку или %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
-msgstr ""
+msgstr "Эта наÑтройка будет применена Ð´Ð»Ñ Ð²Ñех подгрупп еÑли не будет переопределена владельцем группы. Группы которые уже имеют доÑтуп к проекту, будут иметь его и дальше пока не будут удалены вручную."
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
-msgstr ""
+msgstr "не может быть отменена до тех пор пока группа \"ПоделитьÑÑ Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð¾Ð²Ð¾Ð¹ блокировкой\" включена, за иÑключением владельца группы родителÑ"
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
-msgstr ""
+msgstr "удалить возможноÑÑ‚ÑŒ поделитьÑÑ Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð¾Ð²Ð¾Ð¹ блокировкой из %{ancestor_group_name}"
+
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr "Группа - Ñто набор из неÑкольких проектов."
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr "При размещении проектов в группе, группа выполнÑет функции папки."
+
+msgid "GroupsEmptyState|No groups found"
+msgstr "Группы не найдены"
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr "Ð’Ñ‹ можете управлÑÑ‚ÑŒ правами и доÑтупом учаÑтников вашей группы к каждому проекту в группе."
+
+msgid "GroupsTreeRole|as"
+msgstr "как"
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr "Вы уверены, что вы хотите покинуть группу \"${this.group.fullName}\"?"
+
+msgid "GroupsTree|Create a project in this group."
+msgstr "Создать проект в Ñтой группе."
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr "Создать подгруппу в Ñтой группе."
+
+msgid "GroupsTree|Edit group"
+msgstr "Редактировать группу"
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr "Ðе удалоÑÑŒ покинуть группу. ПожалуйÑта, убедитеÑÑŒ, что вы не единÑтвенный владелец."
+
+msgid "GroupsTree|Filter by name..."
+msgstr "Фильтр по имени..."
+
+msgid "GroupsTree|Leave this group"
+msgstr "Покинуть Ñту группу"
+
+msgid "GroupsTree|Loading groups"
+msgstr "Загрузка групп"
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr "К Ñожалению, по вашему запроÑу групп не найдено"
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr "К Ñожалению, по вашему запроÑу групп или проектов не найдено"
msgid "Health Check"
msgstr "Проверка работоÑпоÑобноÑти"
@@ -742,29 +1093,62 @@ msgstr "Проблем работоÑпоÑобноÑти не обнаружеÐ
msgid "HealthCheck|Unhealthy"
msgstr "ÐеÑтабильный"
+msgid "History"
+msgstr "ИÑториÑ"
+
msgid "Housekeeping successfully started"
msgstr "ОчиÑтка уÑпешно запущена"
msgid "Import repository"
msgstr "Импорт репозиториÑ"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr "Улучшить доÑки обÑуждений Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ верÑии GitLab Enterprise Edition."
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr "Улучшить управление обÑуждениÑми Ñ Ð²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾Ñтью Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ Ð²ÐµÑа обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¸ помощи GitLab Enterprise Edition."
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr "Улучшить поиÑк при помощи РаÑширенного Глобального ПоиÑка в верÑии GitLab Enterprise Edition."
+
msgid "Install a Runner compatible with GitLab CI"
msgstr "УÑтановите Gitlab Runner ÑовмеÑтимый Ñ Gitlab CI"
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] "ЭкземплÑÑ€"
+msgstr[1] "ЭкземплÑры"
+msgstr[2] "ЭкземплÑры"
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr "Внутренний - Группу и включённые в неё проекты может видеть любой зарегиÑтрированный пользователь."
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr "Внутренний - Проект доÑтупен любому зарегиÑтрированному пользователю."
+
msgid "Interval Pattern"
msgstr "Шаблон интервала"
msgid "Introducing Cycle Analytics"
msgstr "Внедрение Цикла Ðналитик"
+msgid "Issue board focus mode"
+msgstr "Режим фокуÑировки над доÑкой обÑуждений"
+
+msgid "Issue boards with milestones"
+msgstr "ДоÑки обÑуждений Ñ Ð²ÐµÑ…Ð°Ð¼Ð¸"
+
msgid "Issue events"
-msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð·Ð°Ð´Ð°Ñ‡Ð¸"
+msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð¾Ð±Ñуждений"
-msgid "Issues"
-msgstr "Задачи"
+msgid "IssueBoards|Board"
+msgstr "ДоÑка"
-msgid "Jobs"
-msgstr ""
+msgid "IssueBoards|Boards"
+msgstr "ДоÑки"
+
+msgid "Issues"
+msgstr "ОбÑуждениÑ"
msgid "LFSStatus|Disabled"
msgstr "Отключено"
@@ -782,14 +1166,23 @@ msgstr[1] "ПоÑледние %d дни"
msgstr[2] "ПоÑледние %d дни"
msgid "Last Pipeline"
-msgstr "ПоÑледний конвейер"
-
-msgid "Last Update"
-msgstr "ПоÑледнее обновление"
+msgstr "ПоÑледнÑÑ Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð›Ð¸Ð½Ð¸Ñ"
msgid "Last commit"
msgstr "ПоÑледний коммит"
+msgid "Last edited %{date}"
+msgstr "Дата поÑледнего изменениÑ: %{date}"
+
+msgid "Last edited by %{name}"
+msgstr "Ðвтор поÑледнего изменениÑ: %{name}"
+
+msgid "Last update"
+msgstr "ПоÑледнее обновление"
+
+msgid "Last updated"
+msgstr "ПоÑледний раз обновлено"
+
msgid "LastPushEvent|You pushed to"
msgstr "Вы отправили в"
@@ -800,7 +1193,10 @@ msgid "Learn more in the"
msgstr "Узнайте больше в"
msgid "Learn more in the|pipeline schedules documentation"
-msgstr "Подробнее в|документации по раÑпиÑаниÑм конвейеров"
+msgstr "Подробнее в|документации по раÑпиÑаниÑм Ñборочных линий"
+
+msgid "Leave"
+msgstr "Покинуть"
msgid "Leave group"
msgstr "Покинуть группу"
@@ -809,16 +1205,28 @@ msgid "Leave project"
msgstr "Покинуть проект"
msgid "License"
-msgstr ""
+msgstr "ЛицензиÑ"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
-msgstr[0] "Ограничение %d ÑобытиÑ"
-msgstr[1] "Ограничение %d Ñобытий"
-msgstr[2] "Ограничение %d Ñобытий"
+msgstr[0] "Показывать %d Ñобытие макÑимум"
+msgstr[1] "Показывать %d ÑÐ¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð¼Ð°ÐºÑимум"
+msgstr[2] "Показывать %d Ñобытий макÑимум"
+
+msgid "Lock"
+msgstr "Блокировка"
+
+msgid "Locked"
+msgstr "Заблокировано"
msgid "Locked Files"
-msgstr ""
+msgstr "Заблокированные Файлы"
+
+msgid "Login"
+msgstr "Войти"
+
+msgid "Maximum git storage failures"
+msgstr "МакÑимальное количеÑтво Ñбоев хранилища git"
msgid "Median"
msgstr "Среднее"
@@ -833,7 +1241,7 @@ msgid "Merge events"
msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ ÑлиÑний"
msgid "Merge request"
-msgstr ""
+msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "Messages"
msgstr "СообщениÑ"
@@ -847,68 +1255,89 @@ msgstr "Мониторинг"
msgid "More information is available|here"
msgstr "Больше информации доÑтупно|тут"
+msgid "Multiple issue boards"
+msgstr "Сводные доÑки задач"
+
+msgid "New Cluster"
+msgstr "Ðовый КлаÑтер"
+
msgid "New Issue"
msgid_plural "New Issues"
-msgstr[0] "Обращение"
-msgstr[1] "ОбращениÑ"
-msgstr[2] "ОбращениÑ"
+msgstr[0] "Ðовое ОбÑуждение"
+msgstr[1] "Ðовых ОбÑуждениÑ"
+msgstr[2] "Ðовых ОбÑуждений"
msgid "New Pipeline Schedule"
-msgstr "Ðовое раÑпиÑание конвейера"
+msgstr "Ðовое РаÑпиÑание Сборочной Линии"
msgid "New branch"
msgstr "ÐÐ¾Ð²Ð°Ñ Ð²ÐµÑ‚ÐºÐ°"
msgid "New directory"
-msgstr "ÐÐ¾Ð²Ð°Ñ Ð´Ð¸Ñ€ÐµÐºÑ‚Ð¾Ñ€Ð¸Ñ"
+msgstr "Ðовый каталог"
msgid "New file"
msgstr "Ðовый файл"
+msgid "New group"
+msgstr "ÐÐ¾Ð²Ð°Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð°"
+
msgid "New issue"
-msgstr "Ðовое обращение"
+msgstr "Ðовое обÑуждение"
msgid "New merge request"
-msgstr "Ðовый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° объединение"
+msgstr "Ðовый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
+
+msgid "New project"
+msgstr "Ðовый проект"
msgid "New schedule"
msgstr "Ðовое раÑпиÑание"
msgid "New snippet"
-msgstr "Ðовый Ñниппет"
+msgstr "Ðовый фрагмент"
+
+msgid "New subgroup"
+msgstr "ÐÐ¾Ð²Ð°Ñ Ð¿Ð¾Ð´Ð³Ñ€ÑƒÐ¿Ð¿Ð°"
msgid "New tag"
msgstr "Ðовый тег"
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr "Ðет образов контейнеров Ð´Ð»Ñ Ñтого проекта. Добавьте образ, ÑÐ»ÐµÐ´ÑƒÑ Ð¸Ð½ÑтрукциÑм выше."
+
msgid "No repository"
msgstr "Ðет репозиториÑ"
msgid "No schedules"
-msgstr "Ðет раÑпиÑаниÑ"
+msgstr "Ðет раÑпиÑаний"
+
+msgid "None"
+msgstr "ПуÑто"
msgid "Not available"
msgstr "ÐедоÑтупно"
msgid "Not enough data"
-msgstr "Ðет данных"
+msgstr "ÐедоÑтаточно данных"
msgid "Notification events"
msgstr "Ð£Ð²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¾ ÑобытиÑÑ…"
msgid "NotificationEvent|Close issue"
-msgstr "Обращение закрыто"
+msgstr "ОбÑуждение закрыто"
msgid "NotificationEvent|Close merge request"
-msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° объединение закрыт"
+msgstr "Закрыт Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "NotificationEvent|Failed pipeline"
-msgstr "Ðеудача в конвейере"
+msgstr "Ðеудача в Ñборочной линии"
msgid "NotificationEvent|Merge merge request"
-msgstr "Объединить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
+msgstr "Влит Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "NotificationEvent|New issue"
-msgstr "Ðовое обращение"
+msgstr "Ðовое обÑуждение"
msgid "NotificationEvent|New merge request"
msgstr "Ðовый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
@@ -917,16 +1346,16 @@ msgid "NotificationEvent|New note"
msgstr "ÐÐ¾Ð²Ð°Ñ Ð·Ð°Ð¼ÐµÑ‚ÐºÐ°"
msgid "NotificationEvent|Reassign issue"
-msgstr "Переназначить обращение"
+msgstr "Переназначить обÑуждение"
msgid "NotificationEvent|Reassign merge request"
-msgstr "Переназначить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
+msgstr "Переназначен Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "NotificationEvent|Reopen issue"
-msgstr "Переоткрыть обращение"
+msgstr "Переоткрыть обÑуждение"
msgid "NotificationEvent|Successful pipeline"
-msgstr "УÑпешно в конвейере"
+msgstr "УÑÐ¿ÐµÑˆÐ½Ð°Ñ ÑÐ±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ"
msgid "NotificationLevel|Custom"
msgstr "ÐаÑтраиваемый"
@@ -949,12 +1378,24 @@ msgstr "ОтÑлеживать"
msgid "Notifications"
msgstr "УведомлениÑ"
+msgid "Number of access attempts"
+msgstr "КоличеÑтво попыток доÑтупа"
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Фильтр"
+msgid "Only project members can comment."
+msgstr "Только учаÑтники проекта могут оÑтавлÑÑ‚ÑŒ комментарии."
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Открыто"
+msgid "Opens in a new window"
+msgstr "ОткроетÑÑ Ð² новом окне"
+
msgid "Options"
msgstr "ÐаÑтройки"
@@ -965,34 +1406,37 @@ msgid "Owner"
msgstr "Владелец"
msgid "Pagination|Last »"
-msgstr ""
+msgstr "ПоÑледнÑÑ Â»"
msgid "Pagination|Next"
-msgstr ""
+msgstr "СледующаÑ"
msgid "Pagination|Prev"
-msgstr ""
+msgstr "ПредыдущаÑ"
msgid "Pagination|« First"
-msgstr ""
+msgstr "« ПерваÑ"
msgid "Password"
msgstr "Пароль"
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr "Люди без разрешений не получат уведомление и не Ñмогут комментировать."
+
msgid "Pipeline"
-msgstr "Конвейер"
+msgstr "Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ"
msgid "Pipeline Health"
-msgstr "Жизненный цикл конвейера"
+msgstr "РаботоÑпоÑобноÑÑ‚ÑŒ Сборочной Линии"
msgid "Pipeline Schedule"
-msgstr "РаÑпиÑание конвейера"
+msgstr "РаÑпиÑание Сборочной Линии"
msgid "Pipeline Schedules"
-msgstr "РаÑпиÑÐ°Ð½Ð¸Ñ ÐºÐ¾Ð½Ð²ÐµÐ¹ÐµÑ€Ð¾Ð²"
+msgstr "РаÑпиÑÐ°Ð½Ð¸Ñ Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ñ‹Ñ… Линий"
msgid "Pipeline quota"
-msgstr ""
+msgstr "Квота Ñборочной линии"
msgid "PipelineCharts|Failed:"
msgstr "Ðеудача:"
@@ -1034,7 +1478,7 @@ msgid "PipelineSchedules|None"
msgstr "ОтÑутÑтвует"
msgid "PipelineSchedules|Provide a short description for this pipeline"
-msgstr "ПредоÑтавьте краткое опиÑание Ñтого конвейера"
+msgstr "ПредоÑтавьте краткое опиÑание Ñтой Ñборочной линии"
msgid "PipelineSchedules|Remove variable row"
msgstr "Удалить значение"
@@ -1052,19 +1496,19 @@ msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "ÐаÑтраиваемый"
msgid "Pipelines"
-msgstr "Конвейер"
+msgstr "Сборочные линии"
msgid "Pipelines charts"
-msgstr "Диаграмма конвейера"
+msgstr "Диаграммы Ñборочных линий"
msgid "Pipelines for last month"
-msgstr "Конвеер за поÑледний меÑÑц"
+msgstr "Сборочные линии за поÑледний меÑÑц"
msgid "Pipelines for last week"
-msgstr "Конвеер за поÑледнюю неделю"
+msgstr "Сборочные линии за поÑледнюю неделю"
msgid "Pipelines for last year"
-msgstr "Конвееры за поÑледний год"
+msgstr "Сборочные линии за поÑледний год"
msgid "Pipeline|all"
msgstr "вÑе"
@@ -1081,8 +1525,53 @@ msgstr "Ñо ÑтадиÑми"
msgid "Preferences"
msgstr "ПредпочтениÑ"
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr "Приватный - ДоÑтуп к проекту должен предоÑтавлÑÑ‚ÑŒÑÑ Ñвно каждому пользователю."
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr "ÐŸÑ€Ð¸Ð²Ð°Ñ‚Ð½Ð°Ñ - Группу и включённые в неё проекты могут видеть только члены группы."
+
msgid "Profile"
-msgstr ""
+msgstr "Профиль"
+
+msgid "Profiles|Account scheduled for removal."
+msgstr "Ð£Ñ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ запланирована к удалению."
+
+msgid "Profiles|Delete Account"
+msgstr "Удалить Учетную запиÑÑŒ"
+
+msgid "Profiles|Delete account"
+msgstr "Удалить учетную запиÑÑŒ"
+
+msgid "Profiles|Delete your account?"
+msgstr "Удалить Ñвою учетную запиÑÑŒ?"
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr "Удаление учетной запиÑи приведет к Ñледующим поÑледÑтвиÑм:"
+
+msgid "Profiles|Invalid password"
+msgstr "Ðеверный пароль"
+
+msgid "Profiles|Invalid username"
+msgstr "Ðеверное Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr "Введите значение %{confirmationValue} Ð´Ð»Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ:"
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr "У Ð²Ð°Ñ Ð½ÐµÑ‚ прав на удаление Ñтого пользователÑ."
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr "Перед удалением учётной запиÑи, вам необходимо передать право Ð²Ð»Ð°Ð´ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ удалить Ñти группы."
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ ÑвлÑетÑÑ Ð²Ð»Ð°Ð´ÐµÐ»ÑŒÑ†ÐµÐ¼ Ñледующих групп:"
+
+msgid "Profiles|your account"
+msgstr "ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ"
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr "Проект '%{project_name}' находитÑÑ Ð² процеÑÑе удалениÑ."
msgid "Project '%{project_name}' queued for deletion."
msgstr "Проект '%{project_name}' добавлен в очередь на удаление."
@@ -1093,9 +1582,6 @@ msgstr "Проект '%{project_name}' уÑпешно Ñоздан."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Проект '%{project_name}' уÑпешно обновлен."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "Проект '%{project_name}' удален."
-
msgid "Project access must be granted explicitly to each user."
msgstr "ДоÑтуп к проекту должен предоÑтавлÑÑ‚ÑŒÑÑ Ñвно каждому пользователю."
@@ -1138,9 +1624,30 @@ msgstr "Этап"
msgid "ProjectNetworkGraph|Graph"
msgstr "Граф"
-msgid "ProjectsDropdown|Frequently visited"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr "ОбратитеÑÑŒ к админиÑтратору Ð´Ð»Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñтой наÑтройки."
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr "Только подпиÑанные коммиты могут быть помещены в Ñтот репозиторий."
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr "Эта наÑтройка применÑетÑÑ Ð½Ð° уровне Ñервера и может быть переопределена админиÑтратором."
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr "Эта наÑтройка применÑетÑÑ Ð½Ð° уровне Ñервера, но была переопределена Ð´Ð»Ñ Ñтого проекта."
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr "Эта наÑтройка будет применена Ð´Ð»Ñ Ð²Ñех проектов, еÑли иное поведение не переопределено админиÑтратором."
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
msgstr ""
+msgid "Projects"
+msgstr "Проекты"
+
+msgid "ProjectsDropdown|Frequently visited"
+msgstr "ЧаÑто поÑещаемые"
+
msgid "ProjectsDropdown|Loading projects"
msgstr "Загрузка проектов"
@@ -1151,7 +1658,7 @@ msgid "ProjectsDropdown|Search your projects"
msgstr "ПоиÑк по вашим проектам"
msgid "ProjectsDropdown|Something went wrong on our end."
-msgstr ""
+msgstr "У Ð½Ð°Ñ Ñ‡Ñ‚Ð¾-то пошло не так."
msgid "ProjectsDropdown|Sorry, no projects matched your search"
msgstr "К Ñожалению, по вашему запроÑу проекты не найдены"
@@ -1159,12 +1666,21 @@ msgstr "К Ñожалению, по вашему запроÑу проекты Ð
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Эта функциональноÑÑ‚ÑŒ требует поддержки localStorage в вашем браузере"
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr "Публичный - Группу и включённые в неё проекты могут видеть вÑе, без какой-либо проверки подлинноÑти."
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr "Публичный - ДоÑтуп к проекту возможен без какой-либо проверки подлинноÑти."
+
msgid "Push Rules"
-msgstr ""
+msgstr "Правила Отправки"
msgid "Push events"
msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð¾Ñ‚Ð¿Ñ€Ð°Ð²ÐºÐ¸"
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "Подробнее"
@@ -1178,7 +1694,7 @@ msgid "RefSwitcher|Tags"
msgstr "Теги"
msgid "Registry"
-msgstr ""
+msgstr "РееÑÑ‚Ñ€"
msgid "Related Commits"
msgstr "СвÑзанные коммиты"
@@ -1187,16 +1703,16 @@ msgid "Related Deployed Jobs"
msgstr "СвÑзанные задачи выгрузки"
msgid "Related Issues"
-msgstr "СвÑзанные вопроÑÑ‹"
+msgstr "СвÑзанные ОбÑуждениÑ"
msgid "Related Jobs"
msgstr "СвÑзанные задачи"
msgid "Related Merge Requests"
-msgstr "СвÑзанные запроÑÑ‹ на ÑлиÑние"
+msgstr "СвÑзанные ЗапроÑÑ‹ на СлиÑние"
msgid "Related Merged Requests"
-msgstr "СвÑзанные объединенные запроÑÑ‹"
+msgstr "СвÑзанные Влитые ЗапроÑÑ‹"
msgid "Remind later"
msgstr "Ðапомнить позже"
@@ -1220,7 +1736,7 @@ msgid "Reset runners registration token"
msgstr "СброÑить ключ региÑтрации Gitlab Runners"
msgid "Revert this commit"
-msgstr "Отменить Ñто изменение"
+msgstr "Отменить Ñто коммит"
msgid "Revert this merge request"
msgstr "Отменить Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
@@ -1228,30 +1744,42 @@ msgstr "Отменить Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "SSH Keys"
msgstr "SSH Ключи"
+msgid "Save"
+msgstr "Сохранить"
+
+msgid "Save changes"
+msgstr "Сохранить изменениÑ"
+
msgid "Save pipeline schedule"
-msgstr "Сохранить раÑпиÑание конвейра"
+msgstr "Сохранить раÑпиÑание Ñборочной лини"
msgid "Schedule a new pipeline"
-msgstr "РаÑпиÑание нового конвейера"
+msgstr "РаÑпиÑание новой Ñборочной линии"
msgid "Schedules"
-msgstr ""
+msgstr "РаÑпиÑаниÑ"
msgid "Scheduling Pipelines"
-msgstr "Планирование конвейеров"
+msgstr "Планирование Сборочных Линий"
msgid "Search branches and tags"
msgstr "Ðайти ветки и теги"
+msgid "Seconds before reseting failure information"
+msgstr "Секунд до очиÑтки информации о ÑбоÑÑ…"
+
+msgid "Seconds to wait after a storage failure"
+msgstr "Секунд задержки поÑле ÑÐ±Ð¾Ñ Ð² хранилище"
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr "Секунд задержки между попытками доÑтупа к хранилищу"
+
msgid "Select Archive Format"
msgstr "Выбрать формат архива"
msgid "Select a timezone"
msgstr "Выбор временной зоны"
-msgid "Select existing branch"
-msgstr "Выбрать ÑущеÑтвующую ветвь"
-
msgid "Select target branch"
msgstr "Выбор целевой ветки"
@@ -1271,16 +1799,16 @@ msgid "Set up auto deploy"
msgstr "ÐаÑтройка автоматичеÑкого развертываниÑ"
msgid "SetPasswordToCloneLink|set a password"
-msgstr "уÑтановить пароль"
+msgstr "уÑтановите пароль"
msgid "Settings"
msgstr "ÐаÑтройки"
msgid "Show parent pages"
-msgstr ""
+msgstr "Показать родительÑкие Ñтраницы"
msgid "Show parent subgroups"
-msgstr ""
+msgstr "Показать родительÑкие подгруппы"
msgid "Showing %d event"
msgid_plural "Showing %d events"
@@ -1291,101 +1819,116 @@ msgstr[2] "Показано %d Ñобытий"
msgid "Snippets"
msgstr "Сниппеты"
+msgid "Something went wrong on our end."
+msgstr "У Ð½Ð°Ñ Ñ‡Ñ‚Ð¾-то пошло не так."
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr "Что-то пошло не так при попытке Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð·Ð°Ð±Ð»Ð¾ÐºÐ¸Ñ€Ð¾Ð²Ð°Ð½Ð½Ð¾Ð³Ð¾ ÑоÑтоÑÐ½Ð¸Ñ Ñтого ${this.issuableDisplayName(this.issuableType)}"
+
+msgid "Something went wrong while fetching the projects."
+msgstr "Что-то пошло не так при получении проектов."
+
+msgid "Something went wrong while fetching the registry list."
+msgstr "Что-то пошло не так при получении ÑпиÑка рееÑтров."
+
+msgid "Sort by"
+msgstr "Сортировать по"
+
msgid "SortOptions|Access level, ascending"
-msgstr ""
+msgstr "Уровень доÑтупа, по возраÑтанию"
msgid "SortOptions|Access level, descending"
-msgstr ""
+msgstr "Уровень доÑтупа, по убыванию"
msgid "SortOptions|Created date"
-msgstr ""
+msgstr "Дата ÑозданиÑ"
msgid "SortOptions|Due date"
-msgstr ""
+msgstr "Срок"
msgid "SortOptions|Due later"
-msgstr ""
+msgstr "Срок позже"
msgid "SortOptions|Due soon"
-msgstr ""
+msgstr "Срок раньше"
msgid "SortOptions|Label priority"
-msgstr ""
+msgstr "Приоритет метки"
msgid "SortOptions|Largest group"
-msgstr ""
+msgstr "ÐšÑ€ÑƒÐ¿Ð½ÐµÐ¹ÑˆÐ°Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð°"
msgid "SortOptions|Largest repository"
-msgstr ""
+msgstr "Крупнейший репозиторий"
msgid "SortOptions|Last created"
-msgstr ""
+msgstr "ПоÑледние Ñозданные"
msgid "SortOptions|Last joined"
-msgstr ""
+msgstr "ПоÑледние проÑоединившиеÑÑ"
msgid "SortOptions|Last updated"
-msgstr ""
+msgstr "ПоÑледние обновлённые"
msgid "SortOptions|Least popular"
-msgstr ""
+msgstr "Ðаименее популÑрный"
msgid "SortOptions|Less weight"
-msgstr ""
+msgstr "Меньший веÑ"
msgid "SortOptions|Milestone"
-msgstr ""
+msgstr "Веха"
msgid "SortOptions|Milestone due later"
-msgstr ""
+msgstr "Веха, наÑÑ‚ÑƒÐ¿Ð°ÑŽÑ‰Ð°Ñ Ð¿Ð¾Ð·Ð´Ð½ÐµÐµ"
msgid "SortOptions|Milestone due soon"
-msgstr ""
+msgstr "Веха, наÑÑ‚ÑƒÐ¿Ð°ÑŽÑ‰Ð°Ñ Ñ€Ð°Ð½ÑŒÑˆÐµ"
msgid "SortOptions|More weight"
-msgstr ""
+msgstr "Больший веÑ"
msgid "SortOptions|Most popular"
-msgstr ""
+msgstr "Ðаиболее популÑрный"
msgid "SortOptions|Name"
-msgstr ""
+msgstr "ИмÑ"
msgid "SortOptions|Name, ascending"
-msgstr ""
+msgstr "ИмÑ, по возраÑтанию"
msgid "SortOptions|Name, descending"
-msgstr ""
+msgstr "ИмÑ, по убыванию"
msgid "SortOptions|Oldest created"
-msgstr ""
+msgstr "Старейшие из Ñозданных"
msgid "SortOptions|Oldest joined"
-msgstr ""
+msgstr "Старейшие из приÑоединившихÑÑ"
msgid "SortOptions|Oldest sign in"
-msgstr ""
+msgstr "Старейшие из заходивших"
msgid "SortOptions|Oldest updated"
-msgstr ""
+msgstr "Старейших из обновленных"
msgid "SortOptions|Popularity"
-msgstr ""
+msgstr "ПопулÑронÑÑ‚ÑŒ"
msgid "SortOptions|Priority"
-msgstr ""
+msgstr "Приоритет"
msgid "SortOptions|Recent sign in"
-msgstr ""
+msgstr "Ðедавно заходившие"
msgid "SortOptions|Start later"
-msgstr ""
+msgstr "Ðачатые позже"
msgid "SortOptions|Start soon"
-msgstr ""
+msgstr "Ðачатые недавно"
msgid "SortOptions|Weight"
-msgstr ""
+msgstr "ВеÑ"
msgid "Source code"
msgstr "ИÑходный код"
@@ -1400,7 +1943,7 @@ msgid "StarProject|Star"
msgstr "Отметить"
msgid "Starred projects"
-msgstr ""
+msgstr "Отмеченные проекты"
msgid "Start a %{new_merge_request} with these changes"
msgstr "Ðачать %{new_merge_request} Ñ Ñтих изменений"
@@ -1408,16 +1951,22 @@ msgstr "Ðачать %{new_merge_request} Ñ Ñтих изменений"
msgid "Start the Runner!"
msgstr "ЗапуÑтить GitLab Runner!"
+msgid "Subgroups"
+msgstr "Подгруппы"
+
+msgid "Subscribe"
+msgstr "ПодпиÑатьÑÑ"
+
msgid "Switch branch/tag"
msgstr "Переключить ветка/тег"
msgid "System Hooks"
-msgstr ""
+msgstr "СиÑтемные Обработчики"
msgid "Tag"
msgid_plural "Tags"
msgstr[0] "Тег"
-msgstr[1] "теги"
+msgstr[1] "Теги"
msgstr[2] "Теги"
msgid "Tags"
@@ -1429,29 +1978,47 @@ msgstr "Ветка"
msgid "Team"
msgstr "Команда"
+msgid "Thanks! Don't show me this again"
+msgstr "СпаÑибо! Больше не показывайте мне Ñто Ñообщение"
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr "РаÑширенный глобальный поиÑк в GitLab - Ñто Ñерьезный инÑтрумент который Ñокращает ваше времÑ. ВмеÑто ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð´ÑƒÐ±Ð»Ð¸Ñ€ÑƒÑŽÑ‰ÐµÐ³Ð¾ кода и траты времени, вы можете иÑкать код внутри других команд, который поможет вам в вашем проекте."
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr "Порог ÑÑ€Ð°Ð±Ð°Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð´Ð»Ñ Ð¡ircuitBreaker должен быть меньше, чем порог ÑÑ€Ð°Ð±Ð°Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð´Ð»Ñ Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ ÑбоÑ"
+
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 "Ðа Ñтапе напиÑÐ°Ð½Ð¸Ñ ÐºÐ¾Ð´Ð° показывает Ð²Ñ€ÐµÐ¼Ñ Ð¿ÐµÑ€Ð²Ð¾Ð³Ð¾ коммита до ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñа на ÑлиÑние. Данные автоматичеÑки добавÑÑ‚ÑÑ Ð¿Ð¾Ñле того, как вы Ñоздать Ñвой первый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние."
+msgstr "Этап напиÑÐ°Ð½Ð¸Ñ ÐºÐ¾Ð´Ð° показывает Ð²Ñ€ÐµÐ¼Ñ Ñ Ð¿ÐµÑ€Ð²Ð¾Ð³Ð¾ коммита до ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñа на ÑлиÑние. Данные автоматичеÑки добавÑÑ‚ÑÑ Ñюда поÑле того, как вы Ñоздать Ñвой первый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние."
msgid "The collection of events added to the data gathered for that stage."
msgstr "ÐšÐ¾Ð»Ð»ÐµÐºÑ†Ð¸Ñ Ñобытий добавленных в данные Ñобранные Ð´Ð»Ñ Ñтого Ñтапа."
msgid "The fork relationship has been removed."
-msgstr "СвÑзь форка удалена."
+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 "Ð¡Ñ‚Ð°Ð´Ð¸Ñ Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð²Ñ€ÐµÐ¼Ñ, которое потребуетÑÑ Ñ Ð¼Ð¾Ð¼ÐµÐ½Ñ‚Ð° ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð´Ð¾ Ð½Ð°Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸ÑŽ вехи, или Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð² вашу доÑку обращений. Ðачните Ñоздавать обращениÑ, чтобы увидеть ÑÐ²ÐµÐ´ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñтой Ñтадии. "
+msgstr "Ð¡Ñ‚Ð°Ð´Ð¸Ñ Ð¾Ð±ÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¿Ð¾ÐºÐ°Ð·Ñ‹Ð²Ð°ÐµÑ‚ времÑ, которое потребуетÑÑ Ñ Ð¼Ð¾Ð¼ÐµÐ½Ñ‚Ð° ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð¾Ð±ÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð´Ð¾ Ð½Ð°Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ð¾Ð±Ñуждению вехи, или Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¾Ð±ÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð½Ð° вашу доÑку задач. Ðачните Ñоздавать обÑуждениÑ, чтобы увидеть ÑÐ²ÐµÐ´ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñтой Ñтадии."
+
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr "КоличеÑтво попыток, которые GitLab будет предпринимать Ð´Ð»Ñ Ð´Ð¾Ñтупа к хранилищу."
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr "КоличеÑтво ошибок, поÑле которого GitLab начнёт временно отключать доÑтуп к шарду хранилища на хоÑте"
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr "КоличеÑтво Ñбоев, поÑле которого Gitlab полноÑтью прекратит доÑтуп к хранилищу. Изменение Ñчётчика \"количеÑтво Ñбоев\" может быть произведено через админиÑтративный интерфейÑ: %{link_to_health_page} или при помощи API %{api_documentation_link}."
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 "РаÑпиÑание конвейеров запуÑкает в будущем неоднократно конвейеры, Ð´Ð»Ñ Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð½Ñ‹Ñ… ветвей или тегов. Запланированные конвейеры наÑледуют Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ Ð½Ð° доÑтуп к проекту на оÑнове ÑвÑзанного Ñ Ð½Ð¸Ð¼Ð¸ пользователÑ."
+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 "Ðа Ñтапе Ð¿Ð»Ð°Ð½Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¿Ð¾ÐºÐ°Ð·Ñ‹Ð²Ð°ÐµÑ‚ Ð²Ñ€ÐµÐ¼Ñ Ð¾Ñ‚ предыдущего шага до Ð¿Ñ€Ð¾Ñ‚Ð°Ð»ÐºÐ¸Ð²Ð°Ð½Ð¸Ñ Ð¿ÐµÑ€Ð²Ð¾Ð³Ð¾ коммита. ДобавлÑетÑÑ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑки, как только проталкиваете Ñвой первый коммит."
+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 "ПроизводÑтвенный Ñтап показывает общее Ð²Ñ€ÐµÐ¼Ñ Ð¼ÐµÐ¶Ð´Ñƒ Ñозданием задачи и развертывание кода в производÑтвенной Ñреде. Данные будут автоматичеÑки добавлены поÑле полного Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ð¸Ð´ÐµÐ¸ производÑтвенного цикла."
+msgstr "ПроизводÑтвенный Ñтап показывает общее Ð²Ñ€ÐµÐ¼Ñ Ð¼ÐµÐ¶Ð´Ñƒ Ñозданием обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¸ развертыванием кода в продуктивной Ñреде. Данные будут автоматичеÑки добавлены поÑле полного Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ð¸Ð´ÐµÐ¸."
msgid "The project can be accessed by any logged in user."
msgstr "ДоÑтуп к проекту возможен любым зарегиÑтрированным пользователем."
@@ -1469,7 +2036,13 @@ msgid "The staging stage shows the time between merging the MR and deploying cod
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 занимает Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑка каждого конвейера Ð´Ð»Ñ ÑоответÑтвующего запроÑа на ÑлиÑние. Данные будут автоматичеÑки добавлены поÑле Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ñ‹ вашего первого конвейера."
+msgstr "Этап теÑÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¿Ð¾ÐºÐ°Ð·Ñ‹Ð²Ð°ÐµÑ‚ времÑ, которое GitLab CI занимает Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑка каждой Ñборочной линии Ð´Ð»Ñ ÑоответÑтвующего запроÑа на ÑлиÑние. Данные будут автоматичеÑки добавлены поÑле Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ñ‹ вашей первой Ñборочной линии."
+
+msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð² Ñекундах, в течение которого GitLab будет хранить информацию о ÑбоÑÑ…. ЕÑли в течение Ñтого времени не было Ñбоев, Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ монтировании очищаетÑÑ."
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð² Ñекундах в течении которого GitLab будет пытатьÑÑ Ð¿Ð¾Ð»ÑƒÑ‡Ð¸Ñ‚ÑŒ доÑтуп к хранилищу. ПоÑле Ñтого времени будет зафикÑирована ошибка Ð¿Ñ€ÐµÐ²Ñ‹ÑˆÐµÐ½Ð¸Ñ Ð²Ñ€ÐµÐ¼ÐµÐ½Ð¸ ожиданиÑ."
msgid "The time taken by each data entry gathered by that stage."
msgstr "ВремÑ, затраченное каждым Ñлементом, Ñобранным на Ñтом Ñтапе."
@@ -1480,17 +2053,32 @@ msgstr "Среднее значение в Ñ€Ñду. Пример: между 3,
msgid "There are problems accessing Git storage: "
msgstr "Проблемы Ñ Ð´Ð¾Ñтупом к Git хранилищу: "
-msgid "This is the author's first Merge Request to this project. Handle with care."
-msgstr ""
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr "Эта ветка была изменена, пока вы её редактировали. Ð’Ñ‹ хотите Ñоздать новую ветку?"
+
+msgid "This is a confidential issue."
+msgstr "Это конфиденциальное обÑуждение."
+
+msgid "This is the author's first Merge Request to this project."
+msgstr "Это первый Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние от автора в Ñтот проект."
+
+msgid "This issue is confidential and locked."
+msgstr "Это обÑуждение конфиденциально и заблокировано."
+
+msgid "This issue is locked."
+msgstr "ОбÑуждение заблокировано."
msgid "This means you can not push code until you create an empty repository or import existing one."
-msgstr "Это означает, что вы не можете пушить код, пока не Ñоздадите пуÑтой репозиторий или не импортируете ÑущеÑтвующий."
+msgstr "Это означает, что вы не можете отправить код, пока не Ñоздадите пуÑтой репозиторий или не импортируете ÑущеÑтвующий."
+
+msgid "This merge request is locked."
+msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние заблокирован."
msgid "Time before an issue gets scheduled"
-msgstr " Ð’Ñ€ÐµÐ¼Ñ Ð´Ð¾ начала Ð¿Ð¾Ð¿Ð°Ð´Ð°Ð½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ñ‹ в планировщик"
+msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð´Ð¾ начала Ð¿Ð¾Ð¿Ð°Ð´Ð°Ð½Ð¸Ñ Ð¾Ð±ÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð² планировщик"
msgid "Time before an issue starts implementation"
-msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð´Ð¾ начала работы над проблемой"
+msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð´Ð¾ начала работы над обÑуждением"
msgid "Time between merge request creation and merge/close"
msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð¼ÐµÐ¶Ð´Ñƒ Ñозданием запроÑа ÑлиÑÐ½Ð¸Ñ Ð¸ ÑлиÑнием / закрытием"
@@ -1499,10 +2087,10 @@ msgid "Time until first merge request"
msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð´Ð¾ первого запроÑа на ÑлиÑние"
msgid "Timeago|%s days ago"
-msgstr "%s день назад"
+msgstr "%s дней назад"
msgid "Timeago|%s days remaining"
-msgstr "ОÑталоÑÑŒ %s день"
+msgstr "ОÑталоÑÑŒ %s дней"
msgid "Timeago|%s hours remaining"
msgstr "ОÑталоÑÑŒ %s чаÑов"
@@ -1514,10 +2102,10 @@ msgid "Timeago|%s minutes remaining"
msgstr "ОÑталоÑÑŒ %s минут"
msgid "Timeago|%s months ago"
-msgstr "%s минут назад"
+msgstr "%s меÑÑцев назад"
msgid "Timeago|%s months remaining"
-msgstr "ОÑталоÑÑŒ %s меÑÑц"
+msgstr "ОÑталоÑÑŒ %s меÑÑцев"
msgid "Timeago|%s seconds remaining"
msgstr "ОÑталоÑÑŒ %s Ñекунд(Ñ‹)"
@@ -1526,13 +2114,13 @@ msgid "Timeago|%s weeks ago"
msgstr "%s недели назад"
msgid "Timeago|%s weeks remaining"
-msgstr "ОÑталоÑÑŒ %s недели"
+msgstr "ОÑталоÑÑŒ %s недель"
msgid "Timeago|%s years ago"
-msgstr "%s год назад"
+msgstr "%s лет назад"
msgid "Timeago|%s years remaining"
-msgstr "ОÑталоÑÑŒ %s год"
+msgstr "ОÑталоÑÑŒ %s лет"
msgid "Timeago|1 day remaining"
msgstr "ОÑталÑÑ Ð´ÐµÐ½ÑŒ"
@@ -1564,9 +2152,6 @@ msgstr "меÑÑц назад"
msgid "Timeago|a week ago"
msgstr "неделю назад"
-msgid "Timeago|a while"
-msgstr "какое-то времÑ"
-
msgid "Timeago|a year ago"
msgstr "год назад"
@@ -1580,16 +2165,16 @@ msgid "Timeago|about an hour ago"
msgstr "около чаÑа назад"
msgid "Timeago|in %s days"
-msgstr "через %s день"
+msgstr "Через %s дней"
msgid "Timeago|in %s hours"
-msgstr "через %s чаÑ"
+msgstr "Через %s чаÑов"
msgid "Timeago|in %s minutes"
msgstr "через %s минут"
msgid "Timeago|in %s months"
-msgstr "через %s меÑÑц"
+msgstr "Через %s меÑÑцев"
msgid "Timeago|in %s seconds"
msgstr "через %s Ñекунд(Ñ‹)"
@@ -1598,7 +2183,7 @@ msgid "Timeago|in %s weeks"
msgstr "через %s недели"
msgid "Timeago|in %s years"
-msgstr "через %s год"
+msgstr "через %s лет"
msgid "Timeago|in 1 day"
msgstr "через день"
@@ -1618,6 +2203,9 @@ msgstr "через неделю"
msgid "Timeago|in 1 year"
msgstr "через год"
+msgid "Timeago|in a while"
+msgstr "через некоторое времÑ"
+
msgid "Timeago|less than a minute ago"
msgstr "менее чем минуту назад"
@@ -1642,9 +2230,36 @@ msgstr "Общее времÑ"
msgid "Total test time for all commits/merges"
msgstr "Общее Ð²Ñ€ÐµÐ¼Ñ Ñ‚ÐµÑÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ„Ð¸ÐºÑаций/ÑлиÑний"
+msgid "Track activity with Contribution Analytics."
+msgstr "ОтÑлеживать активноÑÑ‚ÑŒ Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Ðналитики УчаÑтников."
+
+msgid "Unlock"
+msgstr "Разблокировать"
+
+msgid "Unlocked"
+msgstr "Разблокировано"
+
msgid "Unstar"
msgstr "СнÑÑ‚ÑŒ отметку"
+msgid "Unsubscribe"
+msgstr "ОтпиÑатьÑÑ"
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr "ПовыÑьте ваш тарифный план, чтобы активировать Улучшенный Глобальный ПоиÑк."
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr "ПовыÑьте ваш тарифный план, чтобы активировать Ðналитики УчаÑтников."
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr "ПовыÑьте ваш тарифный план, чтобы активировать Групповые Веб-Обработчики."
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr "Обновите ваш тарифный план Ð´Ð»Ñ Ð¿Ð¾ÑÐ²Ð»ÐµÐ½Ð¸Ñ Ð²ÐµÑа у обÑуждений."
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr "Обновите ваш тарифный план, чтобы улучшить доÑки обÑуждений."
+
msgid "Upload New File"
msgstr "Загрузить новый файл"
@@ -1661,13 +2276,13 @@ msgid "Use your global notification setting"
msgstr "ИÑпользуютÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ñ‹Ð¹ наÑтройки уведомлений"
msgid "View file @ "
-msgstr ""
+msgstr "ПроÑмотр файла @ "
msgid "View open merge request"
msgstr "ПроÑмотреть открытый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "View replaced file @ "
-msgstr ""
+msgstr "ПроÑмотр заменённого файла @ "
msgid "VisibilityLevel|Internal"
msgstr "Ограниченный"
@@ -1687,9 +2302,120 @@ msgstr "Хотите увидеть данные? ОбратитеÑÑŒ к адм
msgid "We don't have enough data to show this stage."
msgstr "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¿Ð¾ Ñтапу отÑутÑтвует."
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr "Веб-обработчики позволÑÑŽÑ‚ вам вызывать Ð°Ð´Ñ€ÐµÑ URL еÑли, например, отправлен новый код или Ñоздано новое обÑуждение. Ð’Ñ‹ можете наÑтроить веб-обработчики так, чтобы они реагировали на определённые ÑобытиÑ, такие как отправки кода, обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ запроÑÑ‹ на ÑлиÑние. Групповые веб-обработчики применÑÑŽÑ‚ÑÑ ÐºÐ¾ вÑем проектам в группе и позволÑÑŽÑ‚ вам Ñтандартизовать функциональноÑÑ‚ÑŒ веб-обработчиков Ð´Ð»Ñ Ð²Ñей вашей группы."
+
+msgid "Weight"
+msgstr "ВеÑ"
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr "Когда доÑтуп к хранилищу получить не удалоÑÑŒ, GitLab приоÑтановит доÑтуп к хранилищу на времÑ, указанное здеÑÑŒ. Это позволит файловой ÑиÑтеме воÑÑтановитьÑÑ. Репозитории на Ñбойных \"шардах\" будут временно недоÑтупны"
+
msgid "Wiki"
msgstr "Wiki"
+msgid "WikiClone|Clone your wiki"
+msgstr "Клонировать wiki"
+
+msgid "WikiClone|Git Access"
+msgstr "ДоÑтуп через Git"
+
+msgid "WikiClone|Install Gollum"
+msgstr "УÑтановка Gollum"
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr "РекомендуетÑÑ ÑƒÑтановить %{markdown}, чтобы возможноÑти GFM отображалиÑÑŒ локально:"
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr "ЗапуÑтите Gollum и редактируете локально"
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr "Ð’Ñ‹ не можете Ñоздавать вики-Ñтраницы"
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr "Это уÑÑ‚Ð°Ñ€ÐµÐ²ÑˆÐ°Ñ Ð²ÐµÑ€ÑÐ¸Ñ Ñтой Ñтраницы."
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr "Ð’Ñ‹ можете увидеть %{most_recent_link} либо проÑмотреть %{history_link}."
+
+msgid "WikiHistoricalPage|history"
+msgstr "иÑториÑ"
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr "поÑледнÑÑ Ð²ÐµÑ€ÑиÑ"
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr "Дополнительные примеры находÑÑ‚ÑÑ Ð² %{docs_link}"
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr "документациÑ"
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr "Ð”Ð»Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÑÑылки на Ñтраницу (в том чиÑле на новую), проÑто введите %{link_example}"
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr "как наÑтроить"
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr "Совет: Ð’Ñ‹ можете указать полный путь Ð´Ð»Ñ Ð½Ð¾Ð²Ð¾Ð³Ð¾ файла. Будут автоматичеÑки Ñозданы любые отÑутÑтвующие каталоги."
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr "ÐÐ¾Ð²Ð°Ñ Ð’Ð¸ÐºÐ¸ Страница"
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr "Ð’Ñ‹ уверены что хотите удалить Ñту Ñтраницу?"
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr "Кто-то редактирует Ñтраницу одновременно Ñ Ð²Ð°Ð¼Ð¸. ПожалуйÑта проверьте %{page_link} и убедитеÑÑŒ, что внеÑенные Вами Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð½Ðµ затрут чужие."
+
+msgid "WikiPageConflictMessage|the page"
+msgstr "Ñтраница"
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr "Создать %{page_title}"
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr "Обновить %{page_title}"
+
+msgid "WikiPage|Page slug"
+msgstr "СемантичеÑкий Url Ð´Ð»Ñ Ñтраницы"
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr "Ðапишите ваше Ñодержимое или перетащите Ñюда файлы..."
+
+msgid "Wiki|Create Page"
+msgstr "Создать Страницу"
+
+msgid "Wiki|Create page"
+msgstr "Создать Ñтраницу"
+
+msgid "Wiki|Edit Page"
+msgstr "Редактировать Ñтраницу"
+
+msgid "Wiki|Empty page"
+msgstr "ПуÑÑ‚Ð°Ñ Ñтраница"
+
+msgid "Wiki|More Pages"
+msgstr "Ещё Ñтраницы"
+
+msgid "Wiki|New page"
+msgstr "ÐÐ¾Ð²Ð°Ñ Ñтраница"
+
+msgid "Wiki|Page history"
+msgstr "ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ñтраницы"
+
+msgid "Wiki|Page version"
+msgstr "ВерÑÐ¸Ñ Ñтраницы"
+
+msgid "Wiki|Pages"
+msgstr "Страницы"
+
+msgid "Wiki|Wiki Pages"
+msgstr "Вики Страницы"
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr "С аналитикой учаÑтников вы можете изучать активноÑÑ‚ÑŒ в обÑуждениÑÑ…, запроÑах на ÑлиÑние и Ñобытий отправки кода Ð´Ð»Ñ Ð²Ð°ÑˆÐµÐ¹ организации и её учаÑтников."
+
msgid "Withdraw Access Request"
msgstr "Отменить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð´Ð¾Ñтупа"
@@ -1700,14 +2426,26 @@ msgid "You are going to remove %{project_name_with_namespace}. Removed project C
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}. Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
+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 on a read-only GitLab instance."
+msgstr "Ð’Ñ‹ находитеÑÑŒ на ÑкземплÑре \"только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ\" клаÑтера GitLab."
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr "Ð’Ñ‹ находитеÑÑŒ на ÑкземплÑре \"только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ\" клаÑтера GitLab. ЕÑли вы хотите произвеÑти любые изменениÑ, вы должны перейти на \"оÑновной\" ÑкземплÑÑ€ по ÑÑылке %{link_to_primary_node}."
+
msgid "You can only add files when you are on a branch"
msgstr "Ð’Ñ‹ можете добавлÑÑ‚ÑŒ только файлы, когда находитеÑÑŒ в ветке"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr "Ð’Ñ‹ не можете запиÑывать на подчиненные ÑкземплÑры \"только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ\" клаÑтера GitLab Geo. ИÑпользуйте вмеÑто Ñтого %{link_to_primary_node}."
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr "Ð’Ñ‹ не можете запиÑывать на Ñтот ÑкземплÑÑ€ \"только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ\" клаÑтера GitLab."
+
msgid "You have reached your project limit"
msgstr "Ð’Ñ‹ доÑтигли Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ Ð² вашем проекте"
@@ -1733,25 +2471,31 @@ msgid "You will receive notifications only for comments in which you were @menti
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} в ваш аккаунт"
+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 "Your comment will not be visible to the public."
+msgstr "Ваш комментарий не будет виден вÑем."
+
+msgid "Your groups"
+msgstr "Ваши группы"
+
msgid "Your name"
msgstr "Ваше имÑ"
msgid "Your projects"
-msgstr ""
+msgstr "Ваши проекты"
msgid "commit"
-msgstr ""
+msgstr "коммит"
msgid "day"
msgid_plural "days"
msgstr[0] "день"
-msgstr[1] "дни"
-msgstr[2] "дни"
+msgstr[1] "дней"
+msgstr[2] "дней"
msgid "new merge request"
msgstr "новый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
@@ -1765,3 +2509,15 @@ msgstr[0] "иÑточник"
msgstr[1] "иÑточники"
msgstr[2] "иÑточники"
+msgid "password"
+msgstr "пароль"
+
+msgid "personal access token"
+msgstr "токен Ð´Ð»Ñ Ð¿ÐµÑ€Ñонального доÑтупа"
+
+msgid "to help your contributors communicate effectively!"
+msgstr "чтобы помочь вашим учаÑтникам взаимодейÑтвовать Ñффективнее!"
+
+msgid "username"
+msgstr "Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"
+
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index ffbbe88cc51..cf51f8ec689 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:43-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-05 08:38-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -22,6 +22,12 @@ msgstr[0] "%d комміт"
msgstr[1] "%d комміта"
msgstr[2] "%d коммітів"
+msgid "%d layer"
+msgid_plural "%d layers"
+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 доданий Комміт був виключений Ð´Ð»Ñ Ð·Ð°Ð¿Ð¾Ð±Ñ–Ð³Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ з продуктивніÑÑ‚ÑŽ."
@@ -31,8 +37,14 @@ msgstr[2] "%s доданих коммітів були виключені длÑ
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} комміт %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
-msgstr ""
+msgstr "на %{number_commits_behind} коммітів позаду %{default_branch}, на %{number_commits_ahead} коммітів попереду"
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr "%{number_of_failures} від %{maximum_failures} невдач. GitLab надаÑÑ‚ÑŒ доÑтуп на наÑтупну Ñпробу."
@@ -52,6 +64,12 @@ msgstr[2] "%{storage_name}: %{failed_attempts} невдалих Ñпроб доÑ
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(перейдіть за поÑиланнÑм %{link} Ð´Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ— ÑтоÑовно вÑтановленнÑ)."
+msgid "+ %{moreCount} more"
+msgstr "+ ще %{moreCount}"
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 конвеєр"
@@ -59,7 +77,10 @@ msgstr[1] "%d конвеєра"
msgstr[2] "%d конвеєрів"
msgid "1st contribution!"
-msgstr ""
+msgstr "Перший внеÑок!"
+
+msgid "2FA enabled"
+msgstr "Двоетапна Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ ÑƒÐ²Ñ–Ð¼ÐºÐ½ÐµÐ½Ð°"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Це набір графічних елементів Ð´Ð»Ñ Ð±ÐµÐ·Ð¿ÐµÑ€ÐµÑ€Ð²Ð½Ð¾Ñ— інтеграції"
@@ -85,12 +106,18 @@ msgstr "Ðктивний"
msgid "Activity"
msgstr "ÐктивніÑÑ‚ÑŒ"
+msgid "Add"
+msgstr "Додати"
+
msgid "Add Changelog"
msgstr "Додати ÑпиÑок змін (Changelog)"
msgid "Add Contribution guide"
msgstr "Додати керівництво Ð´Ð»Ñ ÐºÐ¾Ð½Ñ‚Ñ€Ð¸Ð±â€™ÑŽÑ‚Ð¾Ñ€Ñ–Ð²"
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr "Додайте групу Webhooks та GitLab Enterprise Edition."
+
msgid "Add License"
msgstr "Додати ліцензію"
@@ -100,11 +127,20 @@ msgstr "Додати SSH ключа в Ñвій профіль, щоб мати
msgid "Add new directory"
msgstr "Додати новий каталог"
+msgid "AdminHealthPageLink|health page"
+msgstr "Ñторінка ÑтатуÑу"
+
+msgid "Advanced settings"
+msgstr "Додаткові параметри"
+
msgid "All"
msgstr "Ð’ÑÑ–"
+msgid "An error occurred. Please try again."
+msgstr "СталаÑÑŒ помилка. Спробуйте ще раз."
+
msgid "Appearance"
-msgstr ""
+msgstr "Зовнішній виглÑд"
msgid "Applications"
msgstr "Додатки"
@@ -118,6 +154,9 @@ msgstr "Ви впевнені, що хочете видалити цей розÐ
msgid "Are you sure you want to discard your changes?"
msgstr "Ви впевнені, що бажаєте ÑкаÑувати ваші зміни?"
+msgid "Are you sure you want to leave this group?"
+msgstr "Ви впевнені що хочете залишити цю групу?"
+
msgid "Are you sure you want to reset registration token?"
msgstr "Ви впевнені, що бажаєте Ñкинути реєÑтраційний токен?"
@@ -128,91 +167,97 @@ msgid "Are you sure?"
msgstr "Ви впевнені?"
msgid "Artifacts"
-msgstr ""
+msgstr "Ðртефакти"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Прикріпити файл за допомогою перетÑÐ³ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð±Ð¾ %{upload_link}"
msgid "Authentication Log"
-msgstr ""
+msgstr "Журнал автентифікації"
-msgid "Auto DevOps (Beta)"
-msgstr ""
-
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
-msgid "Auto DevOps documentation"
-msgstr ""
+msgid "Author"
+msgstr "Ðвтор"
msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Ð”Ð»Ñ ÐºÐ¾Ñ€ÐµÐºÑ‚Ð½Ð¾Ñ— роботи Auto Review Apps та Auto Deploy необхідно вказати доменне Ñ–Ð¼â€™Ñ Ñ‚Ð° %{kubernetes}."
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
-msgstr ""
+msgstr "Ð”Ð»Ñ ÐºÐ¾Ñ€ÐµÐºÑ‚Ð½Ð¾Ñ— роботи Auto Review Apps та Auto Deploy необхідно вказати доменне ім’Ñ."
msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "Ð”Ð»Ñ ÐºÐ¾Ñ€ÐµÐºÑ‚Ð½Ð¾Ñ— роботи Auto Review Apps та Auto Deploy необхіден %{kubernetes}."
-msgid "AutoDevOps|Learn more in the"
-msgstr ""
+msgid "AutoDevOps|Auto DevOps (Beta)"
+msgstr "Auto DevOps (бета)"
+
+msgid "AutoDevOps|Auto DevOps documentation"
+msgstr "Auto DevOps документаціÑ"
+
+msgid "AutoDevOps|Enable in settings"
+msgstr "Включити в налаштуваннÑÑ…"
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr "AutoDevOps буде автоматично збирати, теÑтувати та розгортати вашу програму на оÑнові визначеної CI/CD конфігурації."
+
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
+msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ в %{link_to_documentation}"
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr "Ви можете активувати %{link_to_settings} Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту."
msgid "Billing"
-msgstr ""
+msgstr "Білінг"
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr ""
+msgstr "%{group_name} зараз має план %{plan_link}."
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr ""
+msgstr "Ðвтоматичні Ð¿Ð¾Ð½Ð¸Ð¶ÐµÐ½Ð½Ñ Ñ‚Ð° Ð¿Ñ–Ð´Ð²Ð¸Ñ‰ÐµÐ½Ð½Ñ Ð´Ð¾ деÑких планів зараз не доÑтупні."
msgid "BillingPlans|Current plan"
-msgstr ""
+msgstr "Поточний план"
msgid "BillingPlans|Customer Support"
-msgstr ""
+msgstr "Служба підтримки"
+
+msgid "BillingPlans|Downgrade"
+msgstr "Понизити"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr ""
+msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про кожен план, читаючи наш %{faq_link}."
msgid "BillingPlans|Manage plan"
-msgstr ""
+msgstr "Ð£Ð¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ð¿Ð»Ð°Ð½Ð¾Ð¼"
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr ""
+msgstr "Будь лаÑка, в цьому випадку зв'ÑжітьÑÑ Ð· %{customer_support_link}."
msgid "BillingPlans|See all %{plan_name} features"
-msgstr ""
+msgstr "ПодивітьÑÑ Ð²ÑÑ– можливоÑÑ‚Ñ– %{plan_name}"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr ""
+msgstr "Ð¦Ñ Ð³Ñ€ÑƒÐ¿Ð° викориÑтовує план, пов'Ñзаний з батьківÑькою групою."
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr ""
+msgstr "Ð”Ð»Ñ ÑƒÐ¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ð¿Ð»Ð°Ð½Ð¾Ð¼ цієї групи відвідайте Ñекцію оплати %{parent_billing_page_link}."
msgid "BillingPlans|Upgrade"
-msgstr ""
+msgstr "Підвищити"
msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr ""
+msgstr "Зараз ви викориÑтовуєте план %{plan_link}."
msgid "BillingPlans|frequently asked questions"
-msgstr ""
+msgstr "ЧаÑÑ‚Ñ– питаннÑ"
msgid "BillingPlans|monthly"
-msgstr ""
+msgstr "щоміÑÑцÑ"
msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr ""
+msgstr "ОплачуєтьÑÑ Ñ‰Ð¾Ñ€Ñ–Ñ‡Ð½Ð¾ %{price_per_year}"
msgid "BillingPlans|per user"
-msgstr ""
-
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
+msgstr "За кориÑтувача"
msgid "Branch"
msgid_plural "Branches"
@@ -223,6 +268,9 @@ 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 has changed"
+msgstr "Гілка змінилаÑÑŒ"
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Пошук гілок"
@@ -233,88 +281,88 @@ msgid "Branches"
msgstr "Гілки"
msgid "Branches|Cant find HEAD commit for this branch"
-msgstr ""
+msgstr "Ðе можу знайти HEAD-комміт Ð´Ð»Ñ Ñ†Ñ–Ñ”Ñ— гілки"
msgid "Branches|Compare"
-msgstr ""
+msgstr "ПорівнÑти"
msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
-msgstr ""
+msgstr "Видалити вÑÑ– гілки Ñкі злиті в '%{default_branch}'"
msgid "Branches|Delete branch"
-msgstr ""
+msgstr "Видалити гілку"
msgid "Branches|Delete merged branches"
-msgstr ""
+msgstr "Видалити злиті гілки"
msgid "Branches|Delete protected branch"
-msgstr ""
+msgstr "Видалити захищену гілку"
msgid "Branches|Delete protected branch '%{branch_name}'?"
-msgstr ""
+msgstr "Видалити захищену гілку \"%{branch_name}\"?"
msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
-msgstr ""
+msgstr "Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð³Ñ–Ð»ÐºÐ¸ '%{branch_name}' неможливо буде ÑкаÑувати. Ви впевнені?"
msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
-msgstr ""
+msgstr "Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð·Ð»Ð¸Ñ‚Ð¸Ñ… гілок неможливо буде ÑкаÑувати. Ви впевнені?"
msgid "Branches|Filter by branch name"
-msgstr ""
+msgstr "Фільтрувати за назвою гілки"
msgid "Branches|Merged into %{default_branch}"
-msgstr ""
+msgstr "Злито в %{default_branch}"
msgid "Branches|New branch"
-msgstr ""
+msgstr "Ðова гілка"
msgid "Branches|No branches to show"
-msgstr ""
+msgstr "Ðемає гілок Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
-msgstr ""
+msgstr "Як тільки ви підтвердите Ñ– натиÑнете %{delete_protected_branch}, дані будуть втрачені, Ñ– Ñ—Ñ… не можливо буде відновити."
msgid "Branches|Only a project master or owner can delete a protected branch"
-msgstr ""
+msgstr "Тільки керівник або влаÑник проекту може видалити захищену гілку"
msgid "Branches|Protected branches can be managed in %{project_settings_link}"
-msgstr ""
+msgstr "УправлÑти захищеними гілками можливо в %{project_settings_link}"
msgid "Branches|Sort by"
-msgstr ""
+msgstr "Сортувати за"
msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
+msgstr "Гілка не може бути оновлена автоматично, тому що вона має розбіжноÑÑ‚Ñ– із upstream."
msgid "Branches|The default branch cannot be deleted"
-msgstr ""
+msgstr "Гілка \"за замовчуваннÑм\" не може бути видалена"
msgid "Branches|This branch hasn’t been merged into %{default_branch}."
-msgstr ""
+msgstr "Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° не була злита в %{default_branch}."
msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
-msgstr ""
+msgstr "Щоб уникнути втрати даних, розглÑньте можливіÑÑ‚ÑŒ Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ†Ñ–Ñ”Ñ— гілки перед Ñ—Ñ— видаленнÑм."
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
-msgstr ""
+msgstr "Ð”Ð»Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ, введіть %{branch_name_confirmation}:"
msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
+msgstr "Щоб відхилити локальні зміни Ñ– перезапиÑати гілку верÑією з upstream, видаліть Ñ—Ñ— тут Ñ– виберіть \"Оновити зараз\" вище."
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
-msgstr ""
+msgstr "Ви збираєтеÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ захищену гілку %{branch_name}."
msgid "Branches|diverged from upstream"
-msgstr ""
+msgstr "розходитьÑÑ Ð· upstream"
msgid "Branches|merged"
-msgstr ""
+msgstr "злита"
msgid "Branches|project settings"
-msgstr ""
+msgstr "ÐаÑтройки проекту"
msgid "Branches|protected"
-msgstr ""
+msgstr "захищені"
msgid "Browse Directory"
msgstr "ПереглÑнути каталог"
@@ -337,12 +385,18 @@ msgstr "CI / CD"
msgid "CI configuration"
msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI"
+msgid "CICD|Jobs"
+msgstr "ЗавданнÑ"
+
msgid "Cancel"
msgstr "СкаÑувати"
msgid "Cancel edit"
msgstr "Відмінити правку"
+msgid "Change Weight"
+msgstr "Вага зміни"
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Вибрати в гілці"
@@ -370,6 +424,9 @@ msgstr "Cherry-pick в цьому комміті"
msgid "Cherry-pick this merge request"
msgstr "Cherry-pick в цьому запиті на злиттÑ"
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr "Виберіть, Ñкі групи ви хочете реплікувати на цю вторинну ноду. Залиште порожнім, щоб реплікувати вÑе."
+
msgid "CiStatusLabel|canceled"
msgstr "ÑкаÑовано"
@@ -424,6 +481,147 @@ msgstr "пропущено"
msgid "CiStatus|running"
msgstr "виконуєтьÑÑ"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr "circuitbreaker api"
+
+msgid "Clone repository"
+msgstr "Клонувати репозиторій"
+
+msgid "Close"
+msgstr "Закрити"
+
+msgid "Cluster"
+msgstr "КлаÑтер"
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr "%{link_to_container_project} напевно було Ñтворено під цим обліковим запиÑом"
+
+msgid "ClusterIntegration|Cluster details"
+msgstr "Параметри клаÑтера"
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· клаÑтером"
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· клаÑтером вимкнена Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту."
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· клаÑтером увімкнена Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту."
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr "Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту увімкнена Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· клаÑтером. Ð’Ð¸ÐºÐ½ÐµÐ½Ð½Ñ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ— не вплине на клаÑтер, але з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ GitLab з ним буде тимчаÑово розірване."
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr "СтворюєтьÑÑ ÐºÐ»Ð°Ñтер в Google Container Engine..."
+
+msgid "ClusterIntegration|Cluster name"
+msgstr "Ім'Ñ ÐºÐ»Ð°Ñтера"
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr "КлаÑтер був уÑпішно Ñтворений в Google Container Engine"
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr "Копіювати назву клаÑтера"
+
+msgid "ClusterIntegration|Create cluster"
+msgstr "Створити клаÑтер"
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr "Створити новий клаÑтер в Google Container Engine"
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr "Увімкнути інтеграцію із клаÑтерами"
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr "Ідентифікатор проекту в Google Cloud Platform"
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr "Google Container Engine"
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr "Проект Google Container Engine"
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про %{link_to_documentation}"
+
+msgid "ClusterIntegration|Machine type"
+msgstr "Тип машини"
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr "ПереконайтеÑÑ, що ваш обліковий Ð·Ð°Ð¿Ð¸Ñ %{link_to_requirements} Ð´Ð»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐºÐ»Ð°Ñтерів"
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr "Ð£Ð¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ”ÑŽ із клаÑтером у вашому Gitlab-проекті."
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr "Ð”Ð»Ñ ÐºÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ñвоїм клаÑтером перейдіть на %{link_gke}"
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr "КількіÑÑ‚ÑŒ вузлів"
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr "Будь-лаÑка впевнітьÑÑ, що ваш Google-аккаунт задовольнÑÑ” наÑтупним вимогам:"
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr "Namespace проекту (не обов’Ñзковий, унікальний)"
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr "Прочитайте нашу документацію %{link_to_help_page} по інтеграції із клаÑтером."
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr "Видалити інтеграцію з клаÑтером"
+
+msgid "ClusterIntegration|Remove integration"
+msgstr "Видалити інтеграцію"
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr "При видаленні інтеграції з клаÑтером буде видалена ÐºÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ ÐºÐ»Ð°Ñтера, Ñку ви додали в цей проект. Дана Ð´Ñ–Ñ Ð½Ðµ видалить Ñам проект."
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr "ПереглÑнути та редагувати параметри вашого клаÑтера"
+
+msgid "ClusterIntegration|See machine types"
+msgstr "ПереглÑнути типи машин"
+
+msgid "ClusterIntegration|See your projects"
+msgstr "ПереглÑнути ваші проекти"
+
+msgid "ClusterIntegration|See zones"
+msgstr "ПереглÑнути зони"
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr "ЩоÑÑŒ пішло не так з нашого боку."
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr "ЩоÑÑŒ пішло не так під Ñ‡Ð°Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐºÐ»Ð°Ñтера в Google Container Engine"
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr "Переключити КлаÑтер"
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr "За допомогою підключеного до цього проекту клаÑтера, ви можете викориÑтовувати Review Apps, розгортати ваші проекти, запуÑкати конвеєри збірки та багато іншого."
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr "Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð¿Ð¾Ð²Ð¸Ð½ÐµÐ½ мати %{link_to_container_engine}"
+
+msgid "ClusterIntegration|Zone"
+msgstr "Зона"
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr "доÑтуп до Google Container Engine"
+
+msgid "ClusterIntegration|cluster"
+msgstr "клаÑтер"
+
+msgid "ClusterIntegration|help page"
+msgstr "Ñторінка допомоги"
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr "задовольнÑÑ” вимогам"
+
+msgid "ClusterIntegration|properly configured"
+msgstr "правильно налаштований"
+
msgid "Comments"
msgstr "Коментарі"
@@ -433,6 +631,15 @@ msgstr[0] "Комміт"
msgstr[1] "Комміта"
msgstr[2] "Коммітів"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+msgid "Commit Message"
+msgstr "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ ÐºÐ¾Ð¼Ð¼Ñ–Ñ‚Ñƒ"
+
msgid "Commit duration in minutes for last 30 commits"
msgstr "ТриваліÑÑ‚ÑŒ оÑтанніх 30 коммітів у хвилинах"
@@ -460,14 +667,65 @@ msgstr "Комміт від"
msgid "Compare"
msgstr "ПорівнÑти"
+msgid "Container Registry"
+msgstr "РеєÑÑ‚Ñ€ Контейнерів"
+
+msgid "ContainerRegistry|Created"
+msgstr "Створений"
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr "Спочатку увійдіть до реєÑтру контейнерів GitLab, викориÑтовуючи логін та пароль. Якщо у Ð²Ð°Ñ %{link_2fa}, треба викориÑтовувати %{link_token}:"
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr "GitLab підтримує до 3 рівнів імен образів. ÐаÑтупні приклади образів Ñ” правильними Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ проекту:"
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr "Як викориÑтовувати РеєÑÑ‚Ñ€ Контейнерів"
+
+msgid "ContainerRegistry|Learn more about"
+msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про"
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr "Ð’ РеєÑтрі Контейнерів немає тегів Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ образу."
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr "ПіÑÐ»Ñ Ð²Ñ…Ð¾Ð´Ñƒ ви можете Ñтворювати та завантажувати образи контейнерів, викориÑтовуючи звичайні %{build} та %{push} команди"
+
+msgid "ContainerRegistry|Remove repository"
+msgstr "Видалити репозиторій"
+
+msgid "ContainerRegistry|Remove tag"
+msgstr "Видалити тег"
+
+msgid "ContainerRegistry|Size"
+msgstr "Розмір"
+
+msgid "ContainerRegistry|Tag"
+msgstr "Тег"
+
+msgid "ContainerRegistry|Tag ID"
+msgstr "Тег ID"
+
+msgid "ContainerRegistry|Use different image names"
+msgstr "ВикориÑтовуйте різні імена образів"
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr "За допомогою вбудованого в GitLab реєÑтру Docker контейнерів кожен проект може мати влаÑне міÑце Ð´Ð»Ñ Ð·Ð±ÐµÑ€Ñ–Ð³Ð°Ð½Ð½Ñ Docker образів."
+
msgid "Contribution guide"
msgstr "Керівництво контриб’юторів"
msgid "Contributors"
msgstr "Контриб’ютори"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr "Задати макÑимальну кількіÑÑ‚ÑŒ потоків Ð´Ð»Ñ Ñ„Ð¾Ð½Ð¾Ð²Ð¾Ð³Ð¾ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ LFS/вкладень Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ вторинного вузла"
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr "Задати макÑимальну кількіÑÑ‚ÑŒ потоків Ð´Ð»Ñ Ñ„Ð¾Ð½Ð¾Ð²Ð¾Ð³Ð¾ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ—Ð² Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ вторинного вузла"
+
msgid "Copy SSH public key to clipboard"
-msgstr ""
+msgstr "Скопіюйте відкритий SSH-ключ в буфер обміну"
msgid "Copy URL to clipboard"
msgstr "Скопіювати URL в буфер обміну"
@@ -478,9 +736,6 @@ msgstr "Скопіювати ідентифікатор в буфер обмін
msgid "Create New Directory"
msgstr "Створити новий каталог"
-msgid "Create a new branch"
-msgstr "Створити нову гілку"
-
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "Створити токен доÑтупу Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ аккауета, щоб відправлÑти або отримувати через %{protocol}."
@@ -490,9 +745,21 @@ msgstr "Створити каталог"
msgid "Create empty bare repository"
msgstr "Створити порожній репозиторій"
+msgid "Create file"
+msgstr "Створити файл"
+
msgid "Create merge request"
msgstr "Створити запит на злиттÑ"
+msgid "Create new branch"
+msgstr "Створити нову гілку"
+
+msgid "Create new directory"
+msgstr "Створити новий каталог"
+
+msgid "Create new file"
+msgstr "Створити новий файл"
+
msgid "Create new..."
msgstr "Створити..."
@@ -544,6 +811,12 @@ msgstr "ДЕВ"
msgid "CycleAnalyticsStage|Test"
msgstr "ТеÑтуваннÑ"
+msgid "DashboardProjects|All"
+msgstr "Ð’ÑÑ–"
+
+msgid "DashboardProjects|Personal"
+msgstr "ОÑобиÑÑ‚Ñ–"
+
msgid "Define a custom pattern with cron syntax"
msgstr "Визначте влаÑний шаблон за допомогою ÑинтакÑиÑу cron"
@@ -562,6 +835,9 @@ msgstr "Ключи Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ"
msgid "Description"
msgstr "ОпиÑ"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr "Шаблони опиÑу дозволÑÑŽÑ‚ÑŒ визначити конкретні шаблони обговорень та запитів на Ð·Ð»Ð¸Ð²Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ проекту."
+
msgid "Details"
msgstr "Деталі"
@@ -571,6 +847,12 @@ msgstr "Ім'Ñ ÐºÐ°Ñ‚Ð°Ð»Ð¾Ð³Ñƒ"
msgid "Discard changes"
msgstr "СкаÑувати зміни"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr "Відмінити блок вÑтупу до Ðналитики Циклу"
+
+msgid "Dismiss Merge Request promotion"
+msgstr "Ðе показувати промоушн запитів на злиттÑ"
+
msgid "Don't show again"
msgstr "Ðе показувати знову"
@@ -610,26 +892,23 @@ msgstr "Редагувати Розклад Конвеєра %{id}"
msgid "Emails"
msgstr "ÐдреÑи електронної пошти"
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
-msgstr "Ð’ÑÑ–"
+msgstr "Фільтрувати по вÑім"
msgid "EventFilterBy|Filter by comments"
-msgstr "Коментарю"
+msgstr "Фільтрувати по коментарÑм"
msgid "EventFilterBy|Filter by issue events"
-msgstr "Проблеми"
+msgstr "Фільтрувати по проблемах"
msgid "EventFilterBy|Filter by merge events"
-msgstr "Запити на злиттÑ"
+msgstr "Фільтрувати по запитам на злиттÑ"
msgid "EventFilterBy|Filter by push events"
-msgstr "По відправленні комміту"
+msgstr "Фільтрувати по push-подіÑÑ…"
msgid "EventFilterBy|Filter by team"
-msgstr "За командою"
+msgstr "Фільтрувати по команді"
msgid "Every day (at 4:00am)"
msgstr "Кожен день (в 4:00 ранку)"
@@ -641,7 +920,10 @@ msgid "Every week (Sundays at 4:00am)"
msgstr "Ð©Ð¾Ñ‚Ð¸Ð¶Ð½Ñ (в неділю о 4:00 ранку)"
msgid "Explore projects"
-msgstr ""
+msgstr "ОглÑд проектів"
+
+msgid "Explore public groups"
+msgstr "ПереглÑнути публічні групи"
msgid "Failed to change the owner"
msgstr "Ðе вдалоÑÑ Ð·Ð¼Ñ–Ð½Ð¸Ñ‚Ð¸ влаÑника"
@@ -649,6 +931,9 @@ msgstr "Ðе вдалоÑÑ Ð·Ð¼Ñ–Ð½Ð¸Ñ‚Ð¸ влаÑника"
msgid "Failed to remove the pipeline schedule"
msgstr "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ розклад Конвеєра"
+msgid "File name"
+msgstr "Ім'Ñ Ñ„Ð°Ð¹Ð»Ñƒ"
+
msgid "Files"
msgstr "Файли"
@@ -676,6 +961,12 @@ msgstr[2] "Форків"
msgid "ForkedFromProjectPath|Forked from"
msgstr "Форк від"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr "Форк із %{project_name} (видалено)"
+
+msgid "Format"
+msgstr "Формат"
+
msgid "From issue creation until deploy to production"
msgstr "З моменту ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ до Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð½Ð° ПРОД"
@@ -686,7 +977,19 @@ msgid "GPG Keys"
msgstr "GPG ключі"
msgid "Geo Nodes"
-msgstr ""
+msgstr "Гео-Вузли"
+
+msgid "Geo|File sync capacity"
+msgstr "ПропуÑкна здатніÑÑ‚ÑŒ Ñинхронізації файлів"
+
+msgid "Geo|Groups to replicate"
+msgstr "Групи Ð´Ð»Ñ Ñ€ÐµÐ¿Ð»Ñ–ÐºÐ°Ñ†Ñ–Ñ—"
+
+msgid "Geo|Repository sync capacity"
+msgstr "ПропуÑкна здатніÑÑ‚ÑŒ Ñинхронізації репозиторіїв"
+
+msgid "Geo|Select groups to replicate."
+msgstr "Виберіть групи Ð´Ð»Ñ Ñ€ÐµÐ¿Ð»Ñ–ÐºÐ°Ñ†Ñ–Ñ—."
msgid "Git storage health information has been reset"
msgstr "Ð†Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ ÑÑ‚Ð°Ñ‚ÑƒÑ Ð·Ð±ÐµÑ€Ñ–Ð³Ð°Ð½Ð½Ñ Git була Ñкинута"
@@ -700,29 +1003,77 @@ msgstr "Перейти до вашого форку"
msgid "GoToYourFork|Fork"
msgstr "Форк"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr "ÐÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ Google не %{link_to_documentation}. ПопроÑÑ–Ñ‚ÑŒ Ñвого адмініÑтратора GitLab, Ñкщо ви хочете ÑкориÑтатиÑÑ Ñ†Ð¸Ð¼ ÑервіÑом."
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
-msgstr ""
+msgstr "Заборонити Ñпільний доÑтуп до проекту в рамках %{group} з іншими групами"
msgid "GroupSettings|Share with group lock"
-msgstr ""
+msgstr "Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñпільного доÑтупу з іншими групами"
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
-msgstr ""
+msgstr "Цей параметр заÑтоÑовано до %{ancestor_group} Ñ– його було перевизначено в цій підгрупі."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Цей параметр заÑтоÑовано до %{ancestor_group}. Щоб поділитиÑÑ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð°Ð¼Ð¸ з цієї групи з іншою групою, попроÑÑ–Ñ‚ÑŒ влаÑника перевизначити параметр або %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "Цей параметр заÑтоÑовано до %{ancestor_group}. Ви можете перевизначити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð±Ð¾ %{remove_ancestor_share_with_group_lock}."
msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
-msgstr ""
+msgstr "Цей параметр буде заÑтоÑовано до вÑÑ–Ñ… підгруп, Ñкщо тільки не буде перевизначено влаÑником групи. Групи, Ñкі вже мають доÑтуп до проекту, будуть мати доÑтуп, Ñкщо вони не будуть вилучені вручну."
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
-msgstr ""
+msgstr "не може бути ÑкаÑовано поки \"Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñпільного доÑтупу з іншими групами\" активне на батьківÑькій групі, за винÑтком влаÑника батьківÑької групи"
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
-msgstr ""
+msgstr "Видалити Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñпільного доÑтупу з іншими групами з %{ancestor_group_name}"
+
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr "Група — набір із декількох проектів."
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr "Ð Ð¾Ð·Ð¼Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð² у групі Ñхоже на Ñ€Ð¾Ð·Ð¼Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð² папці."
+
+msgid "GroupsEmptyState|No groups found"
+msgstr "Групи не знайдені"
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr "Ви можете керувати правами доÑтупу членів групи мати доÑтуп до кожного проекту в ній."
+
+msgid "GroupsTreeRole|as"
+msgstr "Ñк"
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr "Ви впевнені, що хочете залишити групу \"${this.group.fullName}\"?"
+
+msgid "GroupsTree|Create a project in this group."
+msgstr "Створити проект у групі."
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr "Створити підгрупи у цій групі."
+
+msgid "GroupsTree|Edit group"
+msgstr "Редагувати групу"
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr "Ðе вдалоÑÑ Ð·Ð°Ð»Ð¸ÑˆÐ¸Ñ‚Ð¸ групу. Будь-лаÑка впевнітьÑÑ, що ви не єдитий влаÑник."
+
+msgid "GroupsTree|Filter by name..."
+msgstr "Фільтрувати за іменем…"
+
+msgid "GroupsTree|Leave this group"
+msgstr "Залишити цю группу"
+
+msgid "GroupsTree|Loading groups"
+msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð³Ñ€ÑƒÐ¿"
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr "Ðа жаль жодна группа не задовольнÑÑ” параметрам вашого запиту"
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr "Ðа жаль жодна группа чи проект не задовольнÑÑ” параметрам вашого запиту"
msgid "Health Check"
msgstr "Перевірки працездатноÑÑ‚Ñ–"
@@ -742,30 +1093,63 @@ msgstr "Жодних проблем із здоров'Ñм не виÑвлено
msgid "HealthCheck|Unhealthy"
msgstr "Ðездорові"
+msgid "History"
+msgstr "ІÑторіÑ"
+
msgid "Housekeeping successfully started"
msgstr "ÐžÑ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ ÑƒÑпішно розпочато"
msgid "Import repository"
msgstr "Імпорт репозеторіÑ"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr "Покращити дошки обговорень за допомогою верÑÑ–Ñ— GitLab Enterprise Edition."
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr "Покращити ÑƒÐ¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð°Ð¼Ð¸ з можливіÑÑ‚ÑŽ Ð²Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð²Ð°Ð³Ð¸ проблеми за допомогою GitLab Enterprise Edition."
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr "Покращити пошук за допомогою розширеного глобального пошук в верÑÑ–Ñ— GitLab Enterprise Edition."
+
msgid "Install a Runner compatible with GitLab CI"
msgstr "Ð’Ñтановіть Runner, ÑуміÑний з GitLab CI"
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] "ІнÑтанÑ"
+msgstr[1] "ІнÑтанÑа"
+msgstr[2] "ІнÑтанÑів"
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr "Ð’Ð½ÑƒÑ‚Ñ€Ñ–ÑˆÐ½Ñ â€” будь-Ñкий автентифікований кориÑтувач має доÑтуп до цієї групи та уÑÑ–Ñ… Ñ—Ñ— внутрішніх проектів."
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr "Внутрішній — будь-Ñкий автентифікований кориÑтувач має доÑтуп до цього проекту."
+
msgid "Interval Pattern"
msgstr "Шаблон інтервалу"
msgid "Introducing Cycle Analytics"
msgstr "ПредÑтавлÑємо аналітику циклу"
+msgid "Issue board focus mode"
+msgstr "Режим фокуÑÑƒÐ²Ð°Ð½Ð½Ñ Ð½Ð°Ð´ дошкою обговорень"
+
+msgid "Issue boards with milestones"
+msgstr "Дошка обговорень із етапами"
+
msgid "Issue events"
-msgstr "Події проблем"
+msgstr "Проблеми"
+
+msgid "IssueBoards|Board"
+msgstr "Дошка"
+
+msgid "IssueBoards|Boards"
+msgstr "Дошки"
msgid "Issues"
msgstr "Проблеми"
-msgid "Jobs"
-msgstr ""
-
msgid "LFSStatus|Disabled"
msgstr "Вимкнено"
@@ -784,12 +1168,21 @@ msgstr[2] "ОÑтанніх %d днів"
msgid "Last Pipeline"
msgstr "ОÑтанній Конвеєр"
-msgid "Last Update"
-msgstr "ОÑтаннє оновленнÑ"
-
msgid "Last commit"
msgstr "ОÑтанній комміт"
+msgid "Last edited %{date}"
+msgstr "ОÑтанні зміни %{date}"
+
+msgid "Last edited by %{name}"
+msgstr "ОÑтанні зміни від %{name}"
+
+msgid "Last update"
+msgstr "ОÑтаннє оновленнÑ"
+
+msgid "Last updated"
+msgstr "ВоÑтаннє оновленно"
+
msgid "LastPushEvent|You pushed to"
msgstr "Ви надіÑлали зміни до"
@@ -802,6 +1195,9 @@ msgstr "ДізнайтеÑÑŒ більше"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "Детальніше в документації по розкладами конвеєрів"
+msgid "Leave"
+msgstr "Вийти"
+
msgid "Leave group"
msgstr "Залишити групу"
@@ -809,7 +1205,7 @@ msgid "Leave project"
msgstr "Залишити проект"
msgid "License"
-msgstr ""
+msgstr "ЛіцензіÑ"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
@@ -817,8 +1213,20 @@ msgstr[0] "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ %d події"
msgstr[1] "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ %d подій"
msgstr[2] "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ %d подій"
+msgid "Lock"
+msgstr "БлокуваннÑ"
+
+msgid "Locked"
+msgstr "Заблоковано"
+
msgid "Locked Files"
-msgstr ""
+msgstr "Заблоковані файли"
+
+msgid "Login"
+msgstr "Вхід"
+
+msgid "Maximum git storage failures"
+msgstr "МакÑимальна кількіÑÑ‚ÑŒ невдач в Ñховищі даних git"
msgid "Median"
msgstr "Медіана"
@@ -827,13 +1235,13 @@ msgid "Members"
msgstr "КориÑтувачі"
msgid "Merge Requests"
-msgstr "Запит на злиттÑ"
+msgstr "Запити на злиттÑ"
msgid "Merge events"
-msgstr "Події запит на злиттÑ"
+msgstr "Запити на злиттÑ"
msgid "Merge request"
-msgstr ""
+msgstr "Запит на злиттÑ"
msgid "Messages"
msgstr "ПовідомленнÑ"
@@ -847,6 +1255,12 @@ msgstr "Моніторинг"
msgid "More information is available|here"
msgstr "тут"
+msgid "Multiple issue boards"
+msgstr "Зведені дошки обговореннÑ"
+
+msgid "New Cluster"
+msgstr "Ðовий клаÑтер"
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Ðова проблема"
@@ -865,27 +1279,42 @@ msgstr "Ðовий каталог"
msgid "New file"
msgstr "Ðовий файл"
+msgid "New group"
+msgstr "Ðова група"
+
msgid "New issue"
msgstr "Ðова проблема"
msgid "New merge request"
msgstr "Ðовий запит на злиттÑ"
+msgid "New project"
+msgstr "Ðовий проект"
+
msgid "New schedule"
msgstr "Ðовий Розклад"
msgid "New snippet"
msgstr "Ðовий Ñніппет"
+msgid "New subgroup"
+msgstr "Ðова підгрупа"
+
msgid "New tag"
msgstr "Ðовий тег"
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr "Ð’ цьому проекті немає жодного образа контейнера. Додайте його за інÑтрукціÑми вище."
+
msgid "No repository"
msgstr "Ðемає репозеторіÑ"
msgid "No schedules"
msgstr "немає Розкладів"
+msgid "None"
+msgstr "Жоден"
+
msgid "Not available"
msgstr "ÐедоÑтупний"
@@ -949,12 +1378,24 @@ msgstr "ВідÑтежувати"
msgid "Notifications"
msgstr "СповіщеннÑ"
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Фільтр"
+msgid "Only project members can comment."
+msgstr "Тільки учаÑники проекту можуть залишати коментарі."
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Відкрито"
+msgid "Opens in a new window"
+msgstr "ВідкриваєтьÑÑ Ñƒ новому вікні"
+
msgid "Options"
msgstr "Параметри"
@@ -965,20 +1406,23 @@ msgid "Owner"
msgstr "ВлаÑник"
msgid "Pagination|Last »"
-msgstr ""
+msgstr "ОÑÑ‚Ð°Ð½Ð½Ñ Â»"
msgid "Pagination|Next"
-msgstr ""
+msgstr "ÐаÑтупна"
msgid "Pagination|Prev"
-msgstr ""
+msgstr "ПопереднÑ"
msgid "Pagination|« First"
-msgstr ""
+msgstr "« Перша"
msgid "Password"
msgstr "Пароль"
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr "Люди без дозволу ніколи не отримуватимуть Ñповіщень Ñ– не зможуть коментувати."
+
msgid "Pipeline"
msgstr "Конвеєр"
@@ -992,7 +1436,7 @@ msgid "Pipeline Schedules"
msgstr "Розклади Конвеєрів"
msgid "Pipeline quota"
-msgstr ""
+msgstr "Квота на конвеєри"
msgid "PipelineCharts|Failed:"
msgstr "Ðе вдалоÑÑ:"
@@ -1081,8 +1525,53 @@ msgstr "зі ÑтадіÑми"
msgid "Preferences"
msgstr "ÐалаштуваннÑ"
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr "Приватний — доÑтуп до проекту повинен надаватиÑÑ ÐºÐ¾Ð¶Ð½Ð¾Ð¼Ñƒ кориÑтувачеві."
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr "Приватна — цю групу та Ñ—Ñ— проекти можуть бачити тільки Ñ—Ñ— кориÑтувачі."
+
msgid "Profile"
-msgstr ""
+msgstr "Профіль"
+
+msgid "Profiles|Account scheduled for removal."
+msgstr "Обліковий Ð·Ð°Ð¿Ð¸Ñ Ð·Ð°Ð¿Ð»Ð°Ð½Ð¾Ð²Ð°Ð½Ð¸Ð¹ Ð´Ð»Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ."
+
+msgid "Profiles|Delete Account"
+msgstr "Видалити обліковий запиÑ"
+
+msgid "Profiles|Delete account"
+msgstr "Видалити обліковий запиÑ"
+
+msgid "Profiles|Delete your account?"
+msgstr "Видалити ваш обліковий запиÑ?"
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr "Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу неÑе наÑтупні наÑлідки:"
+
+msgid "Profiles|Invalid password"
+msgstr "Ðеправильний пароль"
+
+msgid "Profiles|Invalid username"
+msgstr "Ðеправильне ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача"
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr "Введіть ваш %{confirmationValue} Ð´Ð»Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ:"
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr "У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” доÑтупу Ð´Ð»Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ кориÑтувача."
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr "Вам необхідно змінити влаÑника або видалити ці групи перед тим Ñк видалити ваш обліковий запиÑ."
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr "Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ñ” влаÑником в цих групах:"
+
+msgid "Profiles|your account"
+msgstr "ваш обліковий запиÑ"
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr "Проект '%{project_name}' перебуває в процеÑÑ– видаленнÑ."
msgid "Project '%{project_name}' queued for deletion."
msgstr "Проект '%{project_name}' доданий в чергу на видаленнÑ."
@@ -1093,9 +1582,6 @@ msgstr "Проект '%{project_name}' уÑпішно Ñтворений."
msgid "Project '%{project_name}' was successfully updated."
msgstr "Проект '%{project_name}' уÑпішно оновлено."
-msgid "Project '%{project_name}' will be deleted."
-msgstr "Проект '%{project_name}' видалений."
-
msgid "Project access must be granted explicitly to each user."
msgstr "ДоÑтуп до проекту повинен надаватиÑÑ ÐºÐ¾Ð¶Ð½Ð¾Ð¼Ñƒ кориÑтувачеві."
@@ -1138,9 +1624,30 @@ msgstr "Етап"
msgid "ProjectNetworkGraph|Graph"
msgstr "ІÑторіÑ"
-msgid "ProjectsDropdown|Frequently visited"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr "ЗвернітьÑÑ Ð´Ð¾ адмініÑтратора, щоб змінити це налаштуваннÑ."
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr "Тільки підпиÑані комміти можуть бути надіÑлані в цей репозиторій."
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr "Цей параметр заÑтоÑовуєтьÑÑ Ð½Ð° рівні Ñервера та може бути перевизначений адмініÑтратором."
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr "Цей параметр заÑтоÑовуєтьÑÑ Ð½Ð° рівні Ñервера, але його було перевизначено Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту."
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr "Цей параметр буде заÑтоÑовано до вÑÑ–Ñ… проектів, Ñкщо адмініÑтратор не змінить його."
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
msgstr ""
+msgid "Projects"
+msgstr "Проекти"
+
+msgid "ProjectsDropdown|Frequently visited"
+msgstr "ЧаÑто відвідувані"
+
msgid "ProjectsDropdown|Loading projects"
msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð²"
@@ -1151,7 +1658,7 @@ msgid "ProjectsDropdown|Search your projects"
msgstr "Пошук по ваших проектах"
msgid "ProjectsDropdown|Something went wrong on our end."
-msgstr ""
+msgstr "ЩоÑÑŒ пішло не так з нашого боку."
msgid "ProjectsDropdown|Sorry, no projects matched your search"
msgstr "Ðа жаль, по вашоу запиту проектів не знайдено"
@@ -1159,11 +1666,20 @@ msgstr "Ðа жаль, по вашоу запиту проектів не зна
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "Ð¦Ñ Ñ„ÑƒÐ½ÐºÑ†Ñ–Ñ Ð¿Ð¾Ñ‚Ñ€ÐµÐ±ÑƒÑ” підтримки localStorage вашим браузером"
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr "Публічна — група та вÑÑ– публічні проекти можуть переглÑдатиÑÑ Ð±ÐµÐ· автентифікації."
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr "Публічний — проект может переглÑдатиÑÑ Ð±ÐµÐ· автентифікації."
+
msgid "Push Rules"
-msgstr ""
+msgstr "Push-правила"
msgid "Push events"
-msgstr "Push події"
+msgstr "Push-події"
+
+msgid "PushRule|Committer restriction"
+msgstr ""
msgid "Read more"
msgstr "Докладніше"
@@ -1178,7 +1694,7 @@ msgid "RefSwitcher|Tags"
msgstr "Теги"
msgid "Registry"
-msgstr ""
+msgstr "РеєÑÑ‚Ñ€"
msgid "Related Commits"
msgstr "Пов'Ñзані Комміти"
@@ -1228,6 +1744,12 @@ msgstr "СкаÑувати цей запит на злиттÑ"
msgid "SSH Keys"
msgstr "Ключі SSH"
+msgid "Save"
+msgstr "Зберегти"
+
+msgid "Save changes"
+msgstr "Зберегти зміни"
+
msgid "Save pipeline schedule"
msgstr "Зберегти Розклад Конвеєра"
@@ -1235,7 +1757,7 @@ msgid "Schedule a new pipeline"
msgstr "Розклад нового конвеєра"
msgid "Schedules"
-msgstr ""
+msgstr "Розклади"
msgid "Scheduling Pipelines"
msgstr "ÐŸÐ»Ð°Ð½ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ñ–Ð²"
@@ -1243,15 +1765,21 @@ msgstr "ÐŸÐ»Ð°Ð½ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ñ–Ð²"
msgid "Search branches and tags"
msgstr "Пошук гілок та тегів"
+msgid "Seconds before reseting failure information"
+msgstr "КількіÑÑ‚ÑŒ Ñекунд до ÑÐºÐ¸Ð´Ð°Ð½Ð½Ñ Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ— про збої"
+
+msgid "Seconds to wait after a storage failure"
+msgstr "Скільки Ñекунд очікувати піÑÐ»Ñ Ð·Ð±Ð¾ÑŽ в Ñховищі даних"
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr "КількіÑÑ‚ÑŒ Ñекунд Ð¾Ñ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð¿ÐµÑ€ÐµÐ´ повторною Ñпробою доÑтупу до Ñховища даних"
+
msgid "Select Archive Format"
msgstr "Виберіть формат архіву"
msgid "Select a timezone"
msgstr "Вибрати чаÑовий поÑÑ"
-msgid "Select existing branch"
-msgstr "Виберіть гілку"
-
msgid "Select target branch"
msgstr "Вибір цільової гілки"
@@ -1277,10 +1805,10 @@ msgid "Settings"
msgstr "ÐалаштуваннÑ"
msgid "Show parent pages"
-msgstr ""
+msgstr "Показати батьківÑькі Ñторінки"
msgid "Show parent subgroups"
-msgstr ""
+msgstr "Показати батьківÑькі підгрупи"
msgid "Showing %d event"
msgid_plural "Showing %d events"
@@ -1291,101 +1819,116 @@ msgstr[2] "Показано %d подій"
msgid "Snippets"
msgstr "Фрагменти"
+msgid "Something went wrong on our end."
+msgstr "ЩоÑÑŒ пішло не так з нашого боку"
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr "ЩоÑÑŒ пішло не так, намагаючиÑÑŒ змінити ÑÑ‚Ð°Ñ‚ÑƒÑ Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ ${this.issuableDisplayName(this.issuableType)}"
+
+msgid "Something went wrong while fetching the projects."
+msgstr "ЩоÑÑŒ пішло не так під Ñ‡Ð°Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð²"
+
+msgid "Something went wrong while fetching the registry list."
+msgstr "ЩоÑÑŒ пішло не так при отриманні ÑпиÑку із реєÑтру."
+
+msgid "Sort by"
+msgstr "Сортувати за"
+
msgid "SortOptions|Access level, ascending"
-msgstr ""
+msgstr "Рівень доÑтупу, в порÑдку зроÑтаннÑ"
msgid "SortOptions|Access level, descending"
-msgstr ""
+msgstr "Рівень доÑтупу, в порÑдку ÑпаданнÑ"
msgid "SortOptions|Created date"
-msgstr ""
+msgstr "Дата ÑтвореннÑ"
msgid "SortOptions|Due date"
-msgstr ""
+msgstr "Запланована дата завершеннÑ"
msgid "SortOptions|Due later"
-msgstr ""
+msgstr "Заплановано пізніше"
msgid "SortOptions|Due soon"
-msgstr ""
+msgstr "Заплановано незабаром"
msgid "SortOptions|Label priority"
-msgstr ""
+msgstr "Пріоритет мітки"
msgid "SortOptions|Largest group"
-msgstr ""
+msgstr "Ðайбільша група"
msgid "SortOptions|Largest repository"
-msgstr ""
+msgstr "Ðайбільший репозиторій"
msgid "SortOptions|Last created"
-msgstr ""
+msgstr "ОÑтанній Ñтворений"
msgid "SortOptions|Last joined"
-msgstr ""
+msgstr "ОÑтанній приєднавшийÑÑ"
msgid "SortOptions|Last updated"
-msgstr ""
+msgstr "ОÑтанній оновлений"
msgid "SortOptions|Least popular"
-msgstr ""
+msgstr "Ðайменш популÑрний"
msgid "SortOptions|Less weight"
-msgstr ""
+msgstr "Ðайменша вага"
msgid "SortOptions|Milestone"
-msgstr ""
+msgstr "Етап"
msgid "SortOptions|Milestone due later"
-msgstr ""
+msgstr "Етап запланований на пізніше"
msgid "SortOptions|Milestone due soon"
-msgstr ""
+msgstr "Етап запланований незабаром"
msgid "SortOptions|More weight"
-msgstr ""
+msgstr "Ðайбільша вага"
msgid "SortOptions|Most popular"
-msgstr ""
+msgstr "Ðайбільш популÑрний"
msgid "SortOptions|Name"
-msgstr ""
+msgstr "Ім'Ñ"
msgid "SortOptions|Name, ascending"
-msgstr ""
+msgstr "Ім'Ñ, за зроÑтаннÑм"
msgid "SortOptions|Name, descending"
-msgstr ""
+msgstr "Ім'Ñ, за ÑпаданнÑм"
msgid "SortOptions|Oldest created"
-msgstr ""
+msgstr "ÐайÑтаріший з Ñтворених"
msgid "SortOptions|Oldest joined"
-msgstr ""
+msgstr "Приєднаний найраніше"
msgid "SortOptions|Oldest sign in"
-msgstr ""
+msgstr "Залогінений найраніше"
msgid "SortOptions|Oldest updated"
-msgstr ""
+msgstr "Оновлений найраніше"
msgid "SortOptions|Popularity"
-msgstr ""
+msgstr "ПопулÑрніÑÑ‚ÑŒ"
msgid "SortOptions|Priority"
-msgstr ""
+msgstr "Пріоритет"
msgid "SortOptions|Recent sign in"
-msgstr ""
+msgstr "Ðещодавно зареєÑтровані"
msgid "SortOptions|Start later"
-msgstr ""
+msgstr "Розпочатий пізніше"
msgid "SortOptions|Start soon"
-msgstr ""
+msgstr "Розпочатий нещодавно"
msgid "SortOptions|Weight"
-msgstr ""
+msgstr "Вага"
msgid "Source code"
msgstr "Код"
@@ -1400,7 +1943,7 @@ msgid "StarProject|Star"
msgstr "ПідпиÑатиÑÑ"
msgid "Starred projects"
-msgstr ""
+msgstr "Відмічені проекти"
msgid "Start a %{new_merge_request} with these changes"
msgstr "Почати %{new_merge_request} з цих змін"
@@ -1408,11 +1951,17 @@ msgstr "Почати %{new_merge_request} з цих змін"
msgid "Start the Runner!"
msgstr "ЗапуÑÑ‚Ñ–Ñ‚ÑŒ Runner!"
+msgid "Subgroups"
+msgstr "Підгрупи"
+
+msgid "Subscribe"
+msgstr "ПідпиÑатиÑÑ"
+
msgid "Switch branch/tag"
msgstr "тег"
msgid "System Hooks"
-msgstr ""
+msgstr "СиÑтемні Hook'и"
msgid "Tag"
msgid_plural "Tags"
@@ -1429,6 +1978,15 @@ msgstr "Цільова гілка"
msgid "Team"
msgstr "Команда"
+msgid "Thanks! Don't show me this again"
+msgstr "ДÑкую! Більше не показувати це повідомленнÑ"
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr "Розширений глобальний пошук в GitLab - це потужний інÑтрумент Ñкий заощаджує ваш чаÑ. ЗаміÑÑ‚ÑŒ Ð´ÑƒÐ±Ð»ÑŽÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð´Ñƒ Ñ– витрати чаÑу, ви можете шукати код вÑередині інших команд, Ñкий може допомогти у вашому проекті."
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+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 "Ðа Ñтадії напиÑÐ°Ð½Ð½Ñ ÐºÐ¾Ð´Ñƒ, показує Ñ‡Ð°Ñ Ð¿ÐµÑ€ÑˆÐ¾Ð³Ð¾ комміту до ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ на об'єднаннÑ. Дані будуть автоматично додані піÑÐ»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ першого запиту на об'єднаннÑ."
@@ -1441,6 +1999,15 @@ 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 number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr "КількіÑÑ‚ÑŒ збоїв піÑÐ»Ñ Ñкої Gitlab повніÑÑ‚ÑŽ заблокує доÑтуп до Ñховища данних. Лічильник кількоÑÑ‚Ñ– збоїв може бути Ñкинутий в інтерфейÑÑ– адмініÑтратора (%{link_to_health_page}), або через %{api_documentation_link}."
+
msgid "The phase of the development lifecycle."
msgstr "Фаза життєвого циклу розробки."
@@ -1471,6 +2038,12 @@ 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 time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr "КількіÑÑ‚ÑŒ Ñекунд, протÑгом Ñкої GitLab зберігає інформацію про збої. Якщо протÑгом цього періоду жодних збоїв не відбуваєтьÑÑ, Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ точку Ð¼Ð¾Ð½Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÑкидаєтьÑÑ."
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr "КількіÑÑ‚ÑŒ Ñекунд, протÑгом Ñкої GitLab намагатиметьÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ доÑтуп до Ñховища даних. По завершенню цього періоду буде згенерована помилка про Ð¿ÐµÑ€ÐµÐ²Ð¸Ñ‰ÐµÐ½Ð½Ñ Ð»Ñ–Ð¼Ñ–Ñ‚Ñƒ чаÑу."
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "ЧаÑ, витрачений на кожен елемент, зібраний на цьому етапі."
@@ -1480,12 +2053,27 @@ msgstr "Середнє Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð² Ñ€Ñдку. Приклад: між 3,
msgid "There are problems accessing Git storage: "
msgstr "Є проблеми з доÑтупом до Ñховища: "
-msgid "This is the author's first Merge Request to this project. Handle with care."
-msgstr ""
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr "Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° була змінена піÑÐ»Ñ Ñ‚Ð¾Ð³Ð¾ моменту, коли ви почали Ñ—Ñ— редагувати. Ви хотіли б Ñтворити нову?"
+
+msgid "This is a confidential issue."
+msgstr "Це конфіденційна проблема."
+
+msgid "This is the author's first Merge Request to this project."
+msgstr "Це перший запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð²Ñ–Ð´ цього автора Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту."
+
+msgid "This issue is confidential and locked."
+msgstr "Ð¦Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð° конфіденційна Ñ– заблокована."
+
+msgid "This issue is locked."
+msgstr "Ð¦Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð° заблокована."
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Це означає, що ви не можете відправлÑти код, поки не Ñтворите порожній репозиторій або ÐЕ імпортуєте Ñ–Ñнуючий."
+msgid "This merge request is locked."
+msgstr "Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð·Ð°Ð±Ð»Ð¾ÐºÐ¾Ð²Ð°Ð½Ð¾."
+
msgid "Time before an issue gets scheduled"
msgstr "Ð§Ð°Ñ Ð´Ð¾ початку потраплÑÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ в планувальник"
@@ -1564,9 +2152,6 @@ msgstr "міÑÑць тому"
msgid "Timeago|a week ago"
msgstr "тиждень тому"
-msgid "Timeago|a while"
-msgstr "деÑкий Ñ‡Ð°Ñ Ð½Ð°Ð·Ð°Ð´"
-
msgid "Timeago|a year ago"
msgstr "рік тому"
@@ -1618,6 +2203,9 @@ msgstr "через тиждень"
msgid "Timeago|in 1 year"
msgstr "через рік"
+msgid "Timeago|in a while"
+msgstr "невдовзі"
+
msgid "Timeago|less than a minute ago"
msgstr "менше хвилини тому"
@@ -1642,9 +2230,36 @@ msgstr "Загальний чаÑ"
msgid "Total test time for all commits/merges"
msgstr "Загальний чаÑ, щоб перевірити вÑÑ– фікÑації/злиттÑ"
+msgid "Track activity with Contribution Analytics."
+msgstr "ВідÑтежувати активніÑÑ‚ÑŒ за допомогою Ðналітики контриб’юторів."
+
+msgid "Unlock"
+msgstr "Розблокувати"
+
+msgid "Unlocked"
+msgstr "Розблоковано"
+
msgid "Unstar"
msgstr "ВідпиÑатиÑÑŒ"
+msgid "Unsubscribe"
+msgstr "ВідпиÑатиÑÑ"
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr "Перейдіть на вищий тарифний план щоб активувати Покращений Глобальний Пошук."
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr "Перейдіть на вищий тарифний план Ð´Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ— Ðналітики контриб’юторів."
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr "Перейдіть на вищий тарифний план щоб активувати Групові Webhook’и."
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr "Підвищіть план щоб активувати вагу обговорень."
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr "Підвищіть Ñвій план, щоб покращити дошки обговорень."
+
msgid "Upload New File"
msgstr "Завантажити новий файл"
@@ -1661,13 +2276,13 @@ msgid "Use your global notification setting"
msgstr "ВикориÑтовуютьÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ñ– Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ"
msgid "View file @ "
-msgstr ""
+msgstr "ПереглÑд файла @ "
msgid "View open merge request"
msgstr "ПереглÑд відкритих запитів на злиттÑ"
msgid "View replaced file @ "
-msgstr ""
+msgstr "ПереглÑд заміненого файлу @ "
msgid "VisibilityLevel|Internal"
msgstr "Внутрішній"
@@ -1687,9 +2302,120 @@ msgstr "Хочете побачити дані? Будь лаÑка, попроÑ
msgid "We don't have enough data to show this stage."
msgstr "Ми не маємо доÑтатньо даних Ð´Ð»Ñ Ð¿Ð¾ÐºÐ°Ð·Ñƒ цього етапу."
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr "Webhook дозволÑÑŽÑ‚ÑŒ вам викликати адреÑу URL Ñкщо, наприклад, відправлений новий код або Ñтворено нову тему повідомленнÑ. Ви можете налаштувати Webhook так, щоб він реагував на певні події, такі Ñк відправки коду, Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð°Ð±Ð¾ запити на злиттÑ. Групові Webhook’и заÑтоÑовуютьÑÑ Ð´Ð¾ вÑÑ–Ñ… проектів в групі Ñ– дозволÑÑŽÑ‚ÑŒ вам Ñтандартизувати функціональніÑÑ‚ÑŒ Webhook’ів Ð´Ð»Ñ Ð²Ñієї вашої групи."
+
+msgid "Weight"
+msgstr "Вага"
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr "Коли відбуваєтьÑÑ Ð·Ð±Ñ–Ð¹ при доÑтупі до Ñховища даних, GitLab блокує доÑуп до нього протÑгом періоду чаÑу, заданому тут. Це дає можливіÑÑ‚ÑŒ файловій ÑиÑтемі відновитиÑÑ. Репозиторії на шардах (shards) зі збоÑми тимчаÑово не доÑтупні"
+
msgid "Wiki"
msgstr "Wiki"
+msgid "WikiClone|Clone your wiki"
+msgstr "Клонувати ваш wiki"
+
+msgid "WikiClone|Git Access"
+msgstr "Git доÑтуп"
+
+msgid "WikiClone|Install Gollum"
+msgstr "Ð’Ñтановити Gollum"
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr "РекомендуєтьÑÑ Ð²Ñтановити %{markdown}, з тим щоб GFM функції візуалізувалиÑÑ Ð»Ð¾ÐºÐ°Ð»ÑŒÐ½Ð¾:"
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr "ЗапуÑÑ‚Ñ–Ñ‚ÑŒ Gollum Ñ– редагуйте локально"
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr "Ви не можете Ñтворювати вікі-Ñторінки"
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr "Це — Ñтара верÑÑ–Ñ Ñторінки."
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr "Ви можете переглÑнути %{most_recent_link} або перейти на %{history_link}."
+
+msgid "WikiHistoricalPage|history"
+msgstr "Ñ–ÑторіÑ"
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr "оÑÑ‚Ð°Ð½Ð½Ñ Ð²ÐµÑ€ÑÑ–Ñ"
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr "Більше прикладів знаходитьÑÑ Ð² %{docs_link}"
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr "документаціÑ"
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr "Ð”Ð»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¿Ð¾ÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° (нову) Ñторінку, проÑто введіть %{link_example}"
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr "ІнÑтрукціÑ"
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr "Порада: можна вказати повний шлÑÑ… до нового файлу. Ми автоматично Ñтворимо вÑÑ– відÑутні каталоги."
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr "Ðова Вікі-Ñторінка"
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr "Ви дійÑно бажаєте видалити цю Ñторінку?"
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr "ХтоÑÑŒ редагував Ñторінку в той же чаÑ, що Ñ– ви. Будь лаÑка, ознайомтеÑÑ Ð· %{page_link} Ñ– переконайтеÑÑ, ваші зміни не затруть зміни інших."
+
+msgid "WikiPageConflictMessage|the page"
+msgstr "Ñторінка"
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr "Створити %{page_title}"
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr "Оновити %{page_title}"
+
+msgid "WikiPage|Page slug"
+msgstr "ШлÑÑ… Ñторінки"
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr "Ðапишіть текÑÑ‚ або перетÑгніть файли Ñюди..."
+
+msgid "Wiki|Create Page"
+msgstr "Створити Ñторінку"
+
+msgid "Wiki|Create page"
+msgstr "Створити Ñторінку"
+
+msgid "Wiki|Edit Page"
+msgstr "Редагувати Ñторінку"
+
+msgid "Wiki|Empty page"
+msgstr "ÐŸÐ¾Ñ€Ð¾Ð¶Ð½Ñ Ñторінка"
+
+msgid "Wiki|More Pages"
+msgstr "Більше Ñторінок"
+
+msgid "Wiki|New page"
+msgstr "Ðова Ñторінка"
+
+msgid "Wiki|Page history"
+msgstr "ІÑÑ‚Ð¾Ñ€Ñ–Ñ Ñторінки"
+
+msgid "Wiki|Page version"
+msgstr "ВерÑÑ–Ñ Ñторінки"
+
+msgid "Wiki|Pages"
+msgstr "Сторінки"
+
+msgid "Wiki|Wiki Pages"
+msgstr "Вікі-Ñторінки"
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr "З аналітикою контриб’юторів ви може вивчати активніÑÑ‚ÑŒ в обговореннÑÑ…, запитах на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ– подій відправки коду Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ñ— організації Ñ– Ñ—Ñ— учаÑників."
+
msgid "Withdraw Access Request"
msgstr "СкаÑувати запит доÑтупу"
@@ -1705,9 +2431,21 @@ msgstr "Ви збираєтеÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ зв'Ñзок з форка Ð
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 on a read-only GitLab instance."
+msgstr "Ви знаходитеÑÑ Ð½Ð° інÑтанÑÑ– Gitlab \"тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ\"."
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr "Ви знаходитеÑÑ Ð½Ð° інÑтанÑÑ– Gitlab \"тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ\". Якщо ви хочете внеÑти зміни, вам необхідно перейти на %{link_to_primary_node}."
+
msgid "You can only add files when you are on a branch"
msgstr "Ви можете додавати тільки файли, коли перебуваєте в гілці"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr "Ви не можете запиÑувати на вторинні інÑтанÑи \"тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ\" GitLab Geo. Будь лаÑка викориÑтовуйте %{link_to_primary_node}."
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr "Ви не можете запиÑувати на цей \"тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ\" інÑÑ‚Ð°Ð½Ñ GitLab."
+
msgid "You have reached your project limit"
msgstr "Ви доÑÑгли Ð¾Ð±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð² вашому проекті"
@@ -1738,14 +2476,20 @@ 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 "Ви не зможете отримувати Ñ– відправлÑти код проекту через SSH поки %{add_ssh_key_link} в ваш профіль."
+msgid "Your comment will not be visible to the public."
+msgstr "Ваш коментар не буде видимим Ð´Ð»Ñ Ð²ÑÑ–Ñ…."
+
+msgid "Your groups"
+msgstr "Ваші групи"
+
msgid "Your name"
msgstr "Ваше ім'Ñ"
msgid "Your projects"
-msgstr ""
+msgstr "Ваші проекти"
msgid "commit"
-msgstr ""
+msgstr "комміт"
msgid "day"
msgid_plural "days"
@@ -1765,3 +2509,15 @@ msgstr[0] "джерело"
msgstr[1] "джерела"
msgstr[2] "джерел"
+msgid "password"
+msgstr "пароль"
+
+msgid "personal access token"
+msgstr "оÑобиÑтий токен доÑтупу"
+
+msgid "to help your contributors communicate effectively!"
+msgstr "щоб допомогти вашим контриб’юторам ефективно ÑпілкуватиÑÑ!"
+
+msgid "username"
+msgstr "ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача"
+
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index 4a05b159008..0d6f0d201c2 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:44-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-06 01:25-0500\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -20,6 +20,10 @@ msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d 次æ交"
+msgid "%d layer"
+msgid_plural "%d layers"
+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 次æ交。"
@@ -27,8 +31,12 @@ msgstr[0] "为æ高页é¢åŠ è½½é€Ÿåº¦åŠæ€§èƒ½ï¼Œå·²çœç•¥äº† %s 次æ交。"
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "ç”± %{commit_author_link} æ交于 %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] "%{count} ä½å‚与者"
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
-msgstr ""
+msgstr "%{number_commits_behind} 个è½åŽ %{default_branch} 分支的æ交, %{number_commits_ahead} 早超å‰çš„æ交"
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr "已失败 %{number_of_failures} 次/最多å…许失败失败 %{maximum_failures} 次,GitLab 将继续é‡è¯•ã€‚"
@@ -46,12 +54,21 @@ msgstr[0] "%{storage_name}:已 %{failed_attempts} 次å°è¯•è®¿é—®å­˜å‚¨å¤±è´¥ï
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(如需了解更多的安装信æ¯ï¼Œè¯·æŸ¥çœ‹ %{link})"
+msgid "+ %{moreCount} more"
+msgstr "+ 还有 %{moreCount} æ¡"
+
+msgid "- show less"
+msgstr "- 显示较少"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d æ¡æµæ°´çº¿"
msgid "1st contribution!"
-msgstr ""
+msgstr "最高贡献"
+
+msgid "2FA enabled"
+msgstr "å¯ç”¨ä¸¤æ­¥éªŒè¯"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "æŒç»­é›†æˆæ•°æ®å›¾"
@@ -77,12 +94,18 @@ msgstr "å¯ç”¨"
msgid "Activity"
msgstr "活动"
+msgid "Add"
+msgstr "添加"
+
msgid "Add Changelog"
msgstr "添加更新日志"
msgid "Add Contribution guide"
msgstr "添加贡献指å—"
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr "添加æ¥è‡ª Webhooks 或者 GitLab ä¼ä¸šç‰ˆçš„团队。"
+
msgid "Add License"
msgstr "添加许å¯è¯"
@@ -92,11 +115,20 @@ msgstr "新建一个用于推é€æˆ–拉å–çš„ SSH 秘钥到账å·ä¸­ã€‚"
msgid "Add new directory"
msgstr "添加目录"
+msgid "AdminHealthPageLink|health page"
+msgstr "å¥åº·é¡µé¢"
+
+msgid "Advanced settings"
+msgstr "高级设置"
+
msgid "All"
msgstr "全部"
+msgid "An error occurred. Please try again."
+msgstr "å‘生了错误,请å†è¯•ä¸€æ¬¡ã€‚"
+
msgid "Appearance"
-msgstr ""
+msgstr "外观"
msgid "Applications"
msgstr "应用程åº"
@@ -110,6 +142,9 @@ msgstr "确定è¦åˆ é™¤æ­¤æµæ°´çº¿è®¡åˆ’å—?"
msgid "Are you sure you want to discard your changes?"
msgstr "确定è¦æ”¾å¼ƒä¿®æ”¹å—?"
+msgid "Are you sure you want to leave this group?"
+msgstr "确定è¦ç¦»å¼€è¿™ä¸ªç¾¤ç»„å—?"
+
msgid "Are you sure you want to reset registration token?"
msgstr "确定è¦é‡ç½®æ³¨å†Œä»¤ç‰Œå—?"
@@ -120,91 +155,97 @@ msgid "Are you sure?"
msgstr "确定å—?"
msgid "Artifacts"
-msgstr ""
+msgstr "产物"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放文件到此处或者 %{upload_link}"
msgid "Authentication Log"
-msgstr ""
+msgstr "认è¯æ—¥å¿—"
-msgid "Auto DevOps (Beta)"
-msgstr ""
-
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
-msgid "Auto DevOps documentation"
-msgstr ""
+msgid "Author"
+msgstr "作者"
msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "自动审查程åºå’Œè‡ªåŠ¨éƒ¨ç½²ç¨‹åºéœ€è¦ä¸€ä¸ªåŸŸåå’Œ %{kubernetes} æ‰èƒ½æ­£å¸¸å·¥ä½œã€‚"
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
-msgstr ""
+msgstr "自动审查程åºå’Œè‡ªåŠ¨éƒ¨ç½²ç¨‹åºéœ€è¦ä¸€ä¸ªåŸŸåæ‰èƒ½æ­£å¸¸å·¥ä½œã€‚"
msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "自动审查程åºå’Œè‡ªåŠ¨éƒ¨ç½²ç¨‹åºéœ€è¦ %{kubernetes} æ‰èƒ½æ­£å¸¸å·¥ä½œã€‚"
-msgid "AutoDevOps|Learn more in the"
-msgstr ""
+msgid "AutoDevOps|Auto DevOps (Beta)"
+msgstr "DevOps 自动化(测试版)"
+
+msgid "AutoDevOps|Auto DevOps documentation"
+msgstr "DevOps 自动化文档"
+
+msgid "AutoDevOps|Enable in settings"
+msgstr "在设置中å¯ç”¨"
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr "将根æ®é¢„定义的 CI/CD é…置自动构建ã€æµ‹è¯•å’Œéƒ¨ç½²åº”用程åºã€‚"
+
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
+msgstr "想了解更多请访问 %{link_to_documentation}"
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgstr "您å¯ä»¥ä¸ºæ­¤é¡¹ç›®æ¿€æ´» %{link_to_settings}。"
msgid "Billing"
-msgstr ""
+msgstr "è´¦å•"
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr ""
+msgstr "%{group_name} 正在使用 %{plan_link} 方案。"
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr ""
+msgstr "在当å‰æ–¹æ¡ˆä¸­ä¸å¯ä½¿ç”¨è‡ªåŠ¨é™çº§æˆ–自动å‡çº§ã€‚"
msgid "BillingPlans|Current plan"
-msgstr ""
+msgstr "当å‰æ–¹æ¡ˆ"
msgid "BillingPlans|Customer Support"
-msgstr ""
+msgstr "用户支æŒ"
+
+msgid "BillingPlans|Downgrade"
+msgstr "é™çº§"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr ""
+msgstr "阅读 %{faq_link} 以了解更多信æ¯ã€‚"
msgid "BillingPlans|Manage plan"
-msgstr ""
+msgstr "管ç†æ–¹æ¡ˆ"
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr ""
+msgstr "请è”ç³» %{customer_support_link}"
msgid "BillingPlans|See all %{plan_name} features"
-msgstr ""
+msgstr "查看所有 %{plan_name} 功能"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr ""
+msgstr "使用与其父项目一致的方案"
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr ""
+msgstr "访问 %{parent_billing_page_link} 以管ç†è¯¥é¡¹ç›®çš„计费方案。"
msgid "BillingPlans|Upgrade"
-msgstr ""
+msgstr "å‡çº§"
msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr ""
+msgstr "您目å‰æ­£åœ¨ä½¿ç”¨ %{plan_link} 方案。"
msgid "BillingPlans|frequently asked questions"
-msgstr ""
+msgstr "常è§é—®é¢˜"
msgid "BillingPlans|monthly"
-msgstr ""
+msgstr "æ¯æœˆ"
msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr ""
+msgstr "æ¯å¹´æ”¯ä»˜ %{price_per_year}"
msgid "BillingPlans|per user"
-msgstr ""
-
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
+msgstr "æ¯ä¸ªç”¨æˆ·"
msgid "Branch"
msgid_plural "Branches"
@@ -213,6 +254,9 @@ 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 has changed"
+msgstr "分支已有新å˜æ›´"
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "æœç´¢åˆ†æ”¯"
@@ -223,88 +267,88 @@ msgid "Branches"
msgstr "分支"
msgid "Branches|Cant find HEAD commit for this branch"
-msgstr ""
+msgstr "ä¸èƒ½æ‰¾åˆ°è¿™ä¸ªåˆ†æ”¯çš„ HEAD æ交"
msgid "Branches|Compare"
-msgstr ""
+msgstr "比较"
msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
-msgstr ""
+msgstr "删除所有已åˆå¹¶åˆ° %{default_branch} 的分支。"
msgid "Branches|Delete branch"
-msgstr ""
+msgstr "删除分支"
msgid "Branches|Delete merged branches"
-msgstr ""
+msgstr "删除已åˆå¹¶çš„分支"
msgid "Branches|Delete protected branch"
-msgstr ""
+msgstr "删除å—ä¿æŠ¤çš„分支"
msgid "Branches|Delete protected branch '%{branch_name}'?"
-msgstr ""
+msgstr "确认删除å—ä¿æŠ¤çš„分支 '%{branch_name}'?"
msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
-msgstr ""
+msgstr "删除 â€%{branch_name}†åŽå°†æ— æ³•æ¢å¤ï¼Œæ‚¨ç¡®å®šï¼Ÿ"
msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
-msgstr ""
+msgstr "删除已åˆå¹¶çš„分支åŽå°†æ— æ³•æ¢å¤ï¼Œæ‚¨ç¡®å®šï¼Ÿ"
msgid "Branches|Filter by branch name"
-msgstr ""
+msgstr "按分支å称筛选"
msgid "Branches|Merged into %{default_branch}"
-msgstr ""
+msgstr "åˆå¹¶åˆ° %{default_branch}"
msgid "Branches|New branch"
-msgstr ""
+msgstr "新建分支"
msgid "Branches|No branches to show"
-msgstr ""
+msgstr "找ä¸åˆ°åˆ†æ”¯"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
-msgstr ""
+msgstr "确认执行 %{delete_protected_branch} åŽå°†æ— æ³•æ’¤é”€æˆ–æ¢å¤ã€‚"
msgid "Branches|Only a project master or owner can delete a protected branch"
-msgstr ""
+msgstr "åªæœ‰é¡¹ç›®ç®¡ç†è€…或所有者æ‰èƒ½åˆ é™¤å—ä¿æŠ¤çš„分支ï¼"
msgid "Branches|Protected branches can be managed in %{project_settings_link}"
-msgstr ""
+msgstr "在 %{project_settings_link} 管ç†å—ä¿æŠ¤çš„分支"
msgid "Branches|Sort by"
-msgstr ""
+msgstr "排åº"
msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
+msgstr "分支无法自动æ交,因为与上游分支冲çªã€‚"
msgid "Branches|The default branch cannot be deleted"
-msgstr ""
+msgstr "无法删除默认分支"
msgid "Branches|This branch hasn’t been merged into %{default_branch}."
-msgstr ""
+msgstr "此分支尚未åˆå¹¶åˆ° %{default_branch}。"
msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
-msgstr ""
+msgstr "为é¿å…æ•°æ®ä¸¢å¤±ï¼Œè¯·åœ¨åˆ é™¤ä¹‹å‰åˆå¹¶æ­¤åˆ†æ”¯ã€‚"
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
-msgstr ""
+msgstr "è¦ç¡®è®¤ï¼Ÿè¯·è¾“å…¥ %{branch_name_confirmation} :"
msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
+msgstr "è‹¥è¦æ”¾å¼ƒæœ¬åœ°æ›´æ”¹å¹¶ä½¿ç”¨ä¸Šæ¸¸ç‰ˆæœ¬è¦†ç›–本分支,请先删除并“立å³æ›´æ–°â€ã€‚"
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
-msgstr ""
+msgstr "å°†è¦æ°¸ä¹…删除å—ä¿æŠ¤ %{branch_name} 分支。"
msgid "Branches|diverged from upstream"
-msgstr ""
+msgstr "与上游存在差异"
msgid "Branches|merged"
-msgstr ""
+msgstr "å·²åˆå¹¶çš„"
msgid "Branches|project settings"
-msgstr ""
+msgstr "项目设置"
msgid "Branches|protected"
-msgstr ""
+msgstr "å—ä¿æŠ¤çš„"
msgid "Browse Directory"
msgstr "æµè§ˆç›®å½•"
@@ -327,12 +371,18 @@ msgstr "CI / CD"
msgid "CI configuration"
msgstr "CI é…ç½®"
+msgid "CICD|Jobs"
+msgstr "作业"
+
msgid "Cancel"
msgstr "å–消"
msgid "Cancel edit"
msgstr "å–消编辑"
+msgid "Change Weight"
+msgstr "å˜æ›´æƒé‡"
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "选择分支"
@@ -352,7 +402,7 @@ msgid "Charts"
msgstr "统计图"
msgid "Chat"
-msgstr "交æµ"
+msgstr "å³æ—¶é€šè®¯"
msgid "Cherry-pick this commit"
msgstr "优选此æ交"
@@ -360,6 +410,9 @@ msgstr "优选此æ交"
msgid "Cherry-pick this merge request"
msgstr "优选此åˆå¹¶è¯·æ±‚"
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr "选择è¦å¤åˆ¶åˆ°æ­¤èŠ‚点的群组。留空则å¤åˆ¶æ‰€æœ‰ã€‚"
+
msgid "CiStatusLabel|canceled"
msgstr "å·²å–消"
@@ -414,6 +467,147 @@ msgstr "已跳过"
msgid "CiStatus|running"
msgstr "è¿è¡Œä¸­"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr "断路器 API"
+
+msgid "Clone repository"
+msgstr "克隆存储库"
+
+msgid "Close"
+msgstr "关闭"
+
+msgid "Cluster"
+msgstr "集群"
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr "必须在此å¸æˆ·ä¸‹åˆ›å»º %{link_to_container_project}"
+
+msgid "ClusterIntegration|Cluster details"
+msgstr "集群详情"
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr "集群集æˆ"
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr "此项目已ç¦ç”¨é›†ç¾¤é›†æˆã€‚"
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr "此项目已å¯ç”¨é›†ç¾¤é›†æˆã€‚"
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr "此项目已å¯ç”¨é›†ç¾¤é›†æˆã€‚ç¦ç”¨æ­¤é›†æˆä¸ä¼šå½±å“您的集群,它åªä¼šæš‚时关闭 GitLab 的连接。"
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr "集群正在 Google Container Engine 上创建..."
+
+msgid "ClusterIntegration|Cluster name"
+msgstr "集群å称"
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr "集群已在 Google Container Engine 上æˆåŠŸåˆ›å»º"
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr "å¤åˆ¶é›†ç¾¤å称"
+
+msgid "ClusterIntegration|Create cluster"
+msgstr "创建集群"
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr "在 Google Container Engine 上创建新集群"
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr "å¯ç”¨é›†ç¾¤é›†æˆ"
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr "Google 云平å°é¡¹ç›®ID"
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr "Google Container Engine"
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr "Google Container Engine 项目"
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr "了解详细%{link_to_documentation}"
+
+msgid "ClusterIntegration|Machine type"
+msgstr "机器类型"
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr "ç¡®ä¿æ‚¨çš„å¸æˆ·ç¬¦åˆåˆ›å»ºé›†ç¾¤çš„%{link_to_requirements}"
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr "管ç†æ‚¨çš„ GitLab 项目集群集æˆ"
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr "访问%{link_gke}æ¥ç®¡ç†æ‚¨çš„集群"
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr "节点数é‡"
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr "请确ä¿æ‚¨çš„ Google å¸æˆ·ç¬¦åˆä»¥ä¸‹è¦æ±‚:"
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr "项目命å空间(å¯é€‰ï¼Œå”¯ä¸€)"
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr "请阅读关于集群集æˆçš„%{link_to_help_page}。"
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr "删除集群集æˆ"
+
+msgid "ClusterIntegration|Remove integration"
+msgstr "删除集æˆ"
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr "删除集群集æˆå°†åˆ é™¤æ‚¨æ·»åŠ åˆ°æ­¤é¡¹ç›®çš„集群é…置。它ä¸ä¼šåˆ é™¤æ‚¨çš„项目。"
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr "查看并编辑集群的详细信æ¯"
+
+msgid "ClusterIntegration|See machine types"
+msgstr "å‚è§æœºå™¨ç±»åž‹"
+
+msgid "ClusterIntegration|See your projects"
+msgstr "看到您的项目"
+
+msgid "ClusterIntegration|See zones"
+msgstr "查看区域"
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr "å‘生了内部错误"
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr "在 Google Container Engine 上创建集群时å‘生错误"
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr "切æ¢é›†ç¾¤"
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr "使用与此项目关è”的集群,您å¯ä»¥ä½¿ç”¨å®¡é˜…应用程åºï¼Œéƒ¨ç½²åº”用程åºï¼Œè¿è¡Œæµæ°´çº¿ç­‰ç­‰ã€‚"
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr "您的å¸æˆ·å¿…须有%{link_to_container_engine}"
+
+msgid "ClusterIntegration|Zone"
+msgstr "区域"
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr "访问 Google Container Engine"
+
+msgid "ClusterIntegration|cluster"
+msgstr "集群"
+
+msgid "ClusterIntegration|help page"
+msgstr "帮助页é¢"
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr "符åˆè¦æ±‚"
+
+msgid "ClusterIntegration|properly configured"
+msgstr "正确é…ç½®"
+
msgid "Comments"
msgstr "评论"
@@ -421,6 +615,13 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "æ交"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] "æ交 %d 个文件"
+
+msgid "Commit Message"
+msgstr "æ交消æ¯"
+
msgid "Commit duration in minutes for last 30 commits"
msgstr "最近30次æ交相应æŒç»­é›†æˆèŠ±è´¹çš„时间(分钟)"
@@ -448,14 +649,65 @@ msgstr "æ交者:"
msgid "Compare"
msgstr "比较"
+msgid "Container Registry"
+msgstr "容器注册"
+
+msgid "ContainerRegistry|Created"
+msgstr "已创建"
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr "首先使用您的 GitLab 用户å和密ç ç™»å½• GitLab 的容器注册表。如果您有%{link_2fa},您需è¦ä½¿ç”¨%{link_token}:"
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr "GitLab 最多支æŒ3个级别的镜åƒå称。以下镜åƒç¤ºä¾‹å¯¹æ‚¨çš„项目有效:"
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr "如何使用容器注册表"
+
+msgid "ContainerRegistry|Learn more about"
+msgstr "关于更多"
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr "容器注册表中没有此容器镜åƒçš„标签。"
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr "登录åŽæ‚¨å¯ä»¥ä½¿ç”¨é€šç”¨çš„%{build}å’Œ%{push}命令自由创建和上传容器映åƒ"
+
+msgid "ContainerRegistry|Remove repository"
+msgstr "删除存储库"
+
+msgid "ContainerRegistry|Remove tag"
+msgstr "删除标签"
+
+msgid "ContainerRegistry|Size"
+msgstr "大å°"
+
+msgid "ContainerRegistry|Tag"
+msgstr "标签"
+
+msgid "ContainerRegistry|Tag ID"
+msgstr "标签 ID"
+
+msgid "ContainerRegistry|Use different image names"
+msgstr "使用ä¸åŒçš„é•œåƒå称"
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr "å°† Docker 容器注册表集æˆåˆ° GitLab 中,æ¯ä¸ªé¡¹ç›®éƒ½å¯ä»¥æœ‰è‡ªå·±çš„空间æ¥å­˜å‚¨ Docker 的图åƒã€‚"
+
msgid "Contribution guide"
msgstr "贡献指å—"
msgid "Contributors"
msgstr "贡献者"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr "控制此次è¦èŠ‚点的 LFS/attachment 的最大并å‘性"
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr "控制此次è¦èŠ‚点的存储库的最大并å‘"
+
msgid "Copy SSH public key to clipboard"
-msgstr ""
+msgstr "å¤åˆ¶ SSH 公钥到剪贴æ¿"
msgid "Copy URL to clipboard"
msgstr "å¤åˆ¶ URL 到剪贴æ¿"
@@ -466,9 +718,6 @@ msgstr "å¤åˆ¶æ交 SHA 的值到剪贴æ¿"
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 "在å¸æˆ·ä¸Šåˆ›å»ºä¸ªäººè®¿é—®ä»¤ç‰Œï¼Œä»¥é€šè¿‡ %{protocol} æ¥æ‹‰å–或推é€ã€‚"
@@ -478,9 +727,21 @@ msgstr "创建目录"
msgid "Create empty bare repository"
msgstr "创建空的存储库"
+msgid "Create file"
+msgstr "创建文件"
+
msgid "Create merge request"
msgstr "创建åˆå¹¶è¯·æ±‚"
+msgid "Create new branch"
+msgstr "创建新分支"
+
+msgid "Create new directory"
+msgstr "创建新目录"
+
+msgid "Create new file"
+msgstr "创建新文件"
+
msgid "Create new..."
msgstr "创建..."
@@ -532,6 +793,12 @@ msgstr "预å‘布"
msgid "CycleAnalyticsStage|Test"
msgstr "测试"
+msgid "DashboardProjects|All"
+msgstr "所有"
+
+msgid "DashboardProjects|Personal"
+msgstr "个人"
+
msgid "Define a custom pattern with cron syntax"
msgstr "使用 Cron 语法定义自定义模å¼"
@@ -548,6 +815,9 @@ msgstr "部署密钥"
msgid "Description"
msgstr "æè¿°"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr "æ述模æ¿å…许您为项目的议题和åˆå¹¶è¯·æ±‚在创建时选择特定的模版。"
+
msgid "Details"
msgstr "详情"
@@ -557,6 +827,12 @@ msgstr "目录å称"
msgid "Discard changes"
msgstr "放弃更改"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr "关闭循环分æžä»‹ç»æ¡†"
+
+msgid "Dismiss Merge Request promotion"
+msgstr "关闭åˆå¹¶è¯·æ±‚中的促销广告"
+
msgid "Don't show again"
msgstr "ä¸å†æ˜¾ç¤º"
@@ -596,9 +872,6 @@ msgstr "编辑 %{id} æµæ°´çº¿è®¡åˆ’"
msgid "Emails"
msgstr "电å­é‚®ä»¶"
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr "全部"
@@ -627,7 +900,10 @@ msgid "Every week (Sundays at 4:00am)"
msgstr "æ¯å‘¨æ‰§è¡Œï¼ˆå‘¨æ—¥å‡Œæ™¨ 4 点)"
msgid "Explore projects"
-msgstr ""
+msgstr "查看项目"
+
+msgid "Explore public groups"
+msgstr "æœç´¢å…¬å…±ç¾¤ç»„"
msgid "Failed to change the owner"
msgstr "无法å˜æ›´æ‰€æœ‰è€…"
@@ -635,6 +911,9 @@ msgstr "无法å˜æ›´æ‰€æœ‰è€…"
msgid "Failed to remove the pipeline schedule"
msgstr "无法删除æµæ°´çº¿è®¡åˆ’"
+msgid "File name"
+msgstr "文件å"
+
msgid "Files"
msgstr "文件"
@@ -660,6 +939,12 @@ msgstr[0] "派生"
msgid "ForkedFromProjectPath|Forked from"
msgstr "派生自"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr "派生自 %{project_name} (删除)"
+
+msgid "Format"
+msgstr "æ ¼å¼"
+
msgid "From issue creation until deploy to production"
msgstr "从创建议题到部署至生产环境"
@@ -670,7 +955,19 @@ msgid "GPG Keys"
msgstr "GPG 密钥"
msgid "Geo Nodes"
-msgstr ""
+msgstr "Geo 节点"
+
+msgid "Geo|File sync capacity"
+msgstr "文件åŒæ­¥å®¹é‡"
+
+msgid "Geo|Groups to replicate"
+msgstr "è¦å¤åˆ¶çš„群组"
+
+msgid "Geo|Repository sync capacity"
+msgstr "存储库åŒæ­¥å®¹é‡"
+
+msgid "Geo|Select groups to replicate."
+msgstr "选择è¦å¤åˆ¶çš„群组。"
msgid "Git storage health information has been reset"
msgstr "Git 存储å¥åº·ä¿¡æ¯å·²é‡ç½®"
@@ -684,29 +981,77 @@ msgstr "跳转到派生项目"
msgid "GoToYourFork|Fork"
msgstr "跳转到派生项目"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr "Google 身份验è¯ä¸æ˜¯%{link_to_documentation}。如果您想使用此æœåŠ¡ï¼Œè¯·å’¨è¯¢æ‚¨çš„ GitLab 管ç†å‘˜ã€‚"
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
-msgstr ""
+msgstr "ç¦æ­¢ä¸Žå…¶ä»–群组共享 %{group} 中的项目"
msgid "GroupSettings|Share with group lock"
-msgstr ""
+msgstr "共享群组é”"
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
-msgstr ""
+msgstr "此设置已ç»åº”用于 %{ancestor_group},并已覆盖此å­ç»„的设置。"
msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "此设置已应用于 %{ancestor_group}。若è¦ä¸Žå…¶å®ƒç¾¤ç»„共享此群组中的的项目,请è”系所有者覆盖此设置或者 %{remove_ancestor_share_with_group_lock}。"
msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "此设置已应用于 %{ancestor_group}。 您å¯ä»¥è¦†ç›–此设置或 %{remove_ancestor_share_with_group_lock}。"
msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
-msgstr ""
+msgstr "此设置将应用于所有å­ç»„,除éžç”±ç»„所有者覆盖。已ç»æœ‰æƒè®¿é—®è¯¥é¡¹ç›®çš„群组将继续访问,除éžæ‰‹åŠ¨ç§»é™¤ã€‚"
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
-msgstr ""
+msgstr "无法ç¦ç”¨çˆ¶ç»„的“共享群组é”â€ï¼Œåªæœ‰çˆ¶ç¾¤ç»„的所有者æ‰å¯ä»¥æ“作ï¼"
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
-msgstr ""
+msgstr "从 %{ancestor_group_name} 中删除共享群组é”"
+
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr "群组是几个项目的集åˆã€‚"
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr "如果您在一个群组下组织项目,它的工作方å¼å°±åƒä¸€ä¸ªæ–‡ä»¶å¤¹ã€‚"
+
+msgid "GroupsEmptyState|No groups found"
+msgstr "找ä¸åˆ°ç¾¤ç»„"
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr "您å¯ä»¥ç®¡ç†ç¾¤ç»„æˆå‘˜çš„æƒé™å¹¶è®¿é—®ç¾¤ç»„中的æ¯ä¸ªé¡¹ç›®ã€‚"
+
+msgid "GroupsTreeRole|as"
+msgstr "çš„"
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr "您确定è¦ç¦»å¼€ç¾¤ç»„“${this.group.fullName}â€å—?"
+
+msgid "GroupsTree|Create a project in this group."
+msgstr "在此群组中创建一个项目。"
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr "在此群组中创建一个å­ç¾¤ç»„。"
+
+msgid "GroupsTree|Edit group"
+msgstr "编辑群组"
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr "无法离开群组。请确ä¿æ‚¨ä¸æ˜¯å”¯ä¸€çš„所有者。"
+
+msgid "GroupsTree|Filter by name..."
+msgstr "按å称过滤..."
+
+msgid "GroupsTree|Leave this group"
+msgstr "离开这个群组"
+
+msgid "GroupsTree|Loading groups"
+msgstr "加载群组中"
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰æœç´¢åˆ°ä»»ä½•ç¬¦åˆçš„群组"
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰ä»»ä½•ç¾¤ç»„或项目符åˆæ‚¨çš„æœç´¢"
msgid "Health Check"
msgstr "å¥åº·æ£€æŸ¥"
@@ -726,30 +1071,61 @@ msgstr "没有检测到å¥åº·é—®é¢˜"
msgid "HealthCheck|Unhealthy"
msgstr "éžå¥åº·"
+msgid "History"
+msgstr "历å²"
+
msgid "Housekeeping successfully started"
msgstr "已开始维护"
msgid "Import repository"
msgstr "导入存储库"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr "å助改进 GitLab ä¼ä¸šç‰ˆçš„议题看æ¿ã€‚"
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr "å助改善GitLab ä¼ä¸šç‰ˆçš„议题管ç†ä¸Žæƒé‡ã€‚"
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr "å助改进GitLab ä¼ä¸šç‰ˆçš„æœç´¢å’Œé«˜çº§å…¨å±€æœç´¢ 。"
+
msgid "Install a Runner compatible with GitLab CI"
msgstr "安装一个与 GitLab CI 兼容的 Runner"
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] "例å­"
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr "内部 - 任何登录的用户都å¯ä»¥æŸ¥çœ‹è¯¥ç¾¤ç»„和任何内部项目。"
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr "内部 - å¯ä»¥é€šè¿‡ä»»ä½•ç™»å½•ç”¨æˆ·è®¿é—®è¯¥é¡¹ç›®ã€‚"
+
msgid "Interval Pattern"
msgstr "循环周期"
msgid "Introducing Cycle Analytics"
msgstr "周期分æžç®€ä»‹"
+msgid "Issue board focus mode"
+msgstr "议题看æ¿æ¨¡å¼"
+
+msgid "Issue boards with milestones"
+msgstr "议题看æ¿ä¸Žé‡Œç¨‹ç¢‘"
+
msgid "Issue events"
msgstr "议题事件"
+msgid "IssueBoards|Board"
+msgstr "看æ¿"
+
+msgid "IssueBoards|Boards"
+msgstr "看æ¿"
+
msgid "Issues"
msgstr "议题"
-msgid "Jobs"
-msgstr ""
-
msgid "LFSStatus|Disabled"
msgstr "åœç”¨"
@@ -766,12 +1142,21 @@ msgstr[0] "最近 %d 天"
msgid "Last Pipeline"
msgstr "最新æµæ°´çº¿"
-msgid "Last Update"
-msgstr "最åŽæ›´æ–°"
-
msgid "Last commit"
msgstr "最åŽæ交"
+msgid "Last edited %{date}"
+msgstr "最åŽä¿®æ”¹ %{date}"
+
+msgid "Last edited by %{name}"
+msgstr "最åŽä¿®æ”¹äºº %{name}"
+
+msgid "Last update"
+msgstr "最åŽæ›´æ–°"
+
+msgid "Last updated"
+msgstr "最åŽæ›´æ–°"
+
msgid "LastPushEvent|You pushed to"
msgstr "您推é€äº†"
@@ -784,6 +1169,9 @@ msgstr "了解更多"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "æµæ°´çº¿è®¡åˆ’文档"
+msgid "Leave"
+msgstr "离开"
+
msgid "Leave group"
msgstr "退出群组"
@@ -791,14 +1179,26 @@ msgid "Leave project"
msgstr "退出项目"
msgid "License"
-msgstr ""
+msgstr "许å¯åè®®"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "最多显示 %d 个事件"
+msgid "Lock"
+msgstr "é”定"
+
+msgid "Locked"
+msgstr "å·²é”定"
+
msgid "Locked Files"
-msgstr ""
+msgstr "å·²é”定文件"
+
+msgid "Login"
+msgstr "登录"
+
+msgid "Maximum git storage failures"
+msgstr "最大 git 存储失败"
msgid "Median"
msgstr "中ä½æ•°"
@@ -813,7 +1213,7 @@ msgid "Merge events"
msgstr "åˆå¹¶äº‹ä»¶"
msgid "Merge request"
-msgstr ""
+msgstr "åˆå¹¶è¯·æ±‚"
msgid "Messages"
msgstr "消æ¯"
@@ -827,6 +1227,12 @@ msgstr "监控"
msgid "More information is available|here"
msgstr "帮助文档"
+msgid "Multiple issue boards"
+msgstr "多个议题看æ¿"
+
+msgid "New Cluster"
+msgstr "新集群"
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新建议题"
@@ -843,27 +1249,42 @@ msgstr "新建目录"
msgid "New file"
msgstr "新建文件"
+msgid "New group"
+msgstr "新群组"
+
msgid "New issue"
msgstr "新建议题"
msgid "New merge request"
msgstr "新建åˆå¹¶è¯·æ±‚"
+msgid "New project"
+msgstr "新项目"
+
msgid "New schedule"
msgstr "新建计划"
msgid "New snippet"
msgstr "新建代ç ç‰‡æ®µ"
+msgid "New subgroup"
+msgstr "æ–°å­ç¾¤ç»„"
+
msgid "New tag"
msgstr "新建标签"
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr "没有为此项目存储容器镜åƒã€‚请按照上述说明添加一个。"
+
msgid "No repository"
msgstr "没有存储库"
msgid "No schedules"
msgstr "没有计划"
+msgid "None"
+msgstr "æ— "
+
msgid "Not available"
msgstr "æ•°æ®ä¸è¶³"
@@ -927,12 +1348,24 @@ msgstr "关注"
msgid "Notifications"
msgstr "通知"
+msgid "Number of access attempts"
+msgstr "å°è¯•è®¿é—®æ¬¡æ•°"
+
+msgid "Number of failures before backing off"
+msgstr "退出å‰çš„失败次数"
+
msgid "OfSearchInADropdown|Filter"
msgstr "筛选"
+msgid "Only project members can comment."
+msgstr "åªæœ‰é¡¹ç›®æˆå‘˜å¯ä»¥å‘表评论。"
+
msgid "OpenedNDaysAgo|Opened"
msgstr "开始于"
+msgid "Opens in a new window"
+msgstr "打开一个新窗å£"
+
msgid "Options"
msgstr "æ“作"
@@ -943,20 +1376,23 @@ msgid "Owner"
msgstr "所有者"
msgid "Pagination|Last »"
-msgstr ""
+msgstr "尾页 »"
msgid "Pagination|Next"
-msgstr ""
+msgstr "下一页"
msgid "Pagination|Prev"
-msgstr ""
+msgstr "上一页"
msgid "Pagination|« First"
-msgstr ""
+msgstr "« 首页"
msgid "Password"
msgstr "密ç "
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr "未ç»è®¸å¯çš„人将永远ä¸ä¼šæ”¶åˆ°é€šçŸ¥å¹¶ä¸”无法评论。"
+
msgid "Pipeline"
msgstr "æµæ°´çº¿"
@@ -970,7 +1406,7 @@ msgid "Pipeline Schedules"
msgstr "æµæ°´çº¿è®¡åˆ’"
msgid "Pipeline quota"
-msgstr ""
+msgstr "æµæ°´çº¿é…é¢"
msgid "PipelineCharts|Failed:"
msgstr "失败:"
@@ -1059,8 +1495,53 @@ msgstr "于阶段"
msgid "Preferences"
msgstr "å好设置"
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr "ç§äºº - å¿…é¡»å‘æ¯ä¸ªç”¨æˆ·æ˜Žç¡®æŽˆäºˆé¡¹ç›®è®¿é—®æƒé™ã€‚"
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr "ç§äºº - 群组åŠå…¶é¡¹ç›®åªèƒ½ç”±æˆå‘˜æŸ¥çœ‹ã€‚"
+
msgid "Profile"
-msgstr ""
+msgstr "用户信æ¯"
+
+msgid "Profiles|Account scheduled for removal."
+msgstr "å¸æˆ·åˆ é™¤è®¡åˆ’。"
+
+msgid "Profiles|Delete Account"
+msgstr "删除å¸æˆ·"
+
+msgid "Profiles|Delete account"
+msgstr "删除å¸æˆ·"
+
+msgid "Profiles|Delete your account?"
+msgstr "删除您的å¸æˆ·?"
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr "删除å¸æˆ·å…·æœ‰ä»¥ä¸‹æ•ˆæžœï¼š"
+
+msgid "Profiles|Invalid password"
+msgstr "密ç æ— æ•ˆ"
+
+msgid "Profiles|Invalid username"
+msgstr "用户å无效"
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr "键入您的 %{confirmationValue} 以确认:"
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr "您无æƒåˆ é™¤æ­¤ç”¨æˆ·ã€‚"
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr "您必须转移所有æƒæˆ–删除这些群组,然åŽæ‰èƒ½åˆ é™¤æ‚¨çš„å¸æˆ·ã€‚"
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr "您的å¸æˆ·ç›®å‰æ˜¯è¿™äº›ç¾¤ç»„的所有者:"
+
+msgid "Profiles|your account"
+msgstr "您的å¸æˆ·"
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr "项目 “%{project_name}†正在被删除。"
msgid "Project '%{project_name}' queued for deletion."
msgstr "项目 '%{project_name}' 已进入删除队列。"
@@ -1071,9 +1552,6 @@ msgstr "项目 '%{project_name}' 已创建æˆåŠŸã€‚"
msgid "Project '%{project_name}' was successfully updated."
msgstr "项目 '%{project_name}' 已更新完æˆã€‚"
-msgid "Project '%{project_name}' will be deleted."
-msgstr "项目 '%{project_name}' 将被删除。"
-
msgid "Project access must be granted explicitly to each user."
msgstr "项目访问æƒé™å¿…须明确授æƒç»™æ¯ä¸ªç”¨æˆ·ã€‚"
@@ -1116,8 +1594,29 @@ msgstr "阶段"
msgid "ProjectNetworkGraph|Graph"
msgstr "分支图"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr "è”系管ç†å‘˜æ›´æ”¹æ­¤è®¾ç½®ã€‚"
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr "åªæœ‰å·²ç­¾ç½²æ交æ‰å¯ä»¥æŽ¨é€åˆ°æ­¤å­˜å‚¨åº“。"
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr "此设置已应用于æœåŠ¡å™¨çº§åˆ«ï¼Œå¯ç”±ç®¡ç†å‘˜è¦†ç›–。"
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr "此设置应用于æœåŠ¡å™¨çº§åˆ«ï¼Œä½†å·²è¢«è¯¥é¡¹ç›®è¦†ç›–。"
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr "此设置将应用于所有项目,除éžè¢«ç®¡ç†å‘˜è¦†ç›–。"
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr "用户åªèƒ½é€šè¿‡è‡ªå·±å·²éªŒè¯çš„电å­é‚®ä»¶åœ°å€å°†æ交到此存储库中。"
+
+msgid "Projects"
+msgstr "项目"
+
msgid "ProjectsDropdown|Frequently visited"
-msgstr ""
+msgstr "ç»å¸¸è®¿é—®"
msgid "ProjectsDropdown|Loading projects"
msgstr "加载项目中"
@@ -1129,7 +1628,7 @@ msgid "ProjectsDropdown|Search your projects"
msgstr "æœç´¢æ‚¨çš„项目"
msgid "ProjectsDropdown|Something went wrong on our end."
-msgstr ""
+msgstr "å‘生了内部错误"
msgid "ProjectsDropdown|Sorry, no projects matched your search"
msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰æœç´¢åˆ°ç¬¦åˆæ¡ä»¶çš„项目"
@@ -1137,12 +1636,21 @@ msgstr "对ä¸èµ·ï¼Œæ²¡æœ‰æœç´¢åˆ°ç¬¦åˆæ¡ä»¶çš„项目"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "此功能需è¦æµè§ˆå™¨æ”¯æŒ localStorage"
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr "公开 - 群组和任何公共项目å¯ä»¥åœ¨æ²¡æœ‰ä»»ä½•èº«ä»½éªŒè¯çš„情况下查看。"
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr "公开 - 无需任何身份验è¯å³å¯è®¿é—®è¯¥é¡¹ç›®ã€‚"
+
msgid "Push Rules"
-msgstr ""
+msgstr "推é€è§„则"
msgid "Push events"
msgstr "推é€äº‹ä»¶"
+msgid "PushRule|Committer restriction"
+msgstr "æ交é™åˆ¶"
+
msgid "Read more"
msgstr "了解更多"
@@ -1156,7 +1664,7 @@ msgid "RefSwitcher|Tags"
msgstr "标签"
msgid "Registry"
-msgstr ""
+msgstr "注册表"
msgid "Related Commits"
msgstr "相关的æ交"
@@ -1206,6 +1714,12 @@ msgstr "还原此åˆå¹¶è¯·æ±‚"
msgid "SSH Keys"
msgstr "SSH 密钥"
+msgid "Save"
+msgstr "ä¿å­˜"
+
+msgid "Save changes"
+msgstr "ä¿å­˜ä¿®æ”¹"
+
msgid "Save pipeline schedule"
msgstr "ä¿å­˜æµæ°´çº¿è®¡åˆ’"
@@ -1213,7 +1727,7 @@ msgid "Schedule a new pipeline"
msgstr "新建æµæ°´çº¿è®¡åˆ’"
msgid "Schedules"
-msgstr ""
+msgstr "日程"
msgid "Scheduling Pipelines"
msgstr "æµæ°´çº¿è®¡åˆ’"
@@ -1221,15 +1735,21 @@ msgstr "æµæ°´çº¿è®¡åˆ’"
msgid "Search branches and tags"
msgstr "æœç´¢åˆ†æ”¯å’Œæ ‡ç­¾"
+msgid "Seconds before reseting failure information"
+msgstr "é‡ç½®å¤±è´¥ä¿¡æ¯ç­‰å¾…时间(秒)"
+
+msgid "Seconds to wait after a storage failure"
+msgstr "存储失败åŽç­‰å¾…时间(秒)"
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr "等待存储访问å°è¯•æ—¶é—´(秒)"
+
msgid "Select Archive Format"
msgstr "选择下载格å¼"
msgid "Select a timezone"
msgstr "选择时区"
-msgid "Select existing branch"
-msgstr "选择现有分支"
-
msgid "Select target branch"
msgstr "选择目标分支"
@@ -1255,10 +1775,10 @@ msgid "Settings"
msgstr "设置"
msgid "Show parent pages"
-msgstr ""
+msgstr "查看父页é¢"
msgid "Show parent subgroups"
-msgstr ""
+msgstr "查看群组中的å­ç¾¤ç»„"
msgid "Showing %d event"
msgid_plural "Showing %d events"
@@ -1267,101 +1787,116 @@ msgstr[0] "显示 %d 个事件"
msgid "Snippets"
msgstr "代ç ç‰‡æ®µ"
+msgid "Something went wrong on our end."
+msgstr "å‘生了错误。"
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr "å°è¯•æ›´æ”¹ ${this.issuableDisplayName(this.issuableType)} çš„é”定状æ€æ—¶å‘生错误"
+
+msgid "Something went wrong while fetching the projects."
+msgstr "拉å–项目时å‘生错误。"
+
+msgid "Something went wrong while fetching the registry list."
+msgstr "拉å–注册表列表时å‘生错误。"
+
+msgid "Sort by"
+msgstr "排åº"
+
msgid "SortOptions|Access level, ascending"
-msgstr ""
+msgstr "访问级别,å‡åºæŽ’列"
msgid "SortOptions|Access level, descending"
-msgstr ""
+msgstr "访问级别,é™åºæŽ’列"
msgid "SortOptions|Created date"
-msgstr ""
+msgstr "创建日期"
msgid "SortOptions|Due date"
-msgstr ""
+msgstr "截止日期"
msgid "SortOptions|Due later"
-msgstr ""
+msgstr "已截止"
msgid "SortOptions|Due soon"
-msgstr ""
+msgstr "å³å°†æˆªæ­¢"
msgid "SortOptions|Label priority"
-msgstr ""
+msgstr "标签优先"
msgid "SortOptions|Largest group"
-msgstr ""
+msgstr "最大群组"
msgid "SortOptions|Largest repository"
-msgstr ""
+msgstr "最大存储库"
msgid "SortOptions|Last created"
-msgstr ""
+msgstr "最新创建"
msgid "SortOptions|Last joined"
-msgstr ""
+msgstr "最新加入"
msgid "SortOptions|Last updated"
-msgstr ""
+msgstr "最新更新"
msgid "SortOptions|Least popular"
-msgstr ""
+msgstr "最ä¸å—欢迎"
msgid "SortOptions|Less weight"
-msgstr ""
+msgstr "最低æƒé‡"
msgid "SortOptions|Milestone"
-msgstr ""
+msgstr "里程碑"
msgid "SortOptions|Milestone due later"
-msgstr ""
+msgstr "里程碑截止日期"
msgid "SortOptions|Milestone due soon"
-msgstr ""
+msgstr "å³å°†æˆªæ­¢çš„里程碑"
msgid "SortOptions|More weight"
-msgstr ""
+msgstr "更大的æƒé‡"
msgid "SortOptions|Most popular"
-msgstr ""
+msgstr "最å—欢迎"
msgid "SortOptions|Name"
-msgstr ""
+msgstr "å称"
msgid "SortOptions|Name, ascending"
-msgstr ""
+msgstr "å称,å‡åºæŽ’列"
msgid "SortOptions|Name, descending"
-msgstr ""
+msgstr "å称,é™åºæŽ’列"
msgid "SortOptions|Oldest created"
-msgstr ""
+msgstr "最早的创建"
msgid "SortOptions|Oldest joined"
-msgstr ""
+msgstr "最早的加入"
msgid "SortOptions|Oldest sign in"
-msgstr ""
+msgstr "最早的登录"
msgid "SortOptions|Oldest updated"
-msgstr ""
+msgstr "最早的æ交"
msgid "SortOptions|Popularity"
-msgstr ""
+msgstr "人气"
msgid "SortOptions|Priority"
-msgstr ""
+msgstr "优先"
msgid "SortOptions|Recent sign in"
-msgstr ""
+msgstr "最近登录"
msgid "SortOptions|Start later"
-msgstr ""
+msgstr "ç¨åŽå¼€å§‹"
msgid "SortOptions|Start soon"
-msgstr ""
+msgstr "现在开始"
msgid "SortOptions|Weight"
-msgstr ""
+msgstr "æƒé‡"
msgid "Source code"
msgstr "æºä»£ç "
@@ -1376,7 +1911,7 @@ msgid "StarProject|Star"
msgstr "星标"
msgid "Starred projects"
-msgstr ""
+msgstr "已星标项目"
msgid "Start a %{new_merge_request} with these changes"
msgstr "由此更改 %{new_merge_request}"
@@ -1384,11 +1919,17 @@ msgstr "由此更改 %{new_merge_request}"
msgid "Start the Runner!"
msgstr "å¯åŠ¨ Runner!"
+msgid "Subgroups"
+msgstr "å­ç¾¤ç»„"
+
+msgid "Subscribe"
+msgstr "订阅"
+
msgid "Switch branch/tag"
msgstr "切æ¢åˆ†æ”¯/标签"
msgid "System Hooks"
-msgstr ""
+msgstr "系统钩å­"
msgid "Tag"
msgid_plural "Tags"
@@ -1403,6 +1944,15 @@ msgstr "目标分支"
msgid "Team"
msgstr "团队"
+msgid "Thanks! Don't show me this again"
+msgstr "谢谢 ! 请ä¸è¦å†æ˜¾ç¤º"
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr "GitLab 中的高级全局æœç´¢åŠŸèƒ½æ˜¯éžå¸¸å¼ºå¤§çš„æœç´¢æœåŠ¡ã€‚您å¯ä»¥æœç´¢å…¶ä»–团队的代ç ä»¥å¸®åŠ©æ‚¨å®Œå–„自己项目中的代ç ã€‚从而é¿å…创建é‡å¤çš„代ç æˆ–浪费时间。"
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+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 "ç¼–ç é˜¶æ®µæ¦‚述了从第一次æ交到创建åˆå¹¶è¯·æ±‚的时间。创建第一个åˆå¹¶è¯·æ±‚åŽï¼Œæ•°æ®å°†è‡ªåŠ¨æ·»åŠ åˆ°æ­¤å¤„。"
@@ -1415,6 +1965,15 @@ 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 number of attempts GitLab will make to access a storage."
+msgstr "GitLab 访问存储的次数。"
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr "GitLab 将临时ç¦ç”¨å¯¹ä¸»å­˜å‚¨åˆ†ç‰‡çš„访问"
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr "GitLab 将完全阻止访问存储的故障次数。å¯ä»¥åœ¨ç®¡ç†ç•Œé¢%{link_to_health_page}或使用%{api_documentation_link}é‡ç½®æ•…障次数。"
+
msgid "The phase of the development lifecycle."
msgstr "项目生命周期中的å„个阶段。"
@@ -1445,6 +2004,12 @@ 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 time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr "GitLab å°†ä¿æŒå¤±è´¥ä¿¡æ¯çš„时间(秒)。在此期间ä¸å‘生故障时,有关安装的信æ¯å°†é‡ç½®ã€‚"
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr "GitLab å°†å°è¯•è®¿é—®å­˜å‚¨çš„时间(秒)。在此时间之åŽå°†å¼•å‘超时错误。"
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "该阶段æ¯æ¡æ•°æ®æ‰€èŠ±çš„时间"
@@ -1454,12 +2019,27 @@ msgstr "中ä½æ•°æ˜¯ä¸€ä¸ªæ•°åˆ—中最中间的值。例如在 3ã€5ã€9 之间ï
msgid "There are problems accessing Git storage: "
msgstr "访问 Git 存储时出现问题:"
-msgid "This is the author's first Merge Request to this project. Handle with care."
-msgstr ""
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr "自您开始编辑åŽ, 此分支已更改。您想创建一个新的分支å—?"
+
+msgid "This is a confidential issue."
+msgstr "这是一个机密议题。"
+
+msgid "This is the author's first Merge Request to this project."
+msgstr "这是作者为项目贡献的第一个åˆå¹¶è¯·æ±‚。"
+
+msgid "This issue is confidential and locked."
+msgstr "这个是机密且已é”定的议题。"
+
+msgid "This issue is locked."
+msgstr "此议题已é”定。"
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "在创建一个空的存储库或导入现有存储库之å‰ï¼Œå°†æ— æ³•æŽ¨é€ä»£ç ã€‚"
+msgid "This merge request is locked."
+msgstr "æ­¤åˆå¹¶è¯·æ±‚å·²é”定。"
+
msgid "Time before an issue gets scheduled"
msgstr "议题被列入日程表的时间"
@@ -1538,9 +2118,6 @@ msgstr " 1 个月å‰"
msgid "Timeago|a week ago"
msgstr " 1 星期å‰"
-msgid "Timeago|a while"
-msgstr "刚刚"
-
msgid "Timeago|a year ago"
msgstr " 1 å¹´å‰"
@@ -1592,6 +2169,9 @@ msgstr " 1 星期åŽ"
msgid "Timeago|in 1 year"
msgstr " 1 å¹´åŽ"
+msgid "Timeago|in a while"
+msgstr "刚刚"
+
msgid "Timeago|less than a minute ago"
msgstr "ä¸åˆ° 1 分钟å‰"
@@ -1612,9 +2192,36 @@ msgstr "总时间"
msgid "Total test time for all commits/merges"
msgstr "所有æ交和åˆå¹¶çš„总测试时间"
+msgid "Track activity with Contribution Analytics."
+msgstr "跟踪分æžè´¡çŒ®ä¸Žæ´»åŠ¨ã€‚"
+
+msgid "Unlock"
+msgstr "解é”"
+
+msgid "Unlocked"
+msgstr "已解é”"
+
msgid "Unstar"
msgstr "å–消星标"
+msgid "Unsubscribe"
+msgstr "退订"
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr "å‡çº§æ‚¨çš„方案以å¯ç”¨é«˜çº§å…¨å±€æœç´¢ã€‚"
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr "å‡çº§æ‚¨çš„方案以å¯ç”¨è´¡çŒ®åˆ†æžã€‚"
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr "å‡çº§æ‚¨çš„方案以激活 Webhooks 。"
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr "å‡çº§æ‚¨çš„方案以激活议题æƒé‡ã€‚"
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr "å‡çº§æ‚¨çš„方案以使用议题看æ¿ã€‚"
+
msgid "Upload New File"
msgstr "上传新文件"
@@ -1631,13 +2238,13 @@ msgid "Use your global notification setting"
msgstr "使用全局通知设置"
msgid "View file @ "
-msgstr ""
+msgstr "æµè§ˆæ–‡ä»¶ @ "
msgid "View open merge request"
msgstr "查看待处ç†çš„åˆå¹¶è¯·æ±‚"
msgid "View replaced file @ "
-msgstr ""
+msgstr "查看替æ¢æ–‡ä»¶ @ "
msgid "VisibilityLevel|Internal"
msgstr "内部"
@@ -1657,9 +2264,120 @@ msgstr "æƒé™ä¸è¶³ã€‚如需查看相关数æ®ï¼Œè¯·å‘管ç†å‘˜ç”³è¯·æƒé™ã€‚
msgid "We don't have enough data to show this stage."
msgstr "该阶段的数æ®ä¸è¶³ï¼Œæ— æ³•æ˜¾ç¤ºã€‚"
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr "如果有新的推é€æˆ–新的议题,Webhook将自动触å‘您设置URL。 您å¯ä»¥é…ç½® Webhook æ¥ç›‘å¬ç‰¹å®šäº‹ä»¶ï¼Œå¦‚推é€ã€è®®é¢˜æˆ–åˆå¹¶è¯·æ±‚。 群组 Webhook 将适用于团队中的所有项目,并å…许您设置整个团队中的 Webhook 。"
+
+msgid "Weight"
+msgstr "æƒé‡"
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr "访问存储失败时。 GitLab 将在此处指定的时间内阻止对存储的访问。这å…许文件系统æ¢å¤ã€‚故障分片上的存储库暂时无法使用"
+
msgid "Wiki"
msgstr "Wiki"
+msgid "WikiClone|Clone your wiki"
+msgstr "克隆您的 wiki"
+
+msgid "WikiClone|Git Access"
+msgstr "Git 访问"
+
+msgid "WikiClone|Install Gollum"
+msgstr "安装 Gollum"
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr "建议安装 %{markdown},以便 GFM 功能在本地渲染:"
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr "å¯åŠ¨ Gollum 并在本地编辑"
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr "您ä¸èƒ½åˆ›å»º wiki 页é¢"
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr "这是此页é¢çš„过期版本。"
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr "您å¯ä»¥æŸ¥çœ‹ %{most_recent_link} 或æµè§ˆ %{history_link}。"
+
+msgid "WikiHistoricalPage|history"
+msgstr "历å²"
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr "最新版本"
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr "更多示例在 %{docs_link}"
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr "文档"
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr "è¦é“¾æŽ¥åˆ°(æ–°)页é¢ï¼Œåªéœ€é”®å…¥ %{link_example}"
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr "如何设置"
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr "æ示:您å¯ä»¥æŒ‡å®šæ–°æ–‡ä»¶çš„完整路径。我们将自动创建完整的目录。"
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr "æ–° Wiki 页é¢"
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr "确定è¦åˆ é™¤æ­¤é¡µé¢å—?"
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr "有人在åŒä¸€æ—¶é—´ç¼–辑了页é¢ã€‚请检查 %{page_link} 并确ä¿æ‚¨çš„更改ä¸ä¼šæ— æ„中删除。"
+
+msgid "WikiPageConflictMessage|the page"
+msgstr "这一页"
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr "创建 %{page_title}"
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr "æ›´æ–° %{page_title}"
+
+msgid "WikiPage|Page slug"
+msgstr "页é¢å—"
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr "在这里撰写您的内容或拖曳文件到此..."
+
+msgid "Wiki|Create Page"
+msgstr "创建页é¢"
+
+msgid "Wiki|Create page"
+msgstr "创建页é¢"
+
+msgid "Wiki|Edit Page"
+msgstr "修改页é¢"
+
+msgid "Wiki|Empty page"
+msgstr "空页é¢"
+
+msgid "Wiki|More Pages"
+msgstr "更多页é¢"
+
+msgid "Wiki|New page"
+msgstr "新页é¢"
+
+msgid "Wiki|Page history"
+msgstr "页é¢åŽ†å²"
+
+msgid "Wiki|Page version"
+msgstr "页é¢ç‰ˆæœ¬"
+
+msgid "Wiki|Pages"
+msgstr "页é¢"
+
+msgid "Wiki|Wiki Pages"
+msgstr "Wiki 页é¢"
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr "通过贡献分æžï¼Œæ‚¨å¯ä»¥åˆ†æžæ‚¨çš„组织åŠå…¶æˆå‘˜çš„议题〠åˆå¹¶è¯·æ±‚和推é€æ´»åŠ¨ã€‚"
+
msgid "Withdraw Access Request"
msgstr "å–消æƒé™ç”³è¯·"
@@ -1675,9 +2393,21 @@ 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 on a read-only GitLab instance."
+msgstr "您在一个åªè¯» GitLab 实例上。"
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr "您在一个åªè¯»çš„ GitLab 实例上。如果您想进行任何更改,您必须访问%{link_to_primary_node}。"
+
msgid "You can only add files when you are on a branch"
msgstr "åªèƒ½åœ¨åˆ†æ”¯ä¸Šæ·»åŠ æ–‡ä»¶"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr "您ä¸èƒ½å†™å…¥åªè¯»çš„辅助 GitLab Geo 实例。请改用%{link_to_primary_node}。"
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr "您ä¸èƒ½å†™å…¥è¿™ä¸ªåªè¯»çš„ GitLab 实例。"
+
msgid "You have reached your project limit"
msgstr "您已达到项目数é‡é™åˆ¶"
@@ -1708,14 +2438,20 @@ 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"
msgstr "在账å·ä¸­ %{add_ssh_key_link} 之å‰å°†æ— æ³•é€šè¿‡ SSH 拉å–或推é€ä»£ç ã€‚"
+msgid "Your comment will not be visible to the public."
+msgstr "您的评论将ä¸ä¼šå…¬å¼€æ˜¾ç¤ºã€‚"
+
+msgid "Your groups"
+msgstr "您的群组"
+
msgid "Your name"
msgstr "您的åå­—"
msgid "Your projects"
-msgstr ""
+msgstr "您的项目"
msgid "commit"
-msgstr ""
+msgstr "æ交"
msgid "day"
msgid_plural "days"
@@ -1731,3 +2467,15 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "父级"
+msgid "password"
+msgstr "密ç "
+
+msgid "personal access token"
+msgstr "个人访问令牌"
+
+msgid "to help your contributors communicate effectively!"
+msgstr "帮助您的贡献者进行有效沟通ï¼"
+
+msgid "username"
+msgstr "用户å"
+
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index c3b6cc72aed..afb40e8b75f 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:44-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:30-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Traditional, Hong Kong\n"
"Language: zh_HK\n"
@@ -20,6 +20,10 @@ msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] " %d 次æ交"
+msgid "%d layer"
+msgid_plural "%d layers"
+msgstr[0] ""
+
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 次æ交。"
@@ -27,6 +31,10 @@ msgstr[0] "為æ高é é¢åŠ è¼‰é€Ÿåº¦åŠæ€§èƒ½ï¼Œå·²çœç•¥äº† %s 次æ交。"
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "ç”± %{commit_author_link} æ交於 %{commit_timeago}"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -46,6 +54,12 @@ msgstr[0] "%{storage_name}:已訪å•æ­¤ä¸»æ©Ÿå¤±æ•— %{failed_attempts} 次"
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(想了解更多的安è£è¨Šæ¯è«‹æŸ¥çœ‹ %{link})"
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d æ¢æµæ°´ç·š"
@@ -53,6 +67,9 @@ msgstr[0] "%d æ¢æµæ°´ç·š"
msgid "1st contribution!"
msgstr ""
+msgid "2FA enabled"
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "相關æŒçºŒé›†æˆçš„圖åƒé›†åˆ"
@@ -77,12 +94,18 @@ msgstr "啟用"
msgid "Activity"
msgstr "活動"
+msgid "Add"
+msgstr ""
+
msgid "Add Changelog"
msgstr "添加更新日誌"
msgid "Add Contribution guide"
msgstr "添加貢ç»æŒ‡å—"
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Add License"
msgstr "添加許å¯è­‰"
@@ -92,9 +115,18 @@ msgstr "新增壹個用於推é€æˆ–拉å–çš„ SSH 秘鑰到賬號中。"
msgid "Add new directory"
msgstr "添加新目錄"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr "全部"
+msgid "An error occurred. Please try again."
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -110,6 +142,9 @@ msgstr "確定è¦åˆªé™¤æ­¤æµæ°´ç·šè¨ˆåŠƒå—Žï¼Ÿ"
msgid "Are you sure you want to discard your changes?"
msgstr "確定è¦æ”¾æ£„修改嗎?"
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "確定è¦é‡ç½®è¨»å†Šä»¤ç‰Œå—Žï¼Ÿ"
@@ -128,25 +163,34 @@ msgstr "拖放文件到此處或者 %{upload_link}"
msgid "Authentication Log"
msgstr ""
-msgid "Auto DevOps (Beta)"
+msgid "Author"
msgstr ""
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto DevOps documentation"
+msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
+msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
+msgid "AutoDevOps|Auto DevOps (Beta)"
msgstr ""
-msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgid "AutoDevOps|Auto DevOps documentation"
+msgstr ""
+
+msgid "AutoDevOps|Enable in settings"
+msgstr ""
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
+msgstr ""
+
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
-msgid "AutoDevOps|Learn more in the"
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
msgid "Billing"
@@ -164,6 +208,9 @@ msgstr ""
msgid "BillingPlans|Customer Support"
msgstr ""
+msgid "BillingPlans|Downgrade"
+msgstr ""
+
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
msgstr ""
@@ -200,12 +247,6 @@ msgstr ""
msgid "BillingPlans|per user"
msgstr ""
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
-
msgid "Branch"
msgid_plural "Branches"
msgstr[0] "分支"
@@ -213,6 +254,9 @@ 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 has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "æœç´¢åˆ†æ”¯"
@@ -327,12 +371,18 @@ msgstr ""
msgid "CI configuration"
msgstr "CI é…ç½®"
+msgid "CICD|Jobs"
+msgstr ""
+
msgid "Cancel"
msgstr "å–消"
msgid "Cancel edit"
msgstr "å–消编辑"
+msgid "Change Weight"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "挑é¸åˆ°åˆ†æ”¯"
@@ -360,6 +410,9 @@ msgstr "優é¸æ­¤æ交"
msgid "Cherry-pick this merge request"
msgstr "優é¸æ­¤åˆä½µè«‹æ±‚"
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr ""
+
msgid "CiStatusLabel|canceled"
msgstr "å·²å–消"
@@ -414,6 +467,147 @@ msgstr "已跳éŽ"
msgid "CiStatus|running"
msgstr "é‹è¡Œä¸­"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
+msgid "Clone repository"
+msgstr ""
+
+msgid "Close"
+msgstr ""
+
+msgid "Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Create cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr ""
+
+msgid "ClusterIntegration|Machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr ""
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
+msgstr ""
+
+msgid "ClusterIntegration|See your projects"
+msgstr ""
+
+msgid "ClusterIntegration|See zones"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr ""
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|cluster"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
msgid "Comments"
msgstr "è©•è«– (Comment)"
@@ -421,6 +615,13 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "æ交"
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+
+msgid "Commit Message"
+msgstr ""
+
msgid "Commit duration in minutes for last 30 commits"
msgstr "最近30次æ交花費的時間(分é˜ï¼‰"
@@ -448,12 +649,63 @@ msgstr "æ交者:"
msgid "Compare"
msgstr "比較"
+msgid "Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Created"
+msgstr ""
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr ""
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr ""
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Learn more about"
+msgstr ""
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr ""
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr ""
+
+msgid "ContainerRegistry|Remove repository"
+msgstr ""
+
+msgid "ContainerRegistry|Remove tag"
+msgstr ""
+
+msgid "ContainerRegistry|Size"
+msgstr ""
+
+msgid "ContainerRegistry|Tag"
+msgstr ""
+
+msgid "ContainerRegistry|Tag ID"
+msgstr ""
+
+msgid "ContainerRegistry|Use different image names"
+msgstr ""
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr ""
+
msgid "Contribution guide"
msgstr "è²¢ç»æŒ‡å—"
msgid "Contributors"
msgstr "è²¢ç»è€…"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
+msgstr ""
+
msgid "Copy SSH public key to clipboard"
msgstr ""
@@ -466,9 +718,6 @@ msgstr "複製æ交 SHA 到剪貼æ¿"
msgid "Create New Directory"
msgstr "創建新目錄"
-msgid "Create a new branch"
-msgstr "創建壹個新分支 (branch)"
-
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "在帳戶上創建個人訪å•ä»¤ç‰Œï¼Œä»¥é€šéŽ %{protocol} 來拉å–或推é€ã€‚"
@@ -478,9 +727,21 @@ msgstr "創建目錄"
msgid "Create empty bare repository"
msgstr "創建空的存儲庫"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "創建åˆä½µè«‹æ±‚"
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "創建..."
@@ -532,6 +793,12 @@ msgstr "é ç™¼å¸ƒ"
msgid "CycleAnalyticsStage|Test"
msgstr "測試"
+msgid "DashboardProjects|All"
+msgstr ""
+
+msgid "DashboardProjects|Personal"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr "使用 Cron 語法定義自定義模å¼"
@@ -548,6 +815,9 @@ msgstr ""
msgid "Description"
msgstr "æè¿°"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr ""
+
msgid "Details"
msgstr "詳情"
@@ -557,6 +827,12 @@ msgstr "目錄å稱"
msgid "Discard changes"
msgstr "放棄更改"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Dismiss Merge Request promotion"
+msgstr ""
+
msgid "Don't show again"
msgstr "ä¸å†é¡¯ç¤º"
@@ -596,9 +872,6 @@ msgstr "編輯 %{id} æµæ°´ç·šè¨ˆåŠƒ"
msgid "Emails"
msgstr ""
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr "全部"
@@ -629,12 +902,18 @@ msgstr "æ¯é€±åŸ·è¡Œï¼ˆå‘¨æ—¥æ·©æ™¨ 4 點)"
msgid "Explore projects"
msgstr ""
+msgid "Explore public groups"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr "無法變更所有者"
msgid "Failed to remove the pipeline schedule"
msgstr "無法刪除æµæ°´ç·šè¨ˆåŠƒ"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "文件"
@@ -660,6 +939,12 @@ msgstr[0] "派生"
msgid "ForkedFromProjectPath|Forked from"
msgstr "派生自"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr ""
+
+msgid "Format"
+msgstr ""
+
msgid "From issue creation until deploy to production"
msgstr "從創建議題到部署到生產環境"
@@ -672,6 +957,18 @@ msgstr ""
msgid "Geo Nodes"
msgstr ""
+msgid "Geo|File sync capacity"
+msgstr ""
+
+msgid "Geo|Groups to replicate"
+msgstr ""
+
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
+msgid "Geo|Select groups to replicate."
+msgstr ""
+
msgid "Git storage health information has been reset"
msgstr "Git 存儲å¥åº·ä¿¡æ¯å·²é‡ç½®"
@@ -684,6 +981,9 @@ msgstr "跳轉到派生項目"
msgid "GoToYourFork|Fork"
msgstr "跳轉到派生項目"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -708,6 +1008,51 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
+msgstr ""
+
msgid "Health Check"
msgstr "å¥åº·æª¢æŸ¥ (Health Check)"
@@ -726,28 +1071,59 @@ msgstr "沒有檢測到å¥åº·å•é¡Œ"
msgid "HealthCheck|Unhealthy"
msgstr "ä¸è‰¯"
+msgid "History"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "已開始維護"
msgid "Import repository"
msgstr "導入存儲庫"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr ""
+
msgid "Install a Runner compatible with GitLab CI"
msgstr "安è£å£¹å€‹èˆ‡ GitLab CI 兼容的 Runner"
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] ""
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "循環週期"
msgid "Introducing Cycle Analytics"
msgstr "週期分æžç°¡ä»‹"
+msgid "Issue board focus mode"
+msgstr ""
+
+msgid "Issue boards with milestones"
+msgstr ""
+
msgid "Issue events"
msgstr "議題事件 (issue event)"
-msgid "Issues"
+msgid "IssueBoards|Board"
msgstr ""
-msgid "Jobs"
+msgid "IssueBoards|Boards"
+msgstr ""
+
+msgid "Issues"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -766,12 +1142,21 @@ msgstr[0] "最近 %d 天"
msgid "Last Pipeline"
msgstr "最新æµæ°´ç·š"
-msgid "Last Update"
-msgstr "最後更新"
-
msgid "Last commit"
msgstr "最後æ交"
+msgid "Last edited %{date}"
+msgstr ""
+
+msgid "Last edited by %{name}"
+msgstr ""
+
+msgid "Last update"
+msgstr ""
+
+msgid "Last updated"
+msgstr ""
+
msgid "LastPushEvent|You pushed to"
msgstr "您推é€äº†"
@@ -784,6 +1169,9 @@ msgstr "了解更多"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "æµæ°´ç·šè¨ˆåŠƒæ–‡æª”"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "退出群組"
@@ -797,9 +1185,21 @@ msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "最多顯示 %d 個事件"
+msgid "Lock"
+msgstr ""
+
+msgid "Locked"
+msgstr ""
+
msgid "Locked Files"
msgstr ""
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
+msgstr ""
+
msgid "Median"
msgstr "中ä½æ•¸"
@@ -827,6 +1227,12 @@ msgstr ""
msgid "More information is available|here"
msgstr "幫助文檔"
+msgid "Multiple issue boards"
+msgstr ""
+
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新建議題"
@@ -843,27 +1249,42 @@ msgstr "新增目錄"
msgid "New file"
msgstr "新增文件"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "新議題"
msgid "New merge request"
msgstr "新增åˆä½µè«‹æ±‚"
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "新增计划"
msgid "New snippet"
msgstr "新代碼片段"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "新增標籤"
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr ""
+
msgid "No repository"
msgstr "沒有存儲庫"
msgid "No schedules"
msgstr "沒有計劃"
+msgid "None"
+msgstr ""
+
msgid "Not available"
msgstr "ä¸å¯ç”¨"
@@ -927,12 +1348,24 @@ msgstr "關注"
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "篩é¸"
+msgid "Only project members can comment."
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "開始於"
+msgid "Opens in a new window"
+msgstr ""
+
msgid "Options"
msgstr "æ“作"
@@ -957,6 +1390,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr ""
+
msgid "Pipeline"
msgstr "æµæ°´ç·š"
@@ -1059,9 +1495,54 @@ msgstr "於階段"
msgid "Preferences"
msgstr ""
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
msgstr ""
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "項目 '%{project_name}' 已進入刪除隊列。"
@@ -1071,9 +1552,6 @@ msgstr "é …ç›® '%{project_name}' 已創建æˆåŠŸã€‚"
msgid "Project '%{project_name}' was successfully updated."
msgstr "é …ç›® '%{project_name}' 已更新完æˆã€‚"
-msgid "Project '%{project_name}' will be deleted."
-msgstr "項目 '%{project_name}' 將被刪除。"
-
msgid "Project access must be granted explicitly to each user."
msgstr "項目訪å•æ¬Šé™å¿…須明確授權給æ¯å€‹ç”¨æˆ¶ã€‚"
@@ -1116,6 +1594,27 @@ msgstr "階段"
msgid "ProjectNetworkGraph|Graph"
msgstr "分支圖"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr ""
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr ""
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr ""
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
+msgstr ""
+
+msgid "Projects"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -1137,12 +1636,21 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
+msgstr ""
+
msgid "Push Rules"
msgstr ""
msgid "Push events"
msgstr "推é€äº‹ä»¶ (push event) "
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "了解更多"
@@ -1206,6 +1714,12 @@ msgstr "還原此åˆä½µè«‹æ±‚"
msgid "SSH Keys"
msgstr ""
+msgid "Save"
+msgstr ""
+
+msgid "Save changes"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "ä¿å­˜æµæ°´ç·šè¨ˆåŠƒ"
@@ -1221,15 +1735,21 @@ msgstr "æµæ°´ç·šè¨ˆåŠƒ"
msgid "Search branches and tags"
msgstr "æœç´¢åˆ†æ”¯å’Œæ¨™ç±¤"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "é¸æ“‡ä¸‹è¼‰æ ¼å¼"
msgid "Select a timezone"
msgstr "é¸æ“‡æ™‚å€"
-msgid "Select existing branch"
-msgstr "é¸æ“‡ç¾æœ‰åˆ†æ”¯ (branch)"
-
msgid "Select target branch"
msgstr "é¸æ“‡ç›®æ¨™åˆ†æ”¯"
@@ -1267,6 +1787,21 @@ msgstr[0] "顯示 %d 個事件"
msgid "Snippets"
msgstr ""
+msgid "Something went wrong on our end."
+msgstr ""
+
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr ""
+
+msgid "Something went wrong while fetching the projects."
+msgstr ""
+
+msgid "Something went wrong while fetching the registry list."
+msgstr ""
+
+msgid "Sort by"
+msgstr ""
+
msgid "SortOptions|Access level, ascending"
msgstr ""
@@ -1384,6 +1919,12 @@ msgstr "由此更改 %{new_merge_request}"
msgid "Start the Runner!"
msgstr "é‹ä½œ Runner!"
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "切æ›åˆ†æ”¯/標籤"
@@ -1403,6 +1944,15 @@ msgstr "目標分支"
msgid "Team"
msgstr "團隊"
+msgid "Thanks! Don't show me this again"
+msgstr ""
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr ""
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+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 "編碼階段概述了從第壹次æ交到創建åˆä½µè«‹æ±‚的時間。創建第壹個åˆä½µè«‹æ±‚後,數據將自動添加到此處。"
@@ -1415,6 +1965,15 @@ 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 number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "項目生命週期中的å„個階段。"
@@ -1445,6 +2004,12 @@ 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 time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "該階段æ¯æ¢æ•¸æ“šæ‰€èŠ±çš„時間"
@@ -1454,12 +2019,27 @@ msgstr "中ä½æ•¸æ˜¯å£¹å€‹æ•¸åˆ—中最中間的值。例如在 3ã€5ã€9 之間ï
msgid "There are problems accessing Git storage: "
msgstr "è¨ªå• Git 存儲時出ç¾å•é¡Œï¼š"
-msgid "This is the author's first Merge Request to this project. Handle with care."
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
+msgid "This is a confidential issue."
+msgstr ""
+
+msgid "This is the author's first Merge Request to this project."
+msgstr ""
+
+msgid "This issue is confidential and locked."
+msgstr ""
+
+msgid "This issue is locked."
msgstr ""
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "在創建壹個空的存儲庫或導入ç¾æœ‰å­˜å„²åº«ä¹‹å‰ï¼Œæ‚¨å°‡ç„¡æ³•æŽ¨é€ä»£ç¢¼ã€‚"
+msgid "This merge request is locked."
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr "議題被列入日程表的時間"
@@ -1538,9 +2118,6 @@ msgstr " 1 個月å‰"
msgid "Timeago|a week ago"
msgstr " 1 星期å‰"
-msgid "Timeago|a while"
-msgstr " 剛剛"
-
msgid "Timeago|a year ago"
msgstr " 1 å¹´å‰"
@@ -1592,6 +2169,9 @@ msgstr " 1 星期後"
msgid "Timeago|in 1 year"
msgstr " 1 年後"
+msgid "Timeago|in a while"
+msgstr ""
+
msgid "Timeago|less than a minute ago"
msgstr "ä¸åˆ° 1 分é˜å‰"
@@ -1612,9 +2192,36 @@ msgstr "總時間"
msgid "Total test time for all commits/merges"
msgstr "所有æ交和åˆä½µçš„總測試時間"
+msgid "Track activity with Contribution Analytics."
+msgstr ""
+
+msgid "Unlock"
+msgstr ""
+
+msgid "Unlocked"
+msgstr ""
+
msgid "Unstar"
msgstr "å–消星標"
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr ""
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr ""
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr ""
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr ""
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr ""
+
msgid "Upload New File"
msgstr "上傳新文件"
@@ -1657,9 +2264,120 @@ msgstr "權é™ä¸è¶³ã€‚如需查看相關數據,請å‘管ç†å“¡ç”³è«‹æ¬Šé™ã€‚
msgid "We don't have enough data to show this stage."
msgstr "該階段的數據ä¸è¶³ï¼Œç„¡æ³•é¡¯ç¤ºã€‚"
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr ""
+
+msgid "Weight"
+msgstr ""
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr ""
+msgid "WikiClone|Clone your wiki"
+msgstr ""
+
+msgid "WikiClone|Git Access"
+msgstr ""
+
+msgid "WikiClone|Install Gollum"
+msgstr ""
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr ""
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr ""
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr ""
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr ""
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr ""
+
+msgid "WikiHistoricalPage|history"
+msgstr ""
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr ""
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr ""
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr ""
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr ""
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr ""
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr ""
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr ""
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr ""
+
+msgid "WikiPageConflictMessage|the page"
+msgstr ""
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr ""
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr ""
+
+msgid "WikiPage|Page slug"
+msgstr ""
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr ""
+
+msgid "Wiki|Create Page"
+msgstr ""
+
+msgid "Wiki|Create page"
+msgstr ""
+
+msgid "Wiki|Edit Page"
+msgstr ""
+
+msgid "Wiki|Empty page"
+msgstr ""
+
+msgid "Wiki|More Pages"
+msgstr ""
+
+msgid "Wiki|New page"
+msgstr ""
+
+msgid "Wiki|Page history"
+msgstr ""
+
+msgid "Wiki|Page version"
+msgstr ""
+
+msgid "Wiki|Pages"
+msgstr ""
+
+msgid "Wiki|Wiki Pages"
+msgstr ""
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr ""
+
msgid "Withdraw Access Request"
msgstr "å–消權é™ç”³è¯·"
@@ -1675,9 +2393,21 @@ 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 on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "åªèƒ½åœ¨åˆ†æ”¯ä¸Šæ·»åŠ æ–‡ä»¶"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "您已é”到項目數é‡é™åˆ¶"
@@ -1708,6 +2438,12 @@ 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"
msgstr "在賬號中 %{add_ssh_key_link} 之å‰å°‡ç„¡æ³•é€šéŽ SSH 拉å–或推é€ä»£ç¢¼ã€‚"
+msgid "Your comment will not be visible to the public."
+msgstr ""
+
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "您的åå­—"
@@ -1731,3 +2467,15 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "父級"
+msgid "password"
+msgstr ""
+
+msgid "personal access token"
+msgstr ""
+
+msgid "to help your contributors communicate effectively!"
+msgstr ""
+
+msgid "username"
+msgstr ""
+
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index 8a14cd01566..efef86671f0 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-09-27 16:26+0200\n"
-"PO-Revision-Date: 2017-09-27 13:44-0400\n"
+"POT-Creation-Date: 2017-11-02 14:42+0100\n"
+"PO-Revision-Date: 2017-11-03 12:30-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Traditional\n"
"Language: zh_TW\n"
@@ -20,6 +20,10 @@ msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d 個更動 (commit)"
+msgid "%d layer"
+msgid_plural "%d layers"
+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 個更動 (commit)。"
@@ -27,8 +31,12 @@ msgstr[0] "因效能考é‡ï¼Œä¸é¡¯ç¤º %s 個更動 (commit)。"
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} 在 %{commit_timeago} é€äº¤"
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
-msgstr ""
+msgstr "%{number_commits_behind} 個è½å¾Œ %{default_branch} 分支的修訂版æ交,%{number_commits_ahead} 個超å‰çš„修訂版æ交"
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
msgstr "已失敗 %{number_of_failures} 次,在失敗 %{maximum_failures} æ¬¡å‰ GitLab 會é‡è©¦ã€‚"
@@ -46,12 +54,21 @@ msgstr[0] "%{storage_name}:已存å–此主機失敗 %{failed_attempts} 次"
msgid "(checkout the %{link} for information on how to install it)."
msgstr "(如何安è£è«‹åƒé–± %{link})"
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "%d æ¢æµæ°´ç·š"
msgid "1st contribution!"
-msgstr ""
+msgstr "第一次å”作"
+
+msgid "2FA enabled"
+msgstr "已啟用雙é‡èªè­‰"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "æŒçºŒæ•´åˆ (CI) 相關的圖表"
@@ -60,10 +77,10 @@ msgid "About auto deploy"
msgstr "關於自動部署"
msgid "Abuse Reports"
-msgstr ""
+msgstr "濫用報告"
msgid "Access Tokens"
-msgstr ""
+msgstr "å­˜å–憑證 (access token)"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr "已暫時åœç”¨å¤±æ•—çš„ Git 儲存空間。當儲存空間æ¢å¾©æ­£å¸¸å¾Œï¼Œè«‹é‡ç½®å„²å­˜ç©ºé–“å¥åº·æŒ‡æ•¸ã€‚"
@@ -77,12 +94,18 @@ msgstr "啟用"
msgid "Activity"
msgstr "活動"
+msgid "Add"
+msgstr "增加"
+
msgid "Add Changelog"
msgstr "新增更新日誌"
msgid "Add Contribution guide"
msgstr "新增å”作指å—"
+msgid "Add Group Webhooks and GitLab Enterprise Edition."
+msgstr "加入來自 Webhooks 或者是 GitHub ä¼æ¥­ç‰ˆçš„團隊"
+
msgid "Add License"
msgstr "新增授權æ¢æ¬¾"
@@ -92,12 +115,21 @@ msgstr "請先新增 SSH 金鑰到您的個人帳號,æ‰èƒ½ä½¿ç”¨ SSH 來上å‚
msgid "Add new directory"
msgstr "新增目錄"
+msgid "AdminHealthPageLink|health page"
+msgstr ""
+
+msgid "Advanced settings"
+msgstr ""
+
msgid "All"
msgstr "全部"
-msgid "Appearance"
+msgid "An error occurred. Please try again."
msgstr ""
+msgid "Appearance"
+msgstr "外觀"
+
msgid "Applications"
msgstr "應用程å¼"
@@ -110,6 +142,9 @@ msgstr "確定è¦åˆªé™¤æ­¤æµæ°´ç·š (pipeline) 排程嗎?"
msgid "Are you sure you want to discard your changes?"
msgstr "確定è¦æ”¾æ£„修改嗎?"
+msgid "Are you sure you want to leave this group?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr "確定è¦é‡ç½®è¨»å†Šæ†‘è­‰ (registration token) 嗎?"
@@ -120,91 +155,97 @@ msgid "Are you sure?"
msgstr "確定嗎?"
msgid "Artifacts"
-msgstr ""
+msgstr "產物"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放檔案到此處或者 %{upload_link}"
msgid "Authentication Log"
-msgstr ""
-
-msgid "Auto DevOps (Beta)"
-msgstr ""
+msgstr "é©—è­‰ Log"
-msgid "Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
-msgstr ""
-
-msgid "Auto DevOps documentation"
-msgstr ""
+msgid "Author"
+msgstr "作者"
msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly."
-msgstr ""
+msgstr "è‡ªå‹•å¯©æŸ¥ç¨‹åº & 自動部屬程åºéœ€è¦ä¸€å€‹ç¶²åŸŸå’Œ %{kubernetes} æ‰èƒ½é‹ä½œã€‚"
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
-msgstr ""
+msgstr "è‡ªå‹•å¯©æŸ¥ç¨‹åº & 自動部屬程åºéœ€è¦ä¸€å€‹ç¶²åŸŸæ‰èƒ½æ­£å¸¸é‹ä½œã€‚"
msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly."
+msgstr "è‡ªå‹•å¯©æŸ¥ç¨‹åº & 自動部屬程åºéœ€è¦ %{kubernetes} æ‰èƒ½æ­£å¸¸é‹ä½œã€‚"
+
+msgid "AutoDevOps|Auto DevOps (Beta)"
+msgstr "DevOps 自動化 (測試版)"
+
+msgid "AutoDevOps|Auto DevOps documentation"
+msgstr "「DevOps 自動化〠文件"
+
+msgid "AutoDevOps|Enable in settings"
+msgstr "在設定中啟用"
+
+msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration."
msgstr ""
-msgid "AutoDevOps|Learn more in the"
+msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
+msgstr "了解更多於 %{link_to_documentation}"
+
+msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
msgid "Billing"
-msgstr ""
+msgstr "方案"
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr ""
+msgstr "%{group_name} ç›®å‰ä½¿ç”¨ %{plan_link} 方案。"
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr ""
+msgstr "ç›®å‰ç„¡æ³•è‡ªå‹•å‡ç´šæˆ–é™ç´šè‡³å…¶ä»–方案。"
msgid "BillingPlans|Current plan"
-msgstr ""
+msgstr "ç›®å‰æ–¹æ¡ˆ"
msgid "BillingPlans|Customer Support"
-msgstr ""
+msgstr "客戶æœå‹™"
+
+msgid "BillingPlans|Downgrade"
+msgstr "é™ç´š"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr ""
+msgstr "了解更多我們的方案,或是閱讀 %{faq_link}"
msgid "BillingPlans|Manage plan"
-msgstr ""
+msgstr "管ç†æ–¹æ¡ˆ"
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr ""
+msgstr "è«‹è¯ç¹« %{customer_support_link}"
msgid "BillingPlans|See all %{plan_name} features"
-msgstr ""
+msgstr "查看更多 %{plan_name} 功能"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr ""
+msgstr "此群組使用和上層群組相åŒçš„方案。"
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr ""
+msgstr "è¦ç®¡ç†é€™å€‹ç¾¤çµ„的方案,請ç€è¦½ %{parent_billing_page_link} 的計費部分。"
msgid "BillingPlans|Upgrade"
-msgstr ""
+msgstr "å‡ç´š"
msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr ""
+msgstr "ä½ ç›®å‰æ­£åœ¨ä½¿ç”¨æ–¹æ¡ˆ %{plan_link}"
msgid "BillingPlans|frequently asked questions"
-msgstr ""
+msgstr "常見å•é¡Œ"
msgid "BillingPlans|monthly"
-msgstr ""
+msgstr "æ¯å€‹æœˆ"
msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr ""
+msgstr "æ¯å¹´æ”¯ä»˜ %{price_per_year}"
msgid "BillingPlans|per user"
-msgstr ""
-
-msgid "Billinglans|Downgrade"
-msgstr ""
-
-msgid "Board"
-msgstr ""
+msgstr "æ¯å€‹ä½¿ç”¨è€…"
msgid "Branch"
msgid_plural "Branches"
@@ -213,6 +254,9 @@ 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 has changed"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "æœå°‹åˆ†æ”¯ (branches)"
@@ -223,88 +267,88 @@ msgid "Branches"
msgstr "分支 (branch) "
msgid "Branches|Cant find HEAD commit for this branch"
-msgstr ""
+msgstr "ä¸èƒ½æ‰¾åˆ°é€™å€‹åˆ†æ”¯çš„ HEAD 更動。"
msgid "Branches|Compare"
-msgstr ""
+msgstr "比較"
msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
-msgstr ""
+msgstr "移除所有已經åˆä½µåˆ° %{default_branch} 的分支。"
msgid "Branches|Delete branch"
-msgstr ""
+msgstr "移除分支"
msgid "Branches|Delete merged branches"
-msgstr ""
+msgstr "移除已經åˆä½µçš„分支"
msgid "Branches|Delete protected branch"
-msgstr ""
+msgstr "移除å—ä¿è­·çš„分支"
msgid "Branches|Delete protected branch '%{branch_name}'?"
-msgstr ""
+msgstr "確定è¦ç§»é™¤ã€Œ%{branch_name}ã€é€™å€‹å—ä¿è­·çš„分支嗎 ?"
msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
-msgstr ""
+msgstr "移除 %{branch_name} 分支將無法還原,你確定?"
msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
-msgstr ""
+msgstr "移除已經åˆä½µçš„分支後將無法還原,你確定?"
msgid "Branches|Filter by branch name"
-msgstr ""
+msgstr "按分支å稱篩é¸"
msgid "Branches|Merged into %{default_branch}"
-msgstr ""
+msgstr "åˆä½µåˆ° %{default_branch}"
msgid "Branches|New branch"
-msgstr ""
+msgstr "新增分支"
msgid "Branches|No branches to show"
-msgstr ""
+msgstr "找ä¸åˆ°åˆ†æ”¯"
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
-msgstr ""
+msgstr "當你確èªä¸¦æŒ‰ä¸‹ %{delete_protected_branch},他將無法撤銷或還原。"
msgid "Branches|Only a project master or owner can delete a protected branch"
-msgstr ""
+msgstr "åªæœ‰å°ˆæ¡ˆç®¡ç†è€…或æ“有者æ‰èƒ½åˆªé™¤è¢«ä¿è­·çš„分支。"
msgid "Branches|Protected branches can be managed in %{project_settings_link}"
-msgstr ""
+msgstr "在 %{project_settings_link} 管ç†å—ä¿è­·çš„分支"
msgid "Branches|Sort by"
-msgstr ""
+msgstr "排åºè‡ª"
msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
-msgstr ""
+msgstr "分支無法自動é€äº¤ï¼Œå› ç‚ºèˆ‡ä¸Šæ¸¸åˆ†æ”¯è¡çªã€‚"
msgid "Branches|The default branch cannot be deleted"
-msgstr ""
+msgstr "無法刪除é è¨­åˆ†æ”¯"
msgid "Branches|This branch hasn’t been merged into %{default_branch}."
-msgstr ""
+msgstr "這個分支尚未åˆä½µåˆ° %{default_branch}"
msgid "Branches|To avoid data loss, consider merging this branch before deleting it."
-msgstr ""
+msgstr "為é¿å…資料丟失,請在刪除之å‰åˆä½µè©²åˆ†æ”¯ã€‚"
msgid "Branches|To confirm, type %{branch_name_confirmation}:"
-msgstr ""
+msgstr "為了確èªï¼Œè«‹è¼¸å…¥ %{branch_name_confirmation} :"
msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
-msgstr ""
+msgstr "為了æ¨æ£„ç›®å‰è®Šæ›´ä¸¦ä½¿ç”¨ä¸Šæ¸¸ç‰ˆæœ¬è¦†è“‹æœ¬åˆ†æ”¯ï¼Œè«‹å…ˆåˆªé™¤ä¸¦æŒ‰ä¸‹ \"立刻更新\"。"
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
-msgstr ""
+msgstr "ä½ å°‡è¦æ°¸ä¹…刪除å—ä¿è­·çš„ %{branch_name} 分支。"
msgid "Branches|diverged from upstream"
-msgstr ""
+msgstr "與上游存在差異"
msgid "Branches|merged"
-msgstr ""
+msgstr "å·²åˆä½µ"
msgid "Branches|project settings"
-msgstr ""
+msgstr "專案設定"
msgid "Branches|protected"
-msgstr ""
+msgstr "å—ä¿è­·çš„"
msgid "Browse Directory"
msgstr "ç€è¦½ç›®éŒ„"
@@ -322,17 +366,23 @@ msgid "ByAuthor|by"
msgstr "作者:"
msgid "CI / CD"
-msgstr ""
+msgstr "CI / CD"
msgid "CI configuration"
msgstr "CI 組態"
+msgid "CICD|Jobs"
+msgstr "作業"
+
msgid "Cancel"
msgstr "å–消"
msgid "Cancel edit"
msgstr "å–消編輯"
+msgid "Change Weight"
+msgstr "變更權é‡"
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "挑é¸åˆ°åˆ†æ”¯ (branch) "
@@ -352,7 +402,7 @@ msgid "Charts"
msgstr "統計圖"
msgid "Chat"
-msgstr ""
+msgstr "å³æ™‚通訊"
msgid "Cherry-pick this commit"
msgstr "挑é¸æ­¤æ›´å‹•è¨˜éŒ„ (commit) "
@@ -360,6 +410,9 @@ msgstr "挑é¸æ­¤æ›´å‹•è¨˜éŒ„ (commit) "
msgid "Cherry-pick this merge request"
msgstr "挑é¸æ­¤åˆä½µè«‹æ±‚ (merge request) "
+msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
+msgstr "é¸æ“‡å“ªå€‹ç¾¤çµ„是你è¦è¤‡è£½åˆ°ç¬¬äºŒç¯€é»žçš„。留白則複製全部。"
+
msgid "CiStatusLabel|canceled"
msgstr "å·²å–消"
@@ -414,6 +467,147 @@ msgstr "已跳éŽ"
msgid "CiStatus|running"
msgstr "執行中"
+msgid "CircuitBreakerApiLink|circuitbreaker api"
+msgstr ""
+
+msgid "Clone repository"
+msgstr "克隆倉庫"
+
+msgid "Close"
+msgstr "關閉"
+
+msgid "Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster details"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is disabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgstr ""
+
+msgid "ClusterIntegration|Cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Copy cluster name"
+msgstr ""
+
+msgid "ClusterIntegration|Create cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Enable cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Container Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
+msgstr ""
+
+msgid "ClusterIntegration|Machine type"
+msgstr ""
+
+msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
+msgstr ""
+
+msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgstr ""
+
+msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
+msgstr ""
+
+msgid "ClusterIntegration|Number of nodes"
+msgstr ""
+
+msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgstr ""
+
+msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
+msgstr ""
+
+msgid "ClusterIntegration|Remove cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Remove integration"
+msgstr ""
+
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project."
+msgstr ""
+
+msgid "ClusterIntegration|See and edit the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|See machine types"
+msgstr ""
+
+msgid "ClusterIntegration|See your projects"
+msgstr ""
+
+msgid "ClusterIntegration|See zones"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong on our end."
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Toggle Cluster"
+msgstr ""
+
+msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
+msgstr ""
+
+msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgstr ""
+
+msgid "ClusterIntegration|Zone"
+msgstr ""
+
+msgid "ClusterIntegration|access to Google Container Engine"
+msgstr ""
+
+msgid "ClusterIntegration|cluster"
+msgstr ""
+
+msgid "ClusterIntegration|help page"
+msgstr ""
+
+msgid "ClusterIntegration|meets the requirements"
+msgstr ""
+
+msgid "ClusterIntegration|properly configured"
+msgstr ""
+
msgid "Comments"
msgstr "留言"
@@ -421,6 +615,13 @@ msgid "Commit"
msgid_plural "Commits"
msgstr[0] "更動記錄 (commit) "
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+
+msgid "Commit Message"
+msgstr "更動訊æ¯"
+
msgid "Commit duration in minutes for last 30 commits"
msgstr "最近 30 次更動花費的時間(分é˜ï¼‰"
@@ -448,15 +649,66 @@ msgstr "é€äº¤è€…為 "
msgid "Compare"
msgstr "比較"
+msgid "Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Created"
+msgstr ""
+
+msgid "ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:"
+msgstr ""
+
+msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:"
+msgstr ""
+
+msgid "ContainerRegistry|How to use the Container Registry"
+msgstr ""
+
+msgid "ContainerRegistry|Learn more about"
+msgstr ""
+
+msgid "ContainerRegistry|No tags in Container Registry for this container image."
+msgstr ""
+
+msgid "ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands"
+msgstr ""
+
+msgid "ContainerRegistry|Remove repository"
+msgstr ""
+
+msgid "ContainerRegistry|Remove tag"
+msgstr ""
+
+msgid "ContainerRegistry|Size"
+msgstr ""
+
+msgid "ContainerRegistry|Tag"
+msgstr ""
+
+msgid "ContainerRegistry|Tag ID"
+msgstr ""
+
+msgid "ContainerRegistry|Use different image names"
+msgstr ""
+
+msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
+msgstr ""
+
msgid "Contribution guide"
msgstr "å”作指å—"
msgid "Contributors"
msgstr "å”作者"
-msgid "Copy SSH public key to clipboard"
+msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Control the maximum concurrency of repository backfill for this secondary node"
msgstr ""
+msgid "Copy SSH public key to clipboard"
+msgstr "複製 SSH 公鑰到剪貼簿"
+
msgid "Copy URL to clipboard"
msgstr "複製網å€åˆ°å‰ªè²¼ç°¿"
@@ -466,9 +718,6 @@ msgstr "複製更動記錄 (commit) 的 SHA 值到剪貼簿"
msgid "Create New Directory"
msgstr "建立新目錄"
-msgid "Create a new branch"
-msgstr "建立新分支 (branch)"
-
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "建立個人存å–憑證 (access token) 以使用 %{protocol} 來上傳 (push) 或下載 (pull) 。"
@@ -478,9 +727,21 @@ msgstr "建立目錄"
msgid "Create empty bare repository"
msgstr "建立一個新的 bare repository"
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr "發出åˆä½µè«‹æ±‚ (merge request) "
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr "建立..."
@@ -532,6 +793,12 @@ msgstr "試營é‹"
msgid "CycleAnalyticsStage|Test"
msgstr "測試"
+msgid "DashboardProjects|All"
+msgstr "全部"
+
+msgid "DashboardProjects|Personal"
+msgstr "個人"
+
msgid "Define a custom pattern with cron syntax"
msgstr "使用 Cron 語法自訂排程"
@@ -548,6 +815,9 @@ msgstr "部署金鑰"
msgid "Description"
msgstr "æè¿°"
+msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
+msgstr "æ述範本å…許你為項目的議題和åˆä½µè«‹æ±‚在建立時é¸æ“‡ç‰¹å®šçš„範本。"
+
msgid "Details"
msgstr "細節"
@@ -557,6 +827,12 @@ msgstr "目錄å稱"
msgid "Discard changes"
msgstr "放棄修改"
+msgid "Dismiss Cycle Analytics introduction box"
+msgstr ""
+
+msgid "Dismiss Merge Request promotion"
+msgstr "關閉åˆä½µè«‹æ±‚中的促銷廣告"
+
msgid "Don't show again"
msgstr "ä¸å†é¡¯ç¤º"
@@ -596,9 +872,6 @@ msgstr "編輯 %{id} æµæ°´ç·š (pipeline) 排程"
msgid "Emails"
msgstr "é›»å­éƒµä»¶"
-msgid "Enable in settings"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr "顯示全部"
@@ -627,6 +900,9 @@ msgid "Every week (Sundays at 4:00am)"
msgstr "æ¯é€±åŸ·è¡Œï¼ˆé€±æ—¥æ·©æ™¨ 四點)"
msgid "Explore projects"
+msgstr "ç€è¦½é …ç›®"
+
+msgid "Explore public groups"
msgstr ""
msgid "Failed to change the owner"
@@ -635,6 +911,9 @@ msgstr "無法變更所有權"
msgid "Failed to remove the pipeline schedule"
msgstr "無法刪除æµæ°´ç·š (pipeline) 排程"
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr "檔案"
@@ -660,6 +939,12 @@ msgstr[0] "分支 (fork) "
msgid "ForkedFromProjectPath|Forked from"
msgstr "分支 (fork) 自"
+msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
+msgstr "從 %{project_name} Fork. (deleted)"
+
+msgid "Format"
+msgstr "æ ¼å¼"
+
msgid "From issue creation until deploy to production"
msgstr "從議題 (issue) 建立直到部署至營é‹ç’°å¢ƒ"
@@ -670,8 +955,20 @@ msgid "GPG Keys"
msgstr "GPG 金鑰"
msgid "Geo Nodes"
+msgstr "Geo 節點"
+
+msgid "Geo|File sync capacity"
msgstr ""
+msgid "Geo|Groups to replicate"
+msgstr "è¦è¤‡è£½çš„群組"
+
+msgid "Geo|Repository sync capacity"
+msgstr ""
+
+msgid "Geo|Select groups to replicate."
+msgstr "é¸æ“‡æ¬²è¤‡è£½ä¹‹ç¾¤çµ„。"
+
msgid "Git storage health information has been reset"
msgstr "Git 儲存空間å¥åº·æŒ‡æ•¸å·²é‡ç½®"
@@ -684,28 +981,76 @@ msgstr "å‰å¾€æ‚¨çš„分支 (fork) "
msgid "GoToYourFork|Fork"
msgstr "å‰å¾€æ‚¨çš„分支 (fork) "
-msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
msgstr ""
+msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
+msgstr "ç¦æ­¢èˆ‡å…¶ä»–群組共享 %{group} 中的項目"
+
msgid "GroupSettings|Share with group lock"
-msgstr ""
+msgstr "分享群組鎖"
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
-msgstr ""
+msgstr "這個設定已經套用至 %{ancestor_group},並已經覆蓋此å­çµ„的設定。"
msgid "GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "此設定已經套用在 %{ancestor_group}。若è¦èˆ‡å…¶ä»–群組共享此群組中的項目,請è¯ç¹«æ“有者覆蓋這個設定或者 %{remove_ancestor_share_with_group_lock}"
msgid "GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}."
-msgstr ""
+msgstr "此設定已經套用至 %{ancestor_group}。你å¯ä»¥è¦†è“‹æ­¤è¨­å®šæˆ–是 %{remove_ancestor_share_with_group_lock}"
msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually."
-msgstr ""
+msgstr "此設定將套用在所有å­çµ„,除éžç¾¤çµ„æ“有者覆蓋。已經有權é™ç€è¦½æœ¬é …目的群組將ä»å¯ç€è¦½ï¼Œé™¤éžæ‰‹å‹•ç§»é™¤ã€‚"
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
-msgstr ""
+msgstr "無法調用父組的 \"共享群組鎖\",僅父群組的所有者æ‰å¯æ“作。"
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
+msgstr "從 %{ancestor_group_name} 中移除共享群組鎖"
+
+msgid "GroupsEmptyState|A group is a collection of several projects."
+msgstr ""
+
+msgid "GroupsEmptyState|If you organize your projects under a group, it works like a folder."
+msgstr ""
+
+msgid "GroupsEmptyState|No groups found"
+msgstr ""
+
+msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
+msgstr ""
+
+msgid "GroupsTreeRole|as"
+msgstr ""
+
+msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
+msgstr ""
+
+msgid "GroupsTree|Create a project in this group."
+msgstr ""
+
+msgid "GroupsTree|Create a subgroup in this group."
+msgstr ""
+
+msgid "GroupsTree|Edit group"
+msgstr ""
+
+msgid "GroupsTree|Failed to leave the group. Please make sure you are not the only owner."
+msgstr ""
+
+msgid "GroupsTree|Filter by name..."
+msgstr ""
+
+msgid "GroupsTree|Leave this group"
+msgstr ""
+
+msgid "GroupsTree|Loading groups"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr ""
msgid "Health Check"
@@ -726,30 +1071,61 @@ msgstr "沒有檢測到å¥åº·å•é¡Œ"
msgid "HealthCheck|Unhealthy"
msgstr "ä¸è‰¯"
+msgid "History"
+msgstr "æ­·å²"
+
msgid "Housekeeping successfully started"
msgstr "已開始維護"
msgid "Import repository"
msgstr "匯入檔案庫 (repository)"
+msgid "Improve Issue boards with GitLab Enterprise Edition."
+msgstr "å”助改進 GitLab ä¼æ¥­ç‰ˆçš„å•é¡Œçœ‹æ¿ã€‚"
+
+msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr "å”助改善 GitLab ä¼æ¥­ç‰ˆçš„å•é¡Œç®¡ç†èˆ‡æ¬Šé‡ã€‚"
+
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgstr "å”助改進 GitLab ä¼æ¥­ç‰ˆçš„æœå°‹ & 進階全局æœå°‹ã€‚"
+
msgid "Install a Runner compatible with GitLab CI"
msgstr "安è£èˆ‡ GitLab CI 相容的 Runner"
+msgid "Instance"
+msgid_plural "Instances"
+msgstr[0] "實例"
+
+msgid "Internal - The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "Internal - The project can be accessed by any logged in user."
+msgstr ""
+
msgid "Interval Pattern"
msgstr "循環週期"
msgid "Introducing Cycle Analytics"
msgstr "週期分æžç°¡ä»‹"
+msgid "Issue board focus mode"
+msgstr "å•é¡Œçœ‹æ¿æ¨¡å¼"
+
+msgid "Issue boards with milestones"
+msgstr "å•é¡Œçœ‹æ¿ & 里程碑"
+
msgid "Issue events"
msgstr "議題 (issue) 事件"
+msgid "IssueBoards|Board"
+msgstr "看æ¿"
+
+msgid "IssueBoards|Boards"
+msgstr "看æ¿"
+
msgid "Issues"
msgstr "議題"
-msgid "Jobs"
-msgstr ""
-
msgid "LFSStatus|Disabled"
msgstr "åœç”¨"
@@ -766,12 +1142,21 @@ msgstr[0] "最近 %d 天"
msgid "Last Pipeline"
msgstr "最新æµæ°´ç·š (pipeline) "
-msgid "Last Update"
-msgstr "最後更新"
-
msgid "Last commit"
msgstr "最後更動記錄 (commit) "
+msgid "Last edited %{date}"
+msgstr "上次編輯於 %{date}"
+
+msgid "Last edited by %{name}"
+msgstr "上次編輯由 %{name}"
+
+msgid "Last update"
+msgstr "上次更新"
+
+msgid "Last updated"
+msgstr "上次更新"
+
msgid "LastPushEvent|You pushed to"
msgstr "您上傳 (push) 了"
@@ -784,6 +1169,9 @@ msgstr "了解更多"
msgid "Learn more in the|pipeline schedules documentation"
msgstr "æµæ°´ç·š (pipeline) 排程說明文件"
+msgid "Leave"
+msgstr ""
+
msgid "Leave group"
msgstr "退出群組"
@@ -791,13 +1179,25 @@ msgid "Leave project"
msgstr "退出專案"
msgid "License"
-msgstr ""
+msgstr "授權"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "é™åˆ¶æœ€å¤šé¡¯ç¤º %d 個事件"
+msgid "Lock"
+msgstr "鎖定"
+
+msgid "Locked"
+msgstr "鎖定"
+
msgid "Locked Files"
+msgstr "被鎖定檔案"
+
+msgid "Login"
+msgstr ""
+
+msgid "Maximum git storage failures"
msgstr ""
msgid "Median"
@@ -813,7 +1213,7 @@ msgid "Merge events"
msgstr "åˆä½µ (merge) 事件"
msgid "Merge request"
-msgstr ""
+msgstr "åˆä½µè«‹æ±‚"
msgid "Messages"
msgstr "公告"
@@ -827,6 +1227,12 @@ msgstr "監控"
msgid "More information is available|here"
msgstr "å¥åº·æª¢æŸ¥"
+msgid "Multiple issue boards"
+msgstr "多個å•é¡Œçœ‹æ¿"
+
+msgid "New Cluster"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "建立議題 (issue) "
@@ -843,27 +1249,42 @@ msgstr "新增目錄"
msgid "New file"
msgstr "新增檔案"
+msgid "New group"
+msgstr ""
+
msgid "New issue"
msgstr "新增議題 (issue) "
msgid "New merge request"
msgstr "新增åˆä½µè«‹æ±‚ (merge request) "
+msgid "New project"
+msgstr ""
+
msgid "New schedule"
msgstr "新增排程"
msgid "New snippet"
msgstr "新文字片段"
+msgid "New subgroup"
+msgstr ""
+
msgid "New tag"
msgstr "新增標籤"
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr ""
+
msgid "No repository"
msgstr "找ä¸åˆ°æª”案庫 (repository)"
msgid "No schedules"
msgstr "沒有排程"
+msgid "None"
+msgstr "ç„¡"
+
msgid "Not available"
msgstr "無法使用"
@@ -927,12 +1348,24 @@ msgstr "關注"
msgid "Notifications"
msgstr "通知"
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "篩é¸"
+msgid "Only project members can comment."
+msgstr "åªæœ‰ç¾¤çµ„æˆå“¡æ‰èƒ½ç•™è¨€ã€‚"
+
msgid "OpenedNDaysAgo|Opened"
msgstr "開始於"
+msgid "Opens in a new window"
+msgstr "於新視窗開啟"
+
msgid "Options"
msgstr "é¸é …"
@@ -943,20 +1376,23 @@ msgid "Owner"
msgstr "所有權"
msgid "Pagination|Last »"
-msgstr ""
+msgstr "å°¾é  Â»"
msgid "Pagination|Next"
-msgstr ""
+msgstr "下一é "
msgid "Pagination|Prev"
-msgstr ""
+msgstr "上一é "
msgid "Pagination|« First"
-msgstr ""
+msgstr "« 首é "
msgid "Password"
msgstr "密碼"
+msgid "People without permission will never get a notification and won\\'t be able to comment."
+msgstr "當使用者沒有權é™ï¼Œå°‡ä¸æœƒæ”¶åˆ°ä»»ä½•é€šçŸ¥ä»¥åŠç„¡æ³•ç•™è¨€"
+
msgid "Pipeline"
msgstr "æµæ°´ç·š (pipeline) "
@@ -970,7 +1406,7 @@ msgid "Pipeline Schedules"
msgstr "æµæ°´ç·š (pipeline) 排程"
msgid "Pipeline quota"
-msgstr ""
+msgstr "æµæ°´ç·šé…é¡"
msgid "PipelineCharts|Failed:"
msgstr "失敗:"
@@ -1036,13 +1472,13 @@ msgid "Pipelines charts"
msgstr "æµæ°´ç·š (pipeline) 圖表"
msgid "Pipelines for last month"
-msgstr ""
+msgstr "上個月的æµæ°´ç·š"
msgid "Pipelines for last week"
-msgstr ""
+msgstr "上週的æµæ°´ç·š"
msgid "Pipelines for last year"
-msgstr ""
+msgstr "去年的æµæ°´ç·š"
msgid "Pipeline|all"
msgstr "所有"
@@ -1059,7 +1495,52 @@ msgstr "於階段"
msgid "Preferences"
msgstr "å好設定"
+msgid "Private - Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Private - The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "Profile"
+msgstr "個人資料"
+
+msgid "Profiles|Account scheduled for removal."
+msgstr ""
+
+msgid "Profiles|Delete Account"
+msgstr ""
+
+msgid "Profiles|Delete account"
+msgstr ""
+
+msgid "Profiles|Delete your account?"
+msgstr ""
+
+msgid "Profiles|Deleting an account has the following effects:"
+msgstr ""
+
+msgid "Profiles|Invalid password"
+msgstr ""
+
+msgid "Profiles|Invalid username"
+msgstr ""
+
+msgid "Profiles|Type your %{confirmationValue} to confirm:"
+msgstr ""
+
+msgid "Profiles|You don't have access to delete this user."
+msgstr ""
+
+msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
+msgstr ""
+
+msgid "Profiles|Your account is currently an owner in these groups:"
+msgstr ""
+
+msgid "Profiles|your account"
+msgstr ""
+
+msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@@ -1071,9 +1552,6 @@ msgstr "專案 '%{project_name}' 建立完æˆã€‚"
msgid "Project '%{project_name}' was successfully updated."
msgstr "專案 '%{project_name}' 更新完æˆã€‚"
-msgid "Project '%{project_name}' will be deleted."
-msgstr "專案 '%{project_name}' 將被刪除。"
-
msgid "Project access must be granted explicitly to each user."
msgstr "專案權é™å¿…須一一指派給æ¯å€‹ä½¿ç”¨è€…。"
@@ -1116,20 +1594,41 @@ msgstr "階段"
msgid "ProjectNetworkGraph|Graph"
msgstr "分支圖"
-msgid "ProjectsDropdown|Frequently visited"
+msgid "ProjectSettings|Contact an admin to change this setting."
+msgstr "è¯çµ¡ç®¡ç†å“¡ä»¥è®Šæ›´è¨­å®šã€‚"
+
+msgid "ProjectSettings|Only signed commits can be pushed to this repository."
+msgstr "åªæœ‰å·²ç°½ç½²çš„變更æ‰èƒ½è¢«æŽ¨é€åˆ°å€‰åº«ã€‚"
+
+msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
+msgstr "此設定已經套用於伺æœå™¨å±¤ç´šï¼Œä¸¦ä¸”å¯è¢«ç®¡ç†å“¡è¦†å¯«ã€‚"
+
+msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
+msgstr "此設定已經套用於伺æœå™¨ç´šåˆ¥ï¼Œä½†å·²ç¶“在這個專案被覆寫。"
+
+msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
+msgstr "此設定將套用於所有專案,除éžè¢«ç®¡ç†å“¡è¦†å¯«ã€‚"
+
+msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
msgstr ""
-msgid "ProjectsDropdown|Loading projects"
+msgid "Projects"
msgstr ""
+msgid "ProjectsDropdown|Frequently visited"
+msgstr "經常使用"
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr "讀å–專案中"
+
msgid "ProjectsDropdown|Projects you visit often will appear here"
-msgstr ""
+msgstr "您經常拜訪的專案會顯示在這裡"
msgid "ProjectsDropdown|Search your projects"
msgstr "æœå°‹æ‚¨çš„專案"
msgid "ProjectsDropdown|Something went wrong on our end."
-msgstr ""
+msgstr "發生了內部錯誤"
msgid "ProjectsDropdown|Sorry, no projects matched your search"
msgstr "抱歉,沒有符åˆæœå°‹æ¢ä»¶çš„專案"
@@ -1137,12 +1636,21 @@ msgstr "抱歉,沒有符åˆæœå°‹æ¢ä»¶çš„專案"
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "此功能需è¦ç€è¦½å™¨æ”¯æ´ localStorage"
-msgid "Push Rules"
+msgid "Public - The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "Public - The project can be accessed without any authentication."
msgstr ""
+msgid "Push Rules"
+msgstr "æŽ¨é€ [Push] è¦å‰‡"
+
msgid "Push events"
msgstr "æŽ¨é€ (push) 事件"
+msgid "PushRule|Committer restriction"
+msgstr ""
+
msgid "Read more"
msgstr "瞭解更多"
@@ -1156,7 +1664,7 @@ msgid "RefSwitcher|Tags"
msgstr "標籤"
msgid "Registry"
-msgstr ""
+msgstr "登錄表"
msgid "Related Commits"
msgstr "相關的更動記錄 (commit) "
@@ -1206,6 +1714,12 @@ msgstr "還原此åˆä½µè«‹æ±‚ (merge request) "
msgid "SSH Keys"
msgstr "SSH 金鑰"
+msgid "Save"
+msgstr ""
+
+msgid "Save changes"
+msgstr "儲存變更"
+
msgid "Save pipeline schedule"
msgstr "儲存æµæ°´ç·š (pipeline) 排程"
@@ -1213,7 +1727,7 @@ msgid "Schedule a new pipeline"
msgstr "建立æµæ°´ç·š (pipeline) 排程"
msgid "Schedules"
-msgstr ""
+msgstr "排程"
msgid "Scheduling Pipelines"
msgstr "æµæ°´ç·š (pipeline) 排程"
@@ -1221,15 +1735,21 @@ msgstr "æµæ°´ç·š (pipeline) 排程"
msgid "Search branches and tags"
msgstr "æœå°‹åˆ†æ”¯ (branch) 和標籤"
+msgid "Seconds before reseting failure information"
+msgstr ""
+
+msgid "Seconds to wait after a storage failure"
+msgstr ""
+
+msgid "Seconds to wait for a storage access attempt"
+msgstr ""
+
msgid "Select Archive Format"
msgstr "é¸æ“‡ä¸‹è¼‰æ ¼å¼"
msgid "Select a timezone"
msgstr "é¸æ“‡æ™‚å€"
-msgid "Select existing branch"
-msgstr "é¸æ“‡ç¾æœ‰åˆ†æ”¯ (branch)"
-
msgid "Select target branch"
msgstr "é¸æ“‡ç›®æ¨™åˆ†æ”¯ (branch) "
@@ -1255,119 +1775,134 @@ msgid "Settings"
msgstr "設定"
msgid "Show parent pages"
-msgstr ""
+msgstr "顯示父é é¢"
msgid "Show parent subgroups"
-msgstr ""
+msgstr "顯示群組中的å­ç¾¤çµ„"
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "顯示 %d 個事件"
msgid "Snippets"
+msgstr "文字片段"
+
+msgid "Something went wrong on our end."
msgstr ""
-msgid "SortOptions|Access level, ascending"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgstr "有個地方出錯了,因為他嘗試去變更 ${this.issuableDisplayName(this.issuableType)} 的鎖定狀態。"
+
+msgid "Something went wrong while fetching the projects."
msgstr ""
-msgid "SortOptions|Access level, descending"
+msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "SortOptions|Created date"
+msgid "Sort by"
msgstr ""
+msgid "SortOptions|Access level, ascending"
+msgstr "訪å•ç´šåˆ¥ã€å‡å†ªæŽ’列"
+
+msgid "SortOptions|Access level, descending"
+msgstr "訪å•ç´šåˆ¥ã€é™å†ªæŽ’列"
+
+msgid "SortOptions|Created date"
+msgstr "建立日期"
+
msgid "SortOptions|Due date"
-msgstr ""
+msgstr "截止日期"
msgid "SortOptions|Due later"
-msgstr ""
+msgstr "已截止"
msgid "SortOptions|Due soon"
-msgstr ""
+msgstr "å³å°‡æˆªæ­¢"
msgid "SortOptions|Label priority"
-msgstr ""
+msgstr "標籤優先"
msgid "SortOptions|Largest group"
-msgstr ""
+msgstr "最大群組"
msgid "SortOptions|Largest repository"
-msgstr ""
+msgstr "最大儲存庫"
msgid "SortOptions|Last created"
-msgstr ""
+msgstr "最近建立"
msgid "SortOptions|Last joined"
-msgstr ""
+msgstr "最近加入"
msgid "SortOptions|Last updated"
-msgstr ""
+msgstr "最近更新"
msgid "SortOptions|Least popular"
-msgstr ""
+msgstr "最ä¸å—æ­¡è¿Ž"
msgid "SortOptions|Less weight"
-msgstr ""
+msgstr "最低權é‡"
msgid "SortOptions|Milestone"
-msgstr ""
+msgstr "里程碑"
msgid "SortOptions|Milestone due later"
-msgstr ""
+msgstr "里程碑截止日期"
msgid "SortOptions|Milestone due soon"
-msgstr ""
+msgstr "å³å°‡æˆªæ­¢çš„里程碑"
msgid "SortOptions|More weight"
-msgstr ""
+msgstr "更大的權é‡"
msgid "SortOptions|Most popular"
-msgstr ""
+msgstr "最å—æ­¡è¿Ž"
msgid "SortOptions|Name"
-msgstr ""
+msgstr "å稱"
msgid "SortOptions|Name, ascending"
-msgstr ""
+msgstr "å稱ã€å‡å†ªæŽ’列"
msgid "SortOptions|Name, descending"
-msgstr ""
+msgstr "å稱ã€é™å†ªæŽ’列"
msgid "SortOptions|Oldest created"
-msgstr ""
+msgstr "最早建立"
msgid "SortOptions|Oldest joined"
-msgstr ""
+msgstr "最早加入"
msgid "SortOptions|Oldest sign in"
-msgstr ""
+msgstr "最早登入"
msgid "SortOptions|Oldest updated"
-msgstr ""
+msgstr "最早æ交"
msgid "SortOptions|Popularity"
-msgstr ""
+msgstr "最有人氣"
msgid "SortOptions|Priority"
-msgstr ""
+msgstr "優先"
msgid "SortOptions|Recent sign in"
-msgstr ""
+msgstr "最近登入"
msgid "SortOptions|Start later"
-msgstr ""
+msgstr "ç¨å¾Œé–‹å§‹"
msgid "SortOptions|Start soon"
-msgstr ""
+msgstr "ç¾åœ¨é–‹å§‹"
msgid "SortOptions|Weight"
-msgstr ""
+msgstr "權é‡"
msgid "Source code"
msgstr "原始碼"
msgid "Spam Logs"
-msgstr ""
+msgstr "垃圾訊æ¯è¨˜éŒ„"
msgid "Specify the following URL during the Runner setup:"
msgstr "åœ¨å®‰è£ Runner 時指定以下 URL:"
@@ -1376,7 +1911,7 @@ msgid "StarProject|Star"
msgstr "收è—"
msgid "Starred projects"
-msgstr ""
+msgstr "星標項目"
msgid "Start a %{new_merge_request} with these changes"
msgstr "以這些改動建立一個新的 %{new_merge_request} "
@@ -1384,11 +1919,17 @@ msgstr "以這些改動建立一個新的 %{new_merge_request} "
msgid "Start the Runner!"
msgstr "å•Ÿå‹• Runner!"
+msgid "Subgroups"
+msgstr ""
+
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "切æ›åˆ†æ”¯ (branch) 或標籤"
msgid "System Hooks"
-msgstr ""
+msgstr "系統鉤å­"
msgid "Tag"
msgid_plural "Tags"
@@ -1403,6 +1944,15 @@ msgstr "目標分支 (branch) "
msgid "Team"
msgstr "團隊"
+msgid "Thanks! Don't show me this again"
+msgstr "æ„Ÿè¬ï¼è«‹ä¸è¦å†æ¬¡é¡¯ç¤º"
+
+msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
+msgstr "GitLab 的進階全局æœå°‹åŠŸèƒ½æ˜¯éžå¸¸å¼·å¤§çš„æœå°‹æœå‹™ã€‚您å¯ä»¥æœå°‹å…¶ä»–團隊的代碼以幫助您完善自己項目中的代碼。從而é¿å…建立é‡è¤‡çš„代碼浪費時間。"
+
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "程å¼é–‹ç™¼éšŽæ®µé¡¯ç¤ºå¾žç¬¬ä¸€æ¬¡æ›´å‹•è¨˜éŒ„ (commit) 到建立åˆä½µè«‹æ±‚ (merge request) 的時間。建立第一個åˆä½µè«‹æ±‚後,資料將自動填入。"
@@ -1415,6 +1965,15 @@ 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 number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
+msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr "專案開發週期的å„個階段。"
@@ -1445,6 +2004,12 @@ 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 time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset."
+msgstr ""
+
+msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised."
+msgstr ""
+
msgid "The time taken by each data entry gathered by that stage."
msgstr "該階段中æ¯ä¸€å€‹è³‡æ–™é …目所花的時間。"
@@ -1454,12 +2019,27 @@ msgstr "中ä½æ•¸æ˜¯ä¸€å€‹æ•¸åˆ—中最中間的值。例如在 3ã€5ã€9 之間ï
msgid "There are problems accessing Git storage: "
msgstr "å­˜å– Git 儲存空間時出ç¾å•é¡Œï¼š"
-msgid "This is the author's first Merge Request to this project. Handle with care."
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
+msgid "This is a confidential issue."
+msgstr "這是個隱密å•é¡Œã€‚"
+
+msgid "This is the author's first Merge Request to this project."
+msgstr "這是作者第一次åˆä½µè«‹æ±‚至本專案。"
+
+msgid "This issue is confidential and locked."
+msgstr "這個å•é¡Œæ˜¯ä¿å¯†ä¸”鎖定的。"
+
+msgid "This issue is locked."
+msgstr "這個å•é¡Œå·²è¢«éŽ–定。"
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "這代表在您建立一個空的檔案庫 (repository) 或是匯入一個ç¾å­˜çš„檔案庫之å‰ï¼Œæ‚¨å°‡ç„¡æ³•ä¸Šå‚³æ›´æ–° (push) 。"
+msgid "This merge request is locked."
+msgstr "這個åˆä½µè«‹æ±‚已被鎖定。"
+
msgid "Time before an issue gets scheduled"
msgstr "議題 (issue) 被列入日程表的時間"
@@ -1538,9 +2118,6 @@ msgstr " 1 個月å‰"
msgid "Timeago|a week ago"
msgstr " 1 週å‰"
-msgid "Timeago|a while"
-msgstr "剛剛"
-
msgid "Timeago|a year ago"
msgstr " 1 å¹´å‰"
@@ -1592,6 +2169,9 @@ msgstr " 1 週後"
msgid "Timeago|in 1 year"
msgstr " 1 年後"
+msgid "Timeago|in a while"
+msgstr "剛剛"
+
msgid "Timeago|less than a minute ago"
msgstr "ä¸åˆ° 1 分é˜å‰"
@@ -1612,9 +2192,36 @@ msgstr "總時間"
msgid "Total test time for all commits/merges"
msgstr "åˆä½µ (merge) 與更動記錄 (commit) 的總測試時間"
+msgid "Track activity with Contribution Analytics."
+msgstr "追蹤分æžè²¢ç»èˆ‡æ´»å‹•ã€‚"
+
+msgid "Unlock"
+msgstr "解鎖"
+
+msgid "Unlocked"
+msgstr "已解鎖"
+
msgid "Unstar"
msgstr "å–消收è—"
+msgid "Unsubscribe"
+msgstr ""
+
+msgid "Upgrade your plan to activate Advanced Global Search."
+msgstr "å‡ç´šæ‚¨çš„方案以啟用進階全局æœå°‹ã€‚"
+
+msgid "Upgrade your plan to activate Contribution Analytics."
+msgstr "å‡ç´šæ‚¨çš„方案以啟用貢ç»åˆ†æžã€‚"
+
+msgid "Upgrade your plan to activate Group Webhooks."
+msgstr "å‡ç´šæ‚¨çš„方案以啟用群組 Webhooks。"
+
+msgid "Upgrade your plan to activate Issue weight."
+msgstr "å‡ç´šæ‚¨çš„方案以啟用å•é¡Œæ¬Šé‡ã€‚"
+
+msgid "Upgrade your plan to improve Issue boards."
+msgstr "å‡ç´šæ‚¨çš„方案以使用å•é¡Œçœ‹ç‰ˆ"
+
msgid "Upload New File"
msgstr "上傳新檔案"
@@ -1631,13 +2238,13 @@ msgid "Use your global notification setting"
msgstr "使用全域通知設定"
msgid "View file @ "
-msgstr ""
+msgstr "ç€è¦½æª”案 @ "
msgid "View open merge request"
msgstr "查看此分支的åˆä½µè«‹æ±‚ (merge request)"
msgid "View replaced file @ "
-msgstr ""
+msgstr "ç€è¦½å·²æ›¿æ›æª”案 @ "
msgid "VisibilityLevel|Internal"
msgstr "內部"
@@ -1657,9 +2264,120 @@ msgstr "權é™ä¸è¶³ã€‚如需查看相關資料,請å‘管ç†å“¡ç”³è«‹æ¬Šé™ã€‚
msgid "We don't have enough data to show this stage."
msgstr "因該階段的資料ä¸è¶³è€Œç„¡æ³•é¡¯ç¤ºç›¸é—œè³‡è¨Š"
+msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
+msgstr "如果有新的推é€æˆ–新的议题,Webhook 将自动触å‘您设置 URL。 您å¯ä»¥é…ç½® Webhook æ¥ç›‘å¬ç‰¹å®šäº‹ä»¶ï¼Œå¦‚推é€ã€è®®é¢˜æˆ–åˆå¹¶è¯·æ±‚。 群组 Webhook 将适用于团队中的所有项目,并å…许您设置整个团队中的 Webhook 。"
+
+msgid "Weight"
+msgstr "權é‡"
+
+msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
+msgstr ""
+
msgid "Wiki"
msgstr "Wiki"
+msgid "WikiClone|Clone your wiki"
+msgstr "克隆你的維基"
+
+msgid "WikiClone|Git Access"
+msgstr "Git 讀å–"
+
+msgid "WikiClone|Install Gollum"
+msgstr "å®‰è£ Gollum"
+
+msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
+msgstr "å®ƒè¢«æŽ¨è–¦å®‰è£ %{markdown} 所以那 GFM 功能在本機呈ç¾ï¼š"
+
+msgid "WikiClone|Start Gollum and edit locally"
+msgstr "開始你的 Gollum 並在本機編輯。"
+
+msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgstr "你沒有權é™åŽ»å»ºç«‹ç¶­åŸºé é¢"
+
+msgid "WikiHistoricalPage|This is an old version of this page."
+msgstr "這是這個é é¢è¼ƒèˆŠçš„版本。"
+
+msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
+msgstr "ä½ å¯ä»¥æŸ¥çœ‹ %{most_recent_link} 或是ç€è¦½ %{history_link} 。"
+
+msgid "WikiHistoricalPage|history"
+msgstr "æ­·å²"
+
+msgid "WikiHistoricalPage|most recent version"
+msgstr "最新版本"
+
+msgid "WikiMarkdownDocs|More examples are in the %{docs_link}"
+msgstr "更多範例在 %{docs_link}"
+
+msgid "WikiMarkdownDocs|documentation"
+msgstr "文件"
+
+msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
+msgstr "è‹¥è¦é€£çµåˆ°ä¸€å€‹æ–°çš„é é¢ï¼Œåªè¦ç°¡å–®çš„輸入 %{link_example}。"
+
+msgid "WikiNewPagePlaceholder|how-to-setup"
+msgstr "如何安è£"
+
+msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
+msgstr "å°æ示: ä½ å¯ä»¥æŒ‡å®šæ–°æª”案的絕å°è·¯å¾‘。我們會自動建立所有ä¸å­˜åœ¨çš„目錄。"
+
+msgid "WikiNewPageTitle|New Wiki Page"
+msgstr "新的維基é é¢"
+
+msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
+msgstr "你確定è¦åˆªé™¤é€™å€‹é é¢ï¼Ÿ"
+
+msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
+msgstr "æŸå€‹äººåœ¨åŒä¸€æ™‚間跟你一起編輯了這個é é¢ã€‚請檢查 %{page_link} 並確定你的變更並沒有被他們無æ„中移除。"
+
+msgid "WikiPageConflictMessage|the page"
+msgstr "æ­¤é é¢"
+
+msgid "WikiPageCreate|Create %{page_title}"
+msgstr "建立 %{page_title}"
+
+msgid "WikiPageEdit|Update %{page_title}"
+msgstr "æ›´æ–° %{page_title}"
+
+msgid "WikiPage|Page slug"
+msgstr "é é¢ slug"
+
+msgid "WikiPage|Write your content or drag files here..."
+msgstr "寫上你的內容或拖曳檔案到這..."
+
+msgid "Wiki|Create Page"
+msgstr "建立é é¢"
+
+msgid "Wiki|Create page"
+msgstr "建立é é¢"
+
+msgid "Wiki|Edit Page"
+msgstr "編輯é é¢"
+
+msgid "Wiki|Empty page"
+msgstr "空白é é¢"
+
+msgid "Wiki|More Pages"
+msgstr "更多é é¢"
+
+msgid "Wiki|New page"
+msgstr "æ–°é é¢"
+
+msgid "Wiki|Page history"
+msgstr "æ­·å²é é¢"
+
+msgid "Wiki|Page version"
+msgstr "é é¢ç‰ˆæœ¬"
+
+msgid "Wiki|Pages"
+msgstr "é é¢"
+
+msgid "Wiki|Wiki Pages"
+msgstr "維基é é¢"
+
+msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgstr "é€éŽè²¢ç»åˆ†æžï¼Œæ‚¨å¯ä»¥åˆ†æžæ‚¨çš„組織åŠå…¶æˆå“¡çš„å•é¡Œã€åˆä½µè«‹æ±‚和推é€æ´»å‹•ã€‚"
+
msgid "Withdraw Access Request"
msgstr "å–消權é™ç”³è«‹"
@@ -1675,9 +2393,21 @@ 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 on a read-only GitLab instance."
+msgstr ""
+
+msgid "You are on a read-only GitLab instance. If you want to make any changes, you must visit the %{link_to_primary_node}."
+msgstr ""
+
msgid "You can only add files when you are on a branch"
msgstr "åªèƒ½åœ¨åˆ†æ”¯ (branch) 上建立檔案"
+msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
+msgstr ""
+
+msgid "You cannot write to this read-only GitLab instance."
+msgstr ""
+
msgid "You have reached your project limit"
msgstr "您已é”到專案數é‡é™åˆ¶"
@@ -1708,14 +2438,20 @@ 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"
msgstr "在個人帳號中 %{add_ssh_key_link} 之å‰ï¼Œ 將無法使用 SSH 上傳 (push) 或下載 (pull) 程å¼ç¢¼ã€‚"
+msgid "Your comment will not be visible to the public."
+msgstr "你的留言將ä¸æœƒè¢«å…¬é–‹ã€‚"
+
+msgid "Your groups"
+msgstr ""
+
msgid "Your name"
msgstr "您的åå­—"
msgid "Your projects"
-msgstr ""
+msgstr "你的計劃"
msgid "commit"
-msgstr ""
+msgstr "æ›´å‹•"
msgid "day"
msgid_plural "days"
@@ -1731,3 +2467,15 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "上層"
+msgid "password"
+msgstr ""
+
+msgid "personal access token"
+msgstr ""
+
+msgid "to help your contributors communicate effectively!"
+msgstr "幫助你的貢ç»è€…進行有效的æºé€šï¼"
+
+msgid "username"
+msgstr ""
+
diff --git a/package.json b/package.json
index 5aa3ce3f757..c0a4db122bd 100644
--- a/package.json
+++ b/package.json
@@ -13,15 +13,20 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
- "axios": "^0.16.2",
- "babel-core": "^6.22.1",
- "babel-eslint": "^7.2.1",
- "babel-loader": "^7.1.1",
- "babel-plugin-transform-define": "^1.2.0",
- "babel-preset-latest": "^6.24.0",
- "babel-preset-stage-2": "^6.22.0",
+ "autosize": "^4.0.0",
+ "axios": "^0.17.1",
+ "axios-mock-adapter": "^1.10.0",
+ "babel-core": "^6.26.0",
+ "babel-eslint": "^8.0.2",
+ "babel-loader": "^7.1.2",
+ "babel-plugin-transform-define": "^1.3.0",
+ "babel-preset-latest": "^6.24.1",
+ "babel-preset-stage-2": "^6.24.1",
+ "blackst0ne-mermaid": "^7.1.0-fixed",
"bootstrap-sass": "^3.3.6",
"brace-expansion": "^1.1.8",
+ "classlist-polyfill": "^1.2.0",
+ "clipboard": "^1.7.1",
"compression-webpack-plugin": "^1.0.0",
"copy-webpack-plugin": "^4.0.1",
"core-js": "^2.4.1",
@@ -29,24 +34,26 @@
"css-loader": "^0.28.0",
"d3": "^3.5.11",
"deckar01-task_list": "^2.0.0",
+ "diff": "^3.4.0",
"document-register-element": "1.3.0",
"dropzone": "^4.2.0",
"emoji-unicode-version": "^0.2.1",
"eslint-plugin-html": "^2.0.1",
"exports-loader": "^0.6.4",
"file-loader": "^0.11.1",
+ "fuzzaldrin-plus": "^0.5.0",
"imports-loader": "^0.7.1",
"jed": "^1.1.1",
- "jquery": "^2.2.1",
- "jquery-ujs": "^1.2.1",
+ "jquery": "^2.2.4",
+ "jquery-ujs": "1.2.2",
"js-cookie": "^2.1.3",
"jszip": "^3.1.3",
"jszip-utils": "^0.0.2",
"marked": "^0.3.6",
- "monaco-editor": "0.8.3",
+ "monaco-editor": "0.10.0",
"mousetrap": "^1.4.6",
"name-all-modules-plugin": "^1.0.1",
- "pikaday": "^1.5.1",
+ "pikaday": "^1.6.1",
"prismjs": "^1.6.0",
"raphael": "^2.2.7",
"raven-js": "^3.14.0",
@@ -58,21 +65,23 @@
"three": "^0.84.0",
"three-orbit-controls": "^82.1.0",
"three-stl-loader": "^1.0.4",
- "timeago.js": "^2.0.5",
+ "timeago.js": "^3.0.2",
"underscore": "^1.8.3",
"url-loader": "^0.5.8",
"visibilityjs": "^1.2.4",
- "vue": "^2.2.6",
- "vue-loader": "^11.3.4",
+ "vue": "^2.5.8",
+ "vue-loader": "^13.5.0",
"vue-resource": "^1.3.4",
- "vue-template-compiler": "^2.2.6",
- "vuex": "^2.3.1",
+ "vue-template-compiler": "^2.5.8",
+ "vuex": "^3.0.1",
"webpack": "^3.5.5",
"webpack-bundle-analyzer": "^2.8.2",
- "webpack-stats-plugin": "^0.1.5"
+ "webpack-stats-plugin": "^0.1.5",
+ "worker-loader": "^1.1.0"
},
"devDependencies": {
- "babel-plugin-istanbul": "^4.0.0",
+ "@gitlab-org/gitlab-svgs": "^1.1.1",
+ "babel-plugin-istanbul": "^4.1.5",
"eslint": "^3.10.1",
"eslint-config-airbnb-base": "^10.0.1",
"eslint-import-resolver-webpack": "^0.8.3",
@@ -80,7 +89,6 @@
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jasmine": "^2.1.0",
"eslint-plugin-promise": "^3.5.0",
- "gitlab-svgs": "https://gitlab.com/gitlab-org/gitlab-svgs.git",
"istanbul": "^0.4.5",
"jasmine-core": "^2.6.3",
"jasmine-jquery": "^2.1.1",
diff --git a/qa/.gitignore b/qa/.gitignore
index 3fec32c8427..19ec17d0005 100644
--- a/qa/.gitignore
+++ b/qa/.gitignore
@@ -1 +1,2 @@
tmp/
+.ruby-version
diff --git a/qa/Dockerfile b/qa/Dockerfile
index f3a81a7e355..9b6ffff7c4d 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -9,6 +9,13 @@ RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list
RUN apt-get update && apt-get install -y wget git unzip xvfb
##
+# Install Docker
+#
+RUN wget -q https://download.docker.com/linux/static/stable/x86_64/docker-17.09.0-ce.tgz && \
+ tar -zxf docker-17.09.0-ce.tgz && mv docker/docker /usr/local/bin/docker && \
+ rm docker-17.09.0-ce.tgz
+
+##
# Install Google Chrome version with headless support
#
RUN curl -sS -L https://dl.google.com/linux/linux_signing_key.pub | apt-key add -
diff --git a/qa/bin/qa b/qa/bin/qa
index cecdeac14db..6a772e93cee 100755
--- a/qa/bin/qa
+++ b/qa/bin/qa
@@ -4,4 +4,4 @@ require_relative '../qa'
QA::Scenario
.const_get(ARGV.shift)
- .perform(*ARGV)
+ .launch!(ARGV)
diff --git a/qa/qa.rb b/qa/qa.rb
index db9d8c42fde..06b6a76489b 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -8,6 +8,7 @@ module QA
autoload :Release, 'qa/runtime/release'
autoload :User, 'qa/runtime/user'
autoload :Namespace, 'qa/runtime/namespace'
+ autoload :Scenario, 'qa/runtime/scenario'
end
##
@@ -17,7 +18,9 @@ module QA
##
# Support files
#
+ autoload :Bootable, 'qa/scenario/bootable'
autoload :Actable, 'qa/scenario/actable'
+ autoload :Entrypoint, 'qa/scenario/entrypoint'
autoload :Template, 'qa/scenario/template'
##
@@ -25,15 +28,31 @@ module QA
#
module Test
autoload :Instance, 'qa/scenario/test/instance'
+
+ module Integration
+ autoload :Mattermost, 'qa/scenario/test/integration/mattermost'
+ end
end
##
# GitLab instance scenarios.
#
module Gitlab
+ module Group
+ autoload :Create, 'qa/scenario/gitlab/group/create'
+ end
+
module Project
autoload :Create, 'qa/scenario/gitlab/project/create'
end
+
+ module Sandbox
+ autoload :Prepare, 'qa/scenario/gitlab/sandbox/prepare'
+ end
+
+ module Admin
+ autoload :HashedStorage, 'qa/scenario/gitlab/admin/hashed_storage'
+ end
end
end
@@ -47,14 +66,18 @@ module QA
module Main
autoload :Entry, 'qa/page/main/entry'
+ autoload :Login, 'qa/page/main/login'
autoload :Menu, 'qa/page/main/menu'
+ autoload :OAuth, 'qa/page/main/oauth'
end
module Dashboard
+ autoload :Projects, 'qa/page/dashboard/projects'
autoload :Groups, 'qa/page/dashboard/groups'
end
module Group
+ autoload :New, 'qa/page/group/new'
autoload :Show, 'qa/page/group/show'
end
@@ -65,6 +88,12 @@ module QA
module Admin
autoload :Menu, 'qa/page/admin/menu'
+ autoload :Settings, 'qa/page/admin/settings'
+ end
+
+ module Mattermost
+ autoload :Main, 'qa/page/mattermost/main'
+ autoload :Login, 'qa/page/mattermost/login'
end
end
@@ -76,6 +105,13 @@ module QA
end
##
+ # Classes describing shell interaction with GitLab
+ #
+ module Shell
+ autoload :Omnibus, 'qa/shell/omnibus'
+ end
+
+ ##
# Classes that make it possible to execute features tests.
#
module Specs
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index b9e199000d6..59cd147e055 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -23,7 +23,7 @@ module QA
def password=(pass)
@password = pass
- @uri.password = pass
+ @uri.password = CGI.escape(pass)
end
def use_default_credentials
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
index baa06b1c75e..dd289ffe269 100644
--- a/qa/qa/page/admin/menu.rb
+++ b/qa/qa/page/admin/menu.rb
@@ -3,8 +3,11 @@ module QA
module Admin
class Menu < Page::Base
def go_to_license
- link = find_link 'License'
- link.click
+ click_link 'License'
+ end
+
+ def go_to_settings
+ click_link 'Settings'
end
end
end
diff --git a/qa/qa/page/admin/settings.rb b/qa/qa/page/admin/settings.rb
new file mode 100644
index 00000000000..39e2f2062ad
--- /dev/null
+++ b/qa/qa/page/admin/settings.rb
@@ -0,0 +1,18 @@
+module QA
+ module Page
+ module Admin
+ class Settings < Page::Base
+ def enable_hashed_storage
+ scroll_to 'legend', text: 'Repository Storage'
+ check 'Create new projects using hashed storage paths'
+ end
+
+ def save_settings
+ scroll_to '.form-actions' do
+ click_button 'Save'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index d55326c5262..f9a93ef051e 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -1,3 +1,5 @@
+require 'capybara/dsl'
+
module QA
module Page
class Base
@@ -5,7 +7,22 @@ module QA
include Scenario::Actable
def refresh
- visit current_path
+ visit current_url
+ end
+
+ def scroll_to(selector, text: nil)
+ page.execute_script <<~JS
+ var elements = Array.from(document.querySelectorAll('#{selector}'));
+ var text = '#{text}';
+
+ if (text.length > 0) {
+ elements.find(e => e.textContent === text).scrollIntoView();
+ } else {
+ elements[0].scrollIntoView();
+ }
+ JS
+
+ page.within(selector) { yield } if block_given?
end
end
end
diff --git a/qa/qa/page/dashboard/groups.rb b/qa/qa/page/dashboard/groups.rb
index 3690f40dcfe..083d2e1ab16 100644
--- a/qa/qa/page/dashboard/groups.rb
+++ b/qa/qa/page/dashboard/groups.rb
@@ -2,19 +2,22 @@ module QA
module Page
module Dashboard
class Groups < Page::Base
- def prepare_test_namespace
- if page.has_content?(Runtime::Namespace.name)
- return click_link(Runtime::Namespace.name)
- end
+ def filter_by_name(name)
+ fill_in 'Filter by name...', with: name
+ end
- click_on 'New group'
+ def has_group?(name)
+ filter_by_name(name)
+
+ page.has_link?(name)
+ end
- fill_in 'group_path', with: Runtime::Namespace.name
- fill_in 'group_description',
- with: "QA test run at #{Runtime::Namespace.time}"
- choose 'Private'
+ def go_to_group(name)
+ click_link name
+ end
- click_button 'Create group'
+ def go_to_new_group
+ click_on 'New group'
end
end
end
diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb
new file mode 100644
index 00000000000..7ed27da6d89
--- /dev/null
+++ b/qa/qa/page/dashboard/projects.rb
@@ -0,0 +1,11 @@
+module QA
+ module Page
+ module Dashboard
+ class Projects < Page::Base
+ def go_to_project(name)
+ find_link(text: name).click
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb
new file mode 100644
index 00000000000..cb743a7bf11
--- /dev/null
+++ b/qa/qa/page/group/new.rb
@@ -0,0 +1,23 @@
+module QA
+ module Page
+ module Group
+ class New < Page::Base
+ def set_path(path)
+ fill_in 'group_path', with: path
+ end
+
+ def set_description(description)
+ fill_in 'group_description', with: description
+ end
+
+ def set_visibility(visibility)
+ choose visibility
+ end
+
+ def create
+ click_button 'Create group'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index 296c311d7c6..8080deda675 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -2,8 +2,28 @@ module QA
module Page
module Group
class Show < Page::Base
+ def go_to_subgroup(name)
+ click_link name
+ end
+
+ def has_subgroup?(name)
+ page.has_link?(name)
+ end
+
+ def go_to_new_subgroup
+ within '.new-project-subgroup' do
+ find('.dropdown-toggle').click
+ find("li[data-value='new-subgroup']").click
+ end
+ find("input[data-action='new-subgroup']").click
+ end
+
def go_to_new_project
- click_link 'New Project'
+ within '.new-project-subgroup' do
+ find('.dropdown-toggle').click
+ find("li[data-value='new-project']").click
+ end
+ find("input[data-action='new-project']").click
end
end
end
diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb
index a9810beeb29..ae6484b4bfe 100644
--- a/qa/qa/page/main/entry.rb
+++ b/qa/qa/page/main/entry.rb
@@ -2,29 +2,23 @@ module QA
module Page
module Main
class Entry < Page::Base
- def initialize
- visit('/')
+ def visit_login_page
+ visit("#{Runtime::Scenario.gitlab_address}/users/sign_in")
+ wait_for_instance_to_be_ready
+ end
+
+ private
+ def wait_for_instance_to_be_ready
# This resolves cold boot / background tasks problems
#
start = Time.now
while Time.now - start < 240
break if page.has_css?('.application', wait: 10)
- refresh
- end
- end
- def sign_in_using_credentials
- if page.has_content?('Change your password')
- fill_in :user_password, with: Runtime::User.password
- fill_in :user_password_confirmation, with: Runtime::User.password
- click_button 'Change your password'
+ refresh
end
-
- fill_in :user_login, with: Runtime::User.name
- fill_in :user_password, with: Runtime::User.password
- click_button 'Sign in'
end
end
end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
new file mode 100644
index 00000000000..8b0111a78a2
--- /dev/null
+++ b/qa/qa/page/main/login.rb
@@ -0,0 +1,19 @@
+module QA
+ module Page
+ module Main
+ class Login < Page::Base
+ def sign_in_using_credentials
+ if page.has_content?('Change your password')
+ fill_in :user_password, with: Runtime::User.password
+ fill_in :user_password_confirmation, with: Runtime::User.password
+ click_button 'Change your password'
+ end
+
+ fill_in :user_login, with: Runtime::User.name
+ fill_in :user_password, with: Runtime::User.password
+ click_button 'Sign in'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index 178c5ea6930..bc9c4ec1215 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -7,7 +7,10 @@ module QA
end
def go_to_projects
- within_top_menu { click_link 'Projects' }
+ within_top_menu do
+ click_link 'Projects'
+ click_link 'Your projects'
+ end
end
def go_to_admin_area
diff --git a/qa/qa/page/main/oauth.rb b/qa/qa/page/main/oauth.rb
new file mode 100644
index 00000000000..e746cff0a80
--- /dev/null
+++ b/qa/qa/page/main/oauth.rb
@@ -0,0 +1,15 @@
+module QA
+ module Page
+ module Main
+ class OAuth < Page::Base
+ def needs_authorization?
+ page.current_url.include?('/oauth')
+ end
+
+ def authorize!
+ click_button 'Authorize'
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/mattermost/login.rb b/qa/qa/page/mattermost/login.rb
new file mode 100644
index 00000000000..42ab9c6f675
--- /dev/null
+++ b/qa/qa/page/mattermost/login.rb
@@ -0,0 +1,19 @@
+module QA
+ module Page
+ module Mattermost
+ class Login < Page::Base
+ def initialize
+ visit(Runtime::Scenario.mattermost_address + '/login')
+ end
+
+ def sign_in_using_oauth
+ click_link class: 'btn btn-custom-login gitlab'
+
+ if page.has_content?('Authorize GitLab Mattermost to use your account?')
+ click_button 'Authorize'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/mattermost/main.rb b/qa/qa/page/mattermost/main.rb
new file mode 100644
index 00000000000..4b8fc28e53f
--- /dev/null
+++ b/qa/qa/page/mattermost/main.rb
@@ -0,0 +1,11 @@
+module QA
+ module Page
+ module Mattermost
+ class Main < Page::Base
+ def initialize
+ visit(Runtime::Scenario.mattermost_address)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 68d9597c4d2..3b2bac84f3f 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -14,6 +14,10 @@ module QA
find('#project_clone').value
end
+ def project_name
+ find('.project-title').text
+ end
+
def wait_for_push
sleep 5
end
diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb
index e4910b63a14..b00e925986b 100644
--- a/qa/qa/runtime/namespace.rb
+++ b/qa/qa/runtime/namespace.rb
@@ -8,7 +8,11 @@ module QA
end
def name
- 'qa_test_' + time.strftime('%d_%m_%Y_%H-%M-%S')
+ 'qa-test-' + time.strftime('%d-%m-%Y-%H-%M-%S')
+ end
+
+ def sandbox_name
+ 'gitlab-qa-sandbox'
end
end
end
diff --git a/qa/qa/runtime/scenario.rb b/qa/qa/runtime/scenario.rb
new file mode 100644
index 00000000000..7ef59046640
--- /dev/null
+++ b/qa/qa/runtime/scenario.rb
@@ -0,0 +1,28 @@
+module QA
+ module Runtime
+ ##
+ # Singleton approach to global test scenario arguments.
+ #
+ module Scenario
+ extend self
+
+ attr_reader :attributes
+
+ def define(attribute, value)
+ (@attributes ||= {}).store(attribute.to_sym, value)
+
+ define_singleton_method(attribute) do
+ @attributes[attribute.to_sym].tap do |value|
+ if value.to_s.empty?
+ raise ArgumentError, "Empty `#{attribute}` attribute!"
+ end
+ end
+ end
+ end
+
+ def method_missing(name, *)
+ raise ArgumentError, "Scenario attribute `#{name}` not defined!"
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/bootable.rb b/qa/qa/scenario/bootable.rb
new file mode 100644
index 00000000000..d6de4d404c8
--- /dev/null
+++ b/qa/qa/scenario/bootable.rb
@@ -0,0 +1,45 @@
+require 'optparse'
+
+module QA
+ module Scenario
+ module Bootable
+ Option = Struct.new(:name, :arg, :desc)
+
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def launch!(argv)
+ return self.perform(*argv) unless has_attributes?
+
+ arguments = OptionParser.new do |parser|
+ options.to_a.each do |opt|
+ parser.on(opt.arg, opt.desc) do |value|
+ Runtime::Scenario.define(opt.name, value)
+ end
+ end
+ end
+
+ arguments.parse!(argv)
+
+ self.perform(**Runtime::Scenario.attributes)
+ end
+
+ private
+
+ def attribute(name, arg, desc = '')
+ options.push(Option.new(name, arg, desc))
+ end
+
+ def options
+ @options ||= []
+ end
+
+ def has_attributes?
+ options.any?
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb
new file mode 100644
index 00000000000..b9d924651a0
--- /dev/null
+++ b/qa/qa/scenario/entrypoint.rb
@@ -0,0 +1,35 @@
+module QA
+ module Scenario
+ ##
+ # Base class for running the suite against any GitLab instance,
+ # including staging and on-premises installation.
+ #
+ class Entrypoint < Template
+ include Bootable
+
+ def perform(address, *files)
+ Specs::Config.act { configure_capybara! }
+ Runtime::Scenario.define(:gitlab_address, address)
+
+ ##
+ # Perform before hooks, which are different for CE and EE
+ #
+ Runtime::Release.perform_before_hooks
+
+ Specs::Runner.perform do |specs|
+ specs.tty = true
+ specs.tags = self.class.get_tags
+ specs.files = files.any? ? files : 'qa/specs/features'
+ end
+ end
+
+ def self.tags(*tags)
+ @tags = tags
+ end
+
+ def self.get_tags
+ @tags
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/gitlab/admin/hashed_storage.rb b/qa/qa/scenario/gitlab/admin/hashed_storage.rb
new file mode 100644
index 00000000000..ac2cd549829
--- /dev/null
+++ b/qa/qa/scenario/gitlab/admin/hashed_storage.rb
@@ -0,0 +1,25 @@
+module QA
+ module Scenario
+ module Gitlab
+ module Admin
+ class HashedStorage < Scenario::Template
+ def perform(*traits)
+ raise ArgumentError unless traits.include?(:enabled)
+
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
+ Page::Main::Menu.act { go_to_admin_area }
+ Page::Admin::Menu.act { go_to_settings }
+
+ Page::Admin::Settings.act do
+ enable_hashed_storage
+ save_settings
+ end
+
+ QA::Page::Main::Menu.act { sign_out }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/gitlab/group/create.rb b/qa/qa/scenario/gitlab/group/create.rb
new file mode 100644
index 00000000000..8e6c7c7ad80
--- /dev/null
+++ b/qa/qa/scenario/gitlab/group/create.rb
@@ -0,0 +1,27 @@
+require 'securerandom'
+
+module QA
+ module Scenario
+ module Gitlab
+ module Group
+ class Create < Scenario::Template
+ attr_writer :path, :description
+
+ def initialize
+ @path = Runtime::Namespace.name
+ @description = "QA test run at #{Runtime::Namespace.time}"
+ end
+
+ def perform
+ Page::Group::New.perform do |group|
+ group.set_path(@path)
+ group.set_description(@description)
+ group.set_visibility('Private')
+ group.create
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/gitlab/project/create.rb b/qa/qa/scenario/gitlab/project/create.rb
index b860701c304..bb3b9e19c0f 100644
--- a/qa/qa/scenario/gitlab/project/create.rb
+++ b/qa/qa/scenario/gitlab/project/create.rb
@@ -12,9 +12,21 @@ module QA
end
def perform
- Page::Main::Menu.act { go_to_groups }
- Page::Dashboard::Groups.act { prepare_test_namespace }
- Page::Group::Show.act { go_to_new_project }
+ Scenario::Gitlab::Sandbox::Prepare.perform
+
+ Page::Group::Show.perform do |page|
+ if page.has_subgroup?(Runtime::Namespace.name)
+ page.go_to_subgroup(Runtime::Namespace.name)
+ else
+ page.go_to_new_subgroup
+
+ Scenario::Gitlab::Group::Create.perform do |group|
+ group.path = Runtime::Namespace.name
+ end
+ end
+
+ page.go_to_new_project
+ end
Page::Project::New.perform do |page|
page.choose_test_namespace
diff --git a/qa/qa/scenario/gitlab/sandbox/prepare.rb b/qa/qa/scenario/gitlab/sandbox/prepare.rb
new file mode 100644
index 00000000000..990de456e20
--- /dev/null
+++ b/qa/qa/scenario/gitlab/sandbox/prepare.rb
@@ -0,0 +1,28 @@
+module QA
+ module Scenario
+ module Gitlab
+ module Sandbox
+ # Ensure we're in our sandbox namespace, either by navigating to it or
+ # by creating it if it doesn't yet exist
+ class Prepare < Scenario::Template
+ def perform
+ Page::Main::Menu.act { go_to_groups }
+
+ Page::Dashboard::Groups.perform do |page|
+ if page.has_group?(Runtime::Namespace.sandbox_name)
+ page.go_to_group(Runtime::Namespace.sandbox_name)
+ else
+ page.go_to_new_group
+
+ Scenario::Gitlab::Group::Create.perform do |group|
+ group.path = Runtime::Namespace.sandbox_name
+ group.description = 'QA sandbox'
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb
index 689292bc60b..e2a1f6bf2bd 100644
--- a/qa/qa/scenario/test/instance.rb
+++ b/qa/qa/scenario/test/instance.rb
@@ -5,21 +5,8 @@ module QA
# Run test suite against any GitLab instance,
# including staging and on-premises installation.
#
- class Instance < Scenario::Template
- def perform(address, *files)
- Specs::Config.perform do |specs|
- specs.address = address
- end
-
- ##
- # Perform before hooks, which are different for CE and EE
- #
- Runtime::Release.perform_before_hooks
-
- Specs::Runner.perform do |specs|
- specs.rspec('--tty', files.any? ? files : 'qa/specs/features')
- end
- end
+ class Instance < Entrypoint
+ tags :core
end
end
end
diff --git a/qa/qa/scenario/test/integration/mattermost.rb b/qa/qa/scenario/test/integration/mattermost.rb
new file mode 100644
index 00000000000..7d0702afdb1
--- /dev/null
+++ b/qa/qa/scenario/test/integration/mattermost.rb
@@ -0,0 +1,21 @@
+module QA
+ module Scenario
+ module Test
+ module Integration
+ ##
+ # Run test suite against any GitLab instance where mattermost is enabled,
+ # including staging and on-premises installation.
+ #
+ class Mattermost < Scenario::Entrypoint
+ tags :core, :mattermost
+
+ def perform(address, mattermost, *files)
+ Runtime::Scenario.define(:mattermost_address, mattermost)
+
+ super(address, *files)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/shell/omnibus.rb b/qa/qa/shell/omnibus.rb
new file mode 100644
index 00000000000..6b3628d3109
--- /dev/null
+++ b/qa/qa/shell/omnibus.rb
@@ -0,0 +1,39 @@
+require 'open3'
+
+module QA
+ module Shell
+ class Omnibus
+ include Scenario::Actable
+
+ def initialize(container)
+ @name = container
+ end
+
+ def gitlab_ctl(command, input: nil)
+ if input.nil?
+ shell "docker exec #{@name} gitlab-ctl #{command}"
+ else
+ shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'"
+ end
+ end
+
+ private
+
+ ##
+ # TODO, make it possible to use generic QA framework classes
+ # as a library - gitlab-org/gitlab-qa#94
+ #
+ def shell(command)
+ puts "Executing `#{command}`"
+
+ Open3.popen2e(command) do |_in, out, wait|
+ out.each { |line| puts line }
+
+ if wait.value.exited? && wait.value.exitstatus.nonzero?
+ raise "Docker command `#{command}` failed!"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb
index 79c681168cc..bce7923e52d 100644
--- a/qa/qa/specs/config.rb
+++ b/qa/qa/specs/config.rb
@@ -9,15 +9,9 @@ require 'selenium-webdriver'
module QA
module Specs
class Config < Scenario::Template
- attr_writer :address
-
- def initialize
- @address = ENV['GITLAB_URL']
- end
+ include Scenario::Actable
def perform
- raise 'Please configure GitLab address!' unless @address
-
configure_rspec!
configure_capybara!
end
@@ -56,10 +50,9 @@ module QA
end
Capybara.configure do |config|
- config.app_host = @address
config.default_driver = :chrome
config.javascript_driver = :chrome
- config.default_max_wait_time = 4
+ config.default_max_wait_time = 10
# https://github.com/mattheworiordan/capybara-screenshot/issues/164
config.save_path = 'tmp'
diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb
index 8e1ae6efa47..b155708c387 100644
--- a/qa/qa/specs/features/login/standard_spec.rb
+++ b/qa/qa/specs/features/login/standard_spec.rb
@@ -1,7 +1,8 @@
module QA
- feature 'standard root login' do
+ feature 'standard root login', :core do
scenario 'user logs in using credentials' do
- Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
# TODO, since `Signed in successfully` message was removed
# this is the only way to tell if user is signed in correctly.
diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb
new file mode 100644
index 00000000000..853a9a6a4f4
--- /dev/null
+++ b/qa/qa/specs/features/mattermost/group_create_spec.rb
@@ -0,0 +1,17 @@
+module QA
+ feature 'create a new group', :mattermost do
+ scenario 'creating a group with a mattermost team' do
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
+ Page::Main::Menu.act { go_to_groups }
+
+ Page::Dashboard::Groups.perform do |page|
+ page.go_to_new_group
+
+ expect(page).to have_content(
+ /Create a Mattermost team for this group/
+ )
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb
new file mode 100644
index 00000000000..1fde3f89a99
--- /dev/null
+++ b/qa/qa/specs/features/mattermost/login_spec.rb
@@ -0,0 +1,24 @@
+module QA
+ feature 'logging in to Mattermost', :mattermost do
+ scenario 'can use gitlab oauth' do
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
+ Page::Mattermost::Login.act { sign_in_using_oauth }
+
+ Page::Mattermost::Main.perform do |page|
+ expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/)
+ end
+ end
+
+ ##
+ # TODO, temporary workaround for gitlab-org/gitlab-qa#102.
+ #
+ after do
+ visit Runtime::Scenario.mattermost_address
+ reset_session!
+
+ visit Runtime::Scenario.gitlab_address
+ reset_session!
+ end
+ end
+end
diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb
index 610492b9717..aba0c2b4c14 100644
--- a/qa/qa/specs/features/project/create_spec.rb
+++ b/qa/qa/specs/features/project/create_spec.rb
@@ -1,7 +1,8 @@
module QA
- feature 'create a new project' do
+ feature 'create a new project', :core do
scenario 'user creates a new project' do
- Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |project|
project.name = 'awesome-project'
diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb
index 521bd955857..5cc3b3b9c1b 100644
--- a/qa/qa/specs/features/repository/clone_spec.rb
+++ b/qa/qa/specs/features/repository/clone_spec.rb
@@ -1,5 +1,5 @@
module QA
- feature 'clone code from the repository' do
+ feature 'clone code from the repository', :core do
context 'with regular account over http' do
given(:location) do
Page::Project::Show.act do
@@ -9,7 +9,8 @@ module QA
end
before do
- Page::Main::Entry.act { sign_in_using_credentials }
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |scenario|
scenario.name = 'project-with-code'
diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb
index 5fe45d63d37..30935dc1e13 100644
--- a/qa/qa/specs/features/repository/push_spec.rb
+++ b/qa/qa/specs/features/repository/push_spec.rb
@@ -1,8 +1,9 @@
module QA
- feature 'push code to repository' do
+ feature 'push code to repository', :core do
context 'with regular account over http' do
- scenario 'user pushes code to the repository' do
- Page::Main::Entry.act { sign_in_using_credentials }
+ scenario 'user pushes code to the repository' do
+ Page::Main::Entry.act { visit_login_page }
+ Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |scenario|
scenario.name = 'project_with_code'
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index 83ae15d0995..f98b8f88e9a 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -2,10 +2,23 @@ require 'rspec/core'
module QA
module Specs
- class Runner
- include Scenario::Actable
+ class Runner < Scenario::Template
+ attr_accessor :tty, :tags, :files
+
+ def initialize
+ @tty = false
+ @tags = []
+ @files = ['qa/specs/features']
+ end
+
+ def perform
+ args = []
+ args.push('--tty') if tty
+ tags.to_a.each { |tag| args.push(['-t', tag.to_s]) }
+ args.push(files)
+
+ Specs::Config.perform
- def rspec(*args)
RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
abort if status.nonzero?
end
diff --git a/qa/spec/runtime/scenario_spec.rb b/qa/spec/runtime/scenario_spec.rb
new file mode 100644
index 00000000000..7009192bcc0
--- /dev/null
+++ b/qa/spec/runtime/scenario_spec.rb
@@ -0,0 +1,27 @@
+describe QA::Runtime::Scenario do
+ subject do
+ Module.new.extend(described_class)
+ end
+
+ it 'makes it possible to define global scenario attributes' do
+ subject.define(:my_attribute, 'some-value')
+ subject.define(:another_attribute, 'another-value')
+
+ expect(subject.my_attribute).to eq 'some-value'
+ expect(subject.another_attribute).to eq 'another-value'
+ expect(subject.attributes)
+ .to eq(my_attribute: 'some-value', another_attribute: 'another-value')
+ end
+
+ it 'raises error when attribute is not known' do
+ expect { subject.invalid_accessor }
+ .to raise_error ArgumentError, /invalid_accessor/
+ end
+
+ it 'raises error when attribute is empty' do
+ subject.define(:empty_attribute, '')
+
+ expect { subject.empty_attribute }
+ .to raise_error ArgumentError, /empty_attribute/
+ end
+end
diff --git a/qa/spec/scenario/bootable_spec.rb b/qa/spec/scenario/bootable_spec.rb
new file mode 100644
index 00000000000..273aac7677e
--- /dev/null
+++ b/qa/spec/scenario/bootable_spec.rb
@@ -0,0 +1,24 @@
+describe QA::Scenario::Bootable do
+ subject do
+ Class.new(QA::Scenario::Template)
+ .include(described_class)
+ end
+
+ it 'makes it possible to define the scenario attribute' do
+ subject.class_eval do
+ attribute :something, '--something SOMETHING', 'Some attribute'
+ attribute :another, '--another ANOTHER', 'Some other attribute'
+ end
+
+ expect(subject).to receive(:perform)
+ .with(something: 'test', another: 'other')
+
+ subject.launch!(%w[--another other --something test])
+ end
+
+ it 'does not require attributes to be defined' do
+ expect(subject).to receive(:perform).with('some', 'argv')
+
+ subject.launch!(%w[some argv])
+ end
+end
diff --git a/qa/spec/scenario/entrypoint_spec.rb b/qa/spec/scenario/entrypoint_spec.rb
new file mode 100644
index 00000000000..aec79dcea04
--- /dev/null
+++ b/qa/spec/scenario/entrypoint_spec.rb
@@ -0,0 +1,44 @@
+describe QA::Scenario::Entrypoint do
+ subject do
+ Class.new(QA::Scenario::Entrypoint) do
+ tags :rspec
+ end
+ end
+
+ context '#perform' do
+ let(:arguments) { spy('Runtime::Scenario') }
+ let(:release) { spy('Runtime::Release') }
+ let(:runner) { spy('Specs::Runner') }
+
+ before do
+ stub_const('QA::Runtime::Release', release)
+ stub_const('QA::Runtime::Scenario', arguments)
+ stub_const('QA::Specs::Runner', runner)
+
+ allow(runner).to receive(:perform).and_yield(runner)
+ end
+
+ it 'sets an address of the subject' do
+ subject.perform("hello")
+
+ expect(arguments).to have_received(:define)
+ .with(:gitlab_address, "hello")
+ end
+
+ context 'no paths' do
+ it 'should call runner with default arguments' do
+ subject.perform("test")
+
+ expect(runner).to have_received(:files=).with('qa/specs/features')
+ end
+ end
+
+ context 'specifying paths' do
+ it 'should call runner with paths' do
+ subject.perform('test', 'path1', 'path2')
+
+ expect(runner).to have_received(:files=).with(%w[path1 path2])
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/line_break_after_guard_clauses.rb b/rubocop/cop/line_break_after_guard_clauses.rb
new file mode 100644
index 00000000000..67477f064ab
--- /dev/null
+++ b/rubocop/cop/line_break_after_guard_clauses.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ # Ensures a line break after guard clauses.
+ #
+ # @example
+ # # bad
+ # return unless condition
+ # do_stuff
+ #
+ # # good
+ # return unless condition
+ #
+ # do_stuff
+ #
+ # # bad
+ # raise if condition
+ # do_stuff
+ #
+ # # good
+ # raise if condition
+ #
+ # do_stuff
+ #
+ # Multiple guard clauses are allowed without
+ # line break.
+ #
+ # # good
+ # return unless condition_a
+ # return unless condition_b
+ #
+ # do_stuff
+ #
+ # Guard clauses in case statement are allowed without
+ # line break.
+ #
+ # # good
+ # case model
+ # when condition_a
+ # return true unless condition_b
+ # when
+ # ...
+ # end
+ #
+ # Guard clauses before end are allowed without
+ # line break.
+ #
+ # # good
+ # if condition_a
+ # do_something
+ # else
+ # do_something_else
+ # return unless condition
+ # end
+ #
+ # do_something_more
+ class LineBreakAfterGuardClauses < RuboCop::Cop::Cop
+ MSG = 'Add a line break after guard clauses'
+
+ def_node_matcher :guard_clause_node?, <<-PATTERN
+ [{(send nil? {:raise :fail :throw} ...) return break next} single_line?]
+ PATTERN
+
+ def on_if(node)
+ return unless node.single_line?
+ return unless guard_clause?(node)
+ return if next_line(node).blank? || clause_last_line?(next_line(node)) || guard_clause?(next_sibling(node))
+
+ add_offense(node, :expression, MSG)
+ end
+
+ def autocorrect(node)
+ lambda do |corrector|
+ corrector.insert_after(node.loc.expression, "\n")
+ end
+ end
+
+ private
+
+ def guard_clause?(node)
+ return false unless node.if_type?
+
+ guard_clause_node?(node.if_branch)
+ end
+
+ def next_sibling(node)
+ node.parent.children[node.sibling_index + 1]
+ end
+
+ def next_line(node)
+ processed_source[node.loc.line]
+ end
+
+ def clause_last_line?(line)
+ line =~ /^\s*(?:end|elsif|else|when|rescue|ensure)/
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/add_column_with_default_to_large_table.rb b/rubocop/cop/migration/add_column_with_default_to_large_table.rb
deleted file mode 100644
index fb363f95b56..00000000000
--- a/rubocop/cop/migration/add_column_with_default_to_large_table.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-require_relative '../../migration_helpers'
-
-module RuboCop
- module Cop
- module Migration
- # This cop checks for `add_column_with_default` on a table that's been
- # explicitly blacklisted because of its size.
- #
- # Even though this helper performs the update in batches to avoid
- # downtime, using it with tables with millions of rows still causes a
- # significant delay in the deploy process and is best avoided.
- #
- # See https://gitlab.com/gitlab-com/infrastructure/issues/1602 for more
- # information.
- class AddColumnWithDefaultToLargeTable < RuboCop::Cop::Cop
- include MigrationHelpers
-
- MSG = 'Using `add_column_with_default` on the `%s` table will take a ' \
- 'long time to complete, and should be avoided unless absolutely ' \
- 'necessary'.freeze
-
- LARGE_TABLES = %i[
- ci_pipelines
- ci_builds
- events
- issues
- merge_request_diff_files
- merge_request_diffs
- merge_requests
- namespaces
- notes
- projects
- routes
- users
- ].freeze
-
- def_node_matcher :add_column_with_default?, <<~PATTERN
- (send nil :add_column_with_default $(sym ...) ...)
- PATTERN
-
- def on_send(node)
- return unless in_migration?(node)
-
- matched = add_column_with_default?(node)
- return unless matched
-
- table = matched.to_a.first
- return unless LARGE_TABLES.include?(table)
-
- add_offense(node, :expression, format(MSG, table))
- end
- end
- end
- end
-end
diff --git a/rubocop/cop/migration/datetime.rb b/rubocop/cop/migration/datetime.rb
index 651935dd53e..9cba3c35b26 100644
--- a/rubocop/cop/migration/datetime.rb
+++ b/rubocop/cop/migration/datetime.rb
@@ -7,14 +7,18 @@ module RuboCop
class Datetime < RuboCop::Cop::Cop
include MigrationHelpers
- MSG = 'Do not use the `datetime` data type, use `datetime_with_timezone` instead'.freeze
+ MSG = 'Do not use the `%s` data type, use `datetime_with_timezone` instead'.freeze
# Check methods in table creation.
def on_def(node)
return unless in_migration?(node)
node.each_descendant(:send) do |send_node|
- add_offense(send_node, :selector) if method_name(send_node) == :datetime
+ method_name = node.children[1]
+
+ if method_name == :datetime || method_name == :timestamp
+ add_offense(send_node, :selector, format(MSG, method_name))
+ end
end
end
@@ -23,12 +27,14 @@ module RuboCop
return unless in_migration?(node)
node.each_descendant do |descendant|
- add_offense(node, :expression) if descendant.type == :sym && descendant.children.last == :datetime
- end
- end
+ next unless descendant.type == :sym
- def method_name(node)
- node.children[1]
+ last_argument = descendant.children.last
+
+ if last_argument == :datetime || last_argument == :timestamp
+ add_offense(node, :expression, format(MSG, last_argument))
+ end
+ end
end
end
end
diff --git a/rubocop/cop/migration/update_large_table.rb b/rubocop/cop/migration/update_large_table.rb
new file mode 100644
index 00000000000..3ae3fb1b68e
--- /dev/null
+++ b/rubocop/cop/migration/update_large_table.rb
@@ -0,0 +1,57 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # This cop checks for `add_column_with_default` on a table that's been
+ # explicitly blacklisted because of its size.
+ #
+ # Even though this helper performs the update in batches to avoid
+ # downtime, using it with tables with millions of rows still causes a
+ # significant delay in the deploy process and is best avoided.
+ #
+ # See https://gitlab.com/gitlab-com/infrastructure/issues/1602 for more
+ # information.
+ class UpdateLargeTable < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = 'Using `%s` on the `%s` table will take a long time to ' \
+ 'complete, and should be avoided unless absolutely ' \
+ 'necessary'.freeze
+
+ LARGE_TABLES = %i[
+ ci_pipelines
+ ci_builds
+ events
+ issues
+ merge_request_diff_files
+ merge_request_diffs
+ merge_requests
+ namespaces
+ notes
+ projects
+ routes
+ users
+ ].freeze
+
+ def_node_matcher :batch_update?, <<~PATTERN
+ (send nil ${:add_column_with_default :update_column_in_batches} $(sym ...) ...)
+ PATTERN
+
+ def on_send(node)
+ return unless in_migration?(node)
+
+ matches = batch_update?(node)
+ return unless matches
+
+ update_method = matches.first
+ table = matches.last.to_a.first
+
+ return unless LARGE_TABLES.include?(table)
+
+ add_offense(node, :expression, format(MSG, update_method, table))
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/rspec/env_assignment.rb b/rubocop/cop/rspec/env_assignment.rb
new file mode 100644
index 00000000000..257454af0e1
--- /dev/null
+++ b/rubocop/cop/rspec/env_assignment.rb
@@ -0,0 +1,58 @@
+require 'rubocop-rspec'
+require_relative '../../spec_helpers'
+
+module RuboCop
+ module Cop
+ module RSpec
+ # This cop checks for ENV assignment in specs
+ #
+ # @example
+ #
+ # # bad
+ # before do
+ # ENV['FOO'] = 'bar'
+ # end
+ #
+ # # good
+ # before do
+ # stub_env('FOO', 'bar')
+ # end
+ class EnvAssignment < Cop
+ include SpecHelpers
+
+ MESSAGE = "Don't assign to ENV, use `stub_env` instead.".freeze
+
+ def_node_search :env_assignment?, <<~PATTERN
+ (send (const nil? :ENV) :[]= ...)
+ PATTERN
+
+ # Following is what node.children looks like on a match
+ # [s(:const, nil, :ENV), :[]=, s(:str, "key"), s(:str, "value")]
+ def on_send(node)
+ return unless in_spec?(node)
+ return unless env_assignment?(node)
+
+ add_offense(node, :expression, MESSAGE)
+ end
+
+ def autocorrect(node)
+ lambda do |corrector|
+ corrector.replace(node.loc.expression, stub_env(env_key(node), env_value(node)))
+ end
+ end
+
+ def env_key(node)
+ node.children[2].source
+ end
+
+ def env_value(node)
+ node.children[3].source
+ end
+
+ def stub_env(key, value)
+ "stub_env(#{key}, #{value})"
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/rspec/verbose_include_metadata.rb b/rubocop/cop/rspec/verbose_include_metadata.rb
new file mode 100644
index 00000000000..58390622d60
--- /dev/null
+++ b/rubocop/cop/rspec/verbose_include_metadata.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'rubocop-rspec'
+
+module RuboCop
+ module Cop
+ module RSpec
+ # Checks for verbose include metadata used in the specs.
+ #
+ # @example
+ # # bad
+ # describe MyClass, js: true do
+ # end
+ #
+ # # good
+ # describe MyClass, :js do
+ # end
+ class VerboseIncludeMetadata < Cop
+ MSG = 'Use `%s` instead of `%s`.'
+
+ SELECTORS = %i[describe context feature example_group it specify example scenario its].freeze
+
+ def_node_matcher :include_metadata, <<-PATTERN
+ (send {(const nil :RSpec) nil} {#{SELECTORS.map(&:inspect).join(' ')}}
+ !const
+ ...
+ (hash $...))
+ PATTERN
+
+ def_node_matcher :invalid_metadata?, <<-PATTERN
+ (pair
+ (sym $...)
+ (true))
+ PATTERN
+
+ def on_send(node)
+ invalid_metadata_matches(node) do |match|
+ add_offense(node, :expression, format(MSG, good(match), bad(match)))
+ end
+ end
+
+ def autocorrect(node)
+ lambda do |corrector|
+ invalid_metadata_matches(node) do |match|
+ corrector.replace(match.loc.expression, good(match))
+ end
+ end
+ end
+
+ private
+
+ def invalid_metadata_matches(node)
+ include_metadata(node) do |matches|
+ matches.select(&method(:invalid_metadata?)).each do |match|
+ yield match
+ end
+ end
+ end
+
+ def bad(match)
+ "#{metadata_key(match)}: true"
+ end
+
+ def good(match)
+ ":#{metadata_key(match)}"
+ end
+
+ def metadata_key(match)
+ match.children[0].source
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 1b6e8991a17..7621ea50da9 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,23 +1,26 @@
+require_relative 'cop/active_record_dependent'
+require_relative 'cop/active_record_serialize'
require_relative 'cop/custom_error_class'
require_relative 'cop/gem_fetcher'
-require_relative 'cop/active_record_serialize'
-require_relative 'cop/redirect_with_status'
+require_relative 'cop/in_batches'
+require_relative 'cop/line_break_after_guard_clauses'
require_relative 'cop/polymorphic_associations'
require_relative 'cop/project_path_helper'
-require_relative 'cop/active_record_dependent'
-require_relative 'cop/in_batches'
+require_relative 'cop/redirect_with_status'
require_relative 'cop/migration/add_column'
-require_relative 'cop/migration/add_column_with_default_to_large_table'
require_relative 'cop/migration/add_concurrent_foreign_key'
require_relative 'cop/migration/add_concurrent_index'
require_relative 'cop/migration/add_index'
require_relative 'cop/migration/add_timestamps'
require_relative 'cop/migration/datetime'
-require_relative 'cop/migration/safer_boolean_column'
require_relative 'cop/migration/hash_index'
require_relative 'cop/migration/remove_concurrent_index'
require_relative 'cop/migration/remove_index'
require_relative 'cop/migration/reversible_add_column_with_default'
+require_relative 'cop/migration/safer_boolean_column'
require_relative 'cop/migration/timestamps'
require_relative 'cop/migration/update_column_in_batches'
+require_relative 'cop/migration/update_large_table'
+require_relative 'cop/rspec/env_assignment'
require_relative 'cop/rspec/single_line_hook'
+require_relative 'cop/rspec/verbose_include_metadata'
diff --git a/rubocop/spec_helpers.rb b/rubocop/spec_helpers.rb
new file mode 100644
index 00000000000..a702a083958
--- /dev/null
+++ b/rubocop/spec_helpers.rb
@@ -0,0 +1,12 @@
+module RuboCop
+ module SpecHelpers
+ SPEC_HELPERS = %w[spec_helper.rb rails_helper.rb].freeze
+
+ # Returns true if the given node originated from the spec directory.
+ def in_spec?(node)
+ path = node.location.expression.source_buffer.name
+
+ !SPEC_HELPERS.include?(File.basename(path)) && path.start_with?(File.join(Dir.pwd, 'spec'))
+ end
+ end
+end
diff --git a/scripts/create_mysql_user.sh b/scripts/create_mysql_user.sh
new file mode 100644
index 00000000000..28f6cfb50ae
--- /dev/null
+++ b/scripts/create_mysql_user.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+mysql --user=root --host=mysql <<EOF
+CREATE DATABASE IF NOT EXISTS gitlabhq_test;
+CREATE USER IF NOT EXISTS 'gitlab'@'%';
+GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%';
+FLUSH PRIVILEGES;
+EOF
diff --git a/scripts/create_postgres_user.sh b/scripts/create_postgres_user.sh
new file mode 100644
index 00000000000..8a744df3226
--- /dev/null
+++ b/scripts/create_postgres_user.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+psql -h postgres -U postgres postgres <<EOF
+DROP DATABASE IF EXISTS gitlabhq_test;
+CREATE DATABASE gitlabhq_test;
+CREATE USER gitlab;
+GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab;
+EOF
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 39806901274..ea406aadf39 100644
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -1,6 +1,7 @@
. scripts/utils.sh
export SETUP_DB=${SETUP_DB:-true}
+export CREATE_DB_USER=${CREATE_DB_USER:-$SETUP_DB}
export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true}
export BUNDLE_INSTALL_FLAGS="--without production --jobs $(nproc) --path vendor --retry 3 --quiet"
@@ -13,6 +14,7 @@ fi
retry gem install knapsack
cp config/gitlab.yml.example config/gitlab.yml
+sed -i 's/bin_path: \/usr\/bin\/git/bin_path: \/usr\/local\/bin\/git/' config/gitlab.yml
# Determine the database by looking at the job name.
# For example, we'll get pg if the job is `rspec-pg 19 20`
@@ -26,12 +28,13 @@ fi
cp config/database.yml.$GITLAB_DATABASE config/database.yml
+# Set user to a non-superuser to ensure we test permissions
+sed -i 's/username: root/username: gitlab/g' config/database.yml
+
if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
- sed -i 's/# host:.*/host: postgres/g' config/database.yml
+ sed -i 's/localhost/postgres/g' config/database.yml
else # Assume it's mysql
- sed -i 's/username:.*/username: root/g' config/database.yml
- sed -i 's/password:.*/password:/g' config/database.yml
- sed -i 's/# host:.*/host: mysql/g' config/database.yml
+ sed -i 's/localhost/mysql/g' config/database.yml
fi
cp config/resque.yml.example config/resque.yml
@@ -46,6 +49,16 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml
cp config/redis.shared_state.yml.example config/redis.shared_state.yml
sed -i 's/localhost/redis/g' config/redis.shared_state.yml
+# Some tasks (e.g. db:seed_fu) need to have a properly-configured database
+# user but not necessarily a full schema loaded
+if [ "$CREATE_DB_USER" != "false" ]; then
+ if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
+ . scripts/create_postgres_user.sh
+ else
+ . scripts/create_mysql_user.sh
+ fi
+fi
+
if [ "$SETUP_DB" != "false" ]; then
bundle exec rake db:drop db:create db:schema:load db:migrate
diff --git a/scripts/static-analysis b/scripts/static-analysis
index aeefb2bc96f..51a2fd81a79 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -11,31 +11,40 @@ tasks = [
%w[bundle exec rake brakeman],
%w[bundle exec license_finder],
%w[yarn run eslint],
- %w[bundle exec rubocop --require rubocop-rspec],
+ %w[bundle exec rubocop --parallel],
%w[scripts/lint-conflicts.sh],
%w[bundle exec rake gettext:lint],
%w[scripts/lint-changelog-yaml]
]
failed_tasks = tasks.reduce({}) do |failures, task|
- output, status = Gitlab::Popen.popen(task)
+ start = Time.now
+ puts
+ puts "$ #{task.join(' ')}"
- puts "Running: #{task.join(' ')}"
- puts output
+ output, status = Gitlab::Popen.popen(task)
+ puts "==> Finished in #{Time.now - start} seconds"
+ puts
failures[task.join(' ')] = output unless status.zero?
failures
end
+puts
+puts '==================================================='
+puts
+puts
+
if failed_tasks.empty?
puts 'All static analyses passed successfully.'
else
- puts "\n===================================================\n\n"
- puts "Some static analyses failed:"
+ puts 'Some static analyses failed:'
failed_tasks.each do |failed_task, output|
- puts "\n**** #{failed_task} failed with the following error:\n\n"
+ puts
+ puts "**** #{failed_task} failed with the following error:"
+ puts
puts output
end
diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs
index d3a9f5ff4ea..a270823b857 100755
--- a/scripts/trigger-build-docs
+++ b/scripts/trigger-build-docs
@@ -27,14 +27,7 @@ def docs_branch
# Prefix the remote branch with 'preview-' in order to avoid
# name conflicts in the rare case the branch name already
# exists in the docs repo and truncate to max length.
- "preview-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max]
-end
-
-#
-# Dummy way to find out in which repo we are, CE or EE
-#
-def ee?
- File.exist?('CHANGELOG-EE.md')
+ "#{slug}-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max]
end
#
@@ -56,14 +49,34 @@ def remove_remote_branch
end
#
+# Define suffix in review app URL based on project
+#
+def slug
+ case ENV["CI_PROJECT_NAME"]
+ when 'gitlab-ce'
+ 'ce'
+ when 'gitlab-ee'
+ 'ee'
+ when 'gitlab-runner'
+ 'runner'
+ when 'omnibus-gitlab'
+ 'omnibus'
+ end
+end
+
+#
+# Overriding vars in https://gitlab.com/gitlab-com/gitlab-docs/blob/master/.gitlab-ci.yml
+#
+def param_name
+ "BRANCH_#{slug.upcase}"
+end
+
+#
# Trigger a pipeline in gitlab-docs
#
def trigger_pipeline
- # Overriding vars in https://gitlab.com/gitlab-com/gitlab-docs/blob/master/.gitlab-ci.yml
- param_name = ee? ? 'BRANCH_EE' : 'BRANCH_CE'
-
# The review app URL
- app_url = "http://#{docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}"
+ app_url = "http://#{docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{slug}"
# Create the pipeline
puts "=> Triggering a pipeline..."
diff --git a/scripts/trigger-build-omnibus b/scripts/trigger-build-omnibus
index dcda70d7ed8..0c662ac19d2 100755
--- a/scripts/trigger-build-omnibus
+++ b/scripts/trigger-build-omnibus
@@ -2,26 +2,95 @@
require 'net/http'
require 'json'
+require 'cgi'
-uri = URI('https://gitlab.com/api/v4/projects/20699/trigger/pipeline')
-params = {
- "ref" => ENV["OMNIBUS_BRANCH"] || "master",
- "token" => ENV["BUILD_TRIGGER_TOKEN"],
- "variables[GITLAB_VERSION]" => ENV["CI_COMMIT_SHA"],
- "variables[ALTERNATIVE_SOURCES]" => true,
- "variables[ee]" => ENV["EE_PACKAGE"] || "false"
-}
-
-Dir.glob("*_VERSION").each do |version_file|
- params["variables[#{version_file}]"] = File.read(version_file).strip
-end
+module Omnibus
+ PROJECT_PATH = 'gitlab-org/omnibus-gitlab'.freeze
+
+ class Trigger
+ TOKEN = ENV['BUILD_TRIGGER_TOKEN']
+
+ def initialize
+ @uri = URI("https://gitlab.com/api/v4/projects/#{CGI.escape(Omnibus::PROJECT_PATH)}/trigger/pipeline")
+ @params = env_params.merge(file_params).merge(token: TOKEN)
+ end
+
+ def invoke!
+ res = Net::HTTP.post_form(@uri, @params)
+ id = JSON.parse(res.body)['id']
+
+ if id
+ puts "Triggered https://gitlab.com/#{Omnibus::PROJECT_PATH}/pipelines/#{id}"
+ else
+ raise "Trigger failed! The response from the trigger is: #{res.body}"
+ end
+
+ Omnibus::Pipeline.new(id)
+ end
+
+ private
+
+ def env_params
+ {
+ "ref" => ENV["OMNIBUS_BRANCH"] || "master",
+ "variables[GITLAB_VERSION]" => ENV["CI_COMMIT_SHA"],
+ "variables[ALTERNATIVE_SOURCES]" => true,
+ "variables[ee]" => ENV["EE_PACKAGE"] || "false"
+ }
+ end
+
+ def file_params
+ Hash.new.tap do |params|
+ Dir.glob("*_VERSION").each do |version_file|
+ params["variables[#{version_file}]"] = File.read(version_file).strip
+ end
+ end
+ end
+ end
-res = Net::HTTP.post_form(uri, params)
-pipeline_id = JSON.parse(res.body)['id']
+ class Pipeline
+ INTERVAL = 60 # seconds
+ MAX_DURATION = 3600 * 3 # 3 hours
-unless pipeline_id.nil?
- puts "Triggered pipeline can be found at https://gitlab.com/gitlab-org/omnibus-gitlab/pipelines/#{pipeline_id}"
-else
- puts "Trigger failed. The response from trigger is: "
- puts res.body
+ def initialize(id)
+ @start = Time.now.to_i
+ @uri = URI("https://gitlab.com/api/v4/projects/#{CGI.escape(Omnibus::PROJECT_PATH)}/pipelines/#{id}")
+ end
+
+ def wait!
+ loop do
+ raise 'Pipeline timeout!' if timeout?
+
+ case status
+ when :pending, :running
+ puts "Waiting another #{INTERVAL} seconds ..."
+ sleep INTERVAL
+ when :success
+ puts "Omnibus pipeline succeeded!"
+ break
+ else
+ raise "Omnibus pipeline did not succeed!"
+ end
+
+ STDOUT.flush
+ end
+ end
+
+ def timeout?
+ Time.now.to_i > (@start + MAX_DURATION)
+ end
+
+ def status
+ req = Net::HTTP::Get.new(@uri)
+ req['PRIVATE-TOKEN'] = ENV['GITLAB_QA_ACCESS_TOKEN']
+
+ res = Net::HTTP.start(@uri.hostname, @uri.port, use_ssl: true) do |http|
+ http.request(req)
+ end
+
+ JSON.parse(res.body)['status'].to_s.to_sym
+ end
+ end
end
+
+Omnibus::Trigger.new.invoke!.wait!
diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb
index 6d8b9865dcb..fc1bf67d7b9 100644
--- a/spec/bin/changelog_spec.rb
+++ b/spec/bin/changelog_spec.rb
@@ -84,7 +84,7 @@ describe 'bin/changelog' do
expect do
expect do
expect { described_class.read_type }.to raise_error(SystemExit)
- end.to output("Invalid category index, please select an index between 1 and 7\n").to_stderr
+ end.to output("Invalid category index, please select an index between 1 and 8\n").to_stderr
end.to output.to_stdout
end
end
diff --git a/spec/controllers/admin/hooks_controller_spec.rb b/spec/controllers/admin/hooks_controller_spec.rb
index 1d1070e90f4..e6ba596117a 100644
--- a/spec/controllers/admin/hooks_controller_spec.rb
+++ b/spec/controllers/admin/hooks_controller_spec.rb
@@ -20,7 +20,7 @@ describe Admin::HooksController do
post :create, hook: hook_params
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(SystemHook.all.size).to eq(1)
expect(SystemHook.first).to have_attributes(hook_params)
end
diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb
index 8f1f0ba89ff..944680b3f42 100644
--- a/spec/controllers/admin/impersonations_controller_spec.rb
+++ b/spec/controllers/admin/impersonations_controller_spec.rb
@@ -22,7 +22,7 @@ describe Admin::ImpersonationsController do
it "responds with status 404" do
delete :destroy
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "doesn't sign us in" do
@@ -46,7 +46,7 @@ describe Admin::ImpersonationsController do
it "responds with status 404" do
delete :destroy
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "doesn't sign us in as the impersonator" do
@@ -65,7 +65,7 @@ describe Admin::ImpersonationsController do
it "responds with status 404" do
delete :destroy
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "doesn't sign us in as the impersonator" do
diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb
index 373260b3978..d5a3c250f31 100644
--- a/spec/controllers/admin/projects_controller_spec.rb
+++ b/spec/controllers/admin/projects_controller_spec.rb
@@ -27,7 +27,7 @@ describe Admin::ProjectsController do
get :index
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.body).not_to match(pending_delete_project.name)
expect(response.body).to match(project.name)
end
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index b5fe40d0510..312dbdd0624 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -11,7 +11,7 @@ describe Admin::RunnersController do
it 'lists all runners' do
get :index
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -19,13 +19,13 @@ describe Admin::RunnersController do
it 'shows a particular runner' do
get :show, id: runner.id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'shows 404 for unknown runner' do
get :show, id: 0
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -39,7 +39,7 @@ describe Admin::RunnersController do
runner.reload
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(runner.description).to eq(new_desc)
end
end
@@ -48,7 +48,7 @@ describe Admin::RunnersController do
it 'destroys the runner' do
delete :destroy, id: runner.id
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(Ci::Runner.find_by(id: runner.id)).to be_nil
end
end
@@ -63,7 +63,7 @@ describe Admin::RunnersController do
runner.reload
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(runner.active).to eq(true)
end
end
@@ -78,7 +78,7 @@ describe Admin::RunnersController do
runner.reload
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(runner.active).to eq(false)
end
end
diff --git a/spec/controllers/admin/services_controller_spec.rb b/spec/controllers/admin/services_controller_spec.rb
index 249bd948847..701211c2586 100644
--- a/spec/controllers/admin/services_controller_spec.rb
+++ b/spec/controllers/admin/services_controller_spec.rb
@@ -20,7 +20,7 @@ describe Admin::ServicesController do
it 'successfully displays the template' do
get :edit, id: service.id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
@@ -46,7 +46,7 @@ describe Admin::ServicesController do
put :update, id: service.id, service: { active: true }
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
it 'does not call the propagation worker when service is not active' do
@@ -54,7 +54,7 @@ describe Admin::ServicesController do
put :update, id: service.id, service: { properties: {} }
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
end
diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb
index 585ca31389d..7a96ef6a5cc 100644
--- a/spec/controllers/admin/spam_logs_controller_spec.rb
+++ b/spec/controllers/admin/spam_logs_controller_spec.rb
@@ -14,7 +14,7 @@ describe Admin::SpamLogsController do
it 'lists all spam logs' do
get :index
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -22,14 +22,14 @@ describe Admin::SpamLogsController do
it 'removes only the spam log when removing log' do
expect { delete :destroy, id: first_spam.id }.to change { SpamLog.count }.by(-1)
expect(User.find(user.id)).to be_truthy
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'removes user and his spam logs when removing the user' do
delete :destroy, id: first_spam.id, remove_user: true
expect(flash[:notice]).to eq "User #{user.username} was successfully removed."
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(SpamLog.count).to eq(0)
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
@@ -42,7 +42,7 @@ describe Admin::SpamLogsController do
it 'submits the log as ham' do
post :mark_as_ham, id: first_spam.id
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(SpamLog.find(first_spam.id).submitted_as_ham).to be_truthy
end
end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 25fe547ff37..f044a068938 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -19,7 +19,7 @@ describe Admin::UsersController do
it 'deletes user and ghosts their contributions' do
delete :destroy, id: user.username, format: :json
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(User.exists?(user.id)).to be_falsy
expect(issue.reload.author).to be_ghost
end
@@ -27,7 +27,7 @@ describe Admin::UsersController do
it 'deletes the user and their contributions when hard delete is specified' do
delete :destroy, id: user.username, hard_delete: true, format: :json
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(User.exists?(user.id)).to be_falsy
expect(Issue.exists?(issue.id)).to be_falsy
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 59a6cfbf4f5..fe95d1ef9cd 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -6,6 +6,10 @@ describe ApplicationController do
describe '#check_password_expiration' do
let(:controller) { described_class.new }
+ before do
+ allow(controller).to receive(:session).and_return({})
+ end
+
it 'redirects if the user is over their password expiry' do
user.password_expires_at = Time.new(2002)
@@ -37,83 +41,48 @@ describe ApplicationController do
controller.send(:check_password_expiration)
end
- it 'redirects if the user is over their password expiry and sign-in is disabled' do
- stub_application_setting(password_authentication_enabled: false)
+ it 'does not redirect if the user is over their password expiry but password authentication is disabled for the web interface' do
+ stub_application_setting(password_authentication_enabled_for_web: false)
+ stub_application_setting(password_authentication_enabled_for_git: 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).to receive(:redirect_to)
- expect(controller).to receive(:new_profile_password_path)
+ expect(controller).not_to receive(:redirect_to)
controller.send(:check_password_expiration)
end
end
- describe "#authenticate_user_from_token!" do
- describe "authenticating a user from a private token" do
- controller(described_class) do
- def index
- render text: "authenticated"
- end
- end
-
- context "when the 'private_token' param is populated with the private token" do
- it "logs the user in" do
- get :index, private_token: user.private_token
- expect(response).to have_http_status(200)
- expect(response.body).to eq("authenticated")
- end
- end
-
- context "when the 'PRIVATE-TOKEN' header is populated with the private token" do
- it "logs the user in" do
- @request.headers['PRIVATE-TOKEN'] = user.private_token
- get :index
- expect(response).to have_http_status(200)
- expect(response.body).to eq("authenticated")
- end
- end
-
- it "doesn't log the user in otherwise" do
- @request.headers['PRIVATE-TOKEN'] = "token"
- get :index, private_token: "token", authenticity_token: "token"
- expect(response.status).not_to eq(200)
- expect(response.body).not_to eq("authenticated")
+ describe "#authenticate_user_from_personal_access_token!" do
+ controller(described_class) do
+ def index
+ render text: 'authenticated'
end
end
- describe "authenticating a user from a personal access token" do
- controller(described_class) do
- def index
- render text: 'authenticated'
- end
- end
-
- let(:personal_access_token) { create(:personal_access_token, user: user) }
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
- context "when the 'personal_access_token' param is populated with the personal access token" do
- it "logs the user in" do
- get :index, private_token: personal_access_token.token
- expect(response).to have_http_status(200)
- expect(response.body).to eq('authenticated')
- end
+ context "when the 'personal_access_token' param is populated with the personal access token" do
+ it "logs the user in" do
+ get :index, private_token: personal_access_token.token
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to eq('authenticated')
end
+ end
- context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
- it "logs the user in" do
- @request.headers["PRIVATE-TOKEN"] = personal_access_token.token
- get :index
- expect(response).to have_http_status(200)
- expect(response.body).to eq('authenticated')
- end
+ context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
+ it "logs the user in" do
+ @request.headers["PRIVATE-TOKEN"] = personal_access_token.token
+ get :index
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to eq('authenticated')
end
+ end
- it "doesn't log the user in otherwise" do
- get :index, private_token: "token"
- expect(response.status).not_to eq(200)
- expect(response.body).not_to eq('authenticated')
- end
+ it "doesn't log the user in otherwise" do
+ get :index, private_token: "token"
+ expect(response.status).not_to eq(200)
+ expect(response.body).not_to eq('authenticated')
end
end
@@ -152,21 +121,25 @@ describe ApplicationController do
end
end
+ before do
+ sign_in user
+ end
+
context 'when format is handled' do
let(:requested_format) { :json }
it 'returns 200 response' do
- get :index, private_token: user.private_token, format: requested_format
+ get :index, format: requested_format
- expect(response).to have_http_status 200
+ expect(response).to have_gitlab_http_status 200
end
end
context 'when format is not handled' do
it 'returns 404 response' do
- get :index, private_token: user.private_token
+ get :index
- expect(response).to have_http_status 404
+ expect(response).to have_gitlab_http_status 404
end
end
end
@@ -183,7 +156,7 @@ describe ApplicationController do
context 'when the request format is atom' do
it "logs the user in" do
get :index, rss_token: user.rss_token, format: :atom
- expect(response).to have_http_status 200
+ expect(response).to have_gitlab_http_status 200
expect(response.body).to eq 'authenticated'
end
end
@@ -191,7 +164,7 @@ describe ApplicationController do
context 'when the request format is not atom' do
it "doesn't log the user in" do
get :index, rss_token: user.rss_token
- expect(response.status).not_to have_http_status 200
+ expect(response.status).not_to have_gitlab_http_status 200
expect(response.body).not_to eq 'authenticated'
end
end
@@ -221,6 +194,20 @@ describe ApplicationController do
end
end
+ describe '#set_page_title_header' do
+ let(:controller) { described_class.new }
+
+ it 'URI encodes UTF-8 characters in the title' do
+ response = double(headers: {})
+ allow_any_instance_of(PageLayoutHelper).to receive(:page_title).and_return('€100 · GitLab')
+ allow(controller).to receive(:response).and_return(response)
+
+ controller.send(:set_page_title_header)
+
+ expect(response.headers['Page-Title']).to eq('%E2%82%AC100%20%C2%B7%20GitLab')
+ end
+ end
+
context 'two-factor authentication' do
let(:controller) { described_class.new }
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index be27bbb4283..73fff6eb5ca 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -30,7 +30,7 @@ describe AutocompleteController do
get(:users, project_id: 'unknown')
end
- it { expect(response).to have_http_status(404) }
+ it { expect(response).to have_gitlab_http_status(404) }
end
end
@@ -59,7 +59,7 @@ describe AutocompleteController do
get(:users, group_id: 'unknown')
end
- it { expect(response).to have_http_status(404) }
+ it { expect(response).to have_gitlab_http_status(404) }
end
end
@@ -138,7 +138,7 @@ describe AutocompleteController do
get(:users, project_id: project.id)
end
- it { expect(response).to have_http_status(404) }
+ it { expect(response).to have_gitlab_http_status(404) }
end
describe 'GET #users with unknown project' do
@@ -146,7 +146,7 @@ describe AutocompleteController do
get(:users, project_id: 'unknown')
end
- it { expect(response).to have_http_status(404) }
+ it { expect(response).to have_gitlab_http_status(404) }
end
describe 'GET #users with inaccessible group' do
@@ -155,7 +155,7 @@ describe AutocompleteController do
get(:users, group_id: user.namespace.id)
end
- it { expect(response).to have_http_status(404) }
+ it { expect(response).to have_gitlab_http_status(404) }
end
describe 'GET #users with no project' do
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb
index 5163099cd98..44d504d5852 100644
--- a/spec/controllers/boards/issues_controller_spec.rb
+++ b/spec/controllers/boards/issues_controller_spec.rb
@@ -24,7 +24,7 @@ describe Boards::IssuesController do
it 'returns a not found 404 response' do
list_issues user: user, board: 999, list: list2
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -62,7 +62,7 @@ describe Boards::IssuesController do
it 'returns a not found 404 response' do
list_issues user: user, board: board, list: 999
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -93,7 +93,7 @@ describe Boards::IssuesController do
it 'returns a forbidden 403 response' do
list_issues user: user, board: board, list: list2
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -116,7 +116,7 @@ describe Boards::IssuesController do
it 'returns a successful 200 response' do
create_issue user: user, board: board, list: list1, title: 'New issue'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns the created issue' do
@@ -131,7 +131,7 @@ describe Boards::IssuesController do
it 'returns an unprocessable entity 422 response' do
create_issue user: user, board: board, list: list1, title: nil
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
end
@@ -141,7 +141,7 @@ describe Boards::IssuesController do
create_issue user: user, board: board, list: list, title: 'New issue'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -149,7 +149,7 @@ describe Boards::IssuesController do
it 'returns a not found 404 response' do
create_issue user: user, board: 999, list: list1, title: 'New issue'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -157,7 +157,7 @@ describe Boards::IssuesController do
it 'returns a not found 404 response' do
create_issue user: user, board: board, list: 999, title: 'New issue'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -166,7 +166,7 @@ describe Boards::IssuesController do
it 'returns a forbidden 403 response' do
create_issue user: guest, board: board, list: list1, title: 'New issue'
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -187,7 +187,7 @@ describe Boards::IssuesController do
it 'returns a successful 200 response' do
move user: user, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'moves issue to the desired list' do
@@ -201,19 +201,19 @@ describe Boards::IssuesController do
it 'returns a unprocessable entity 422 response for invalid lists' do
move user: user, board: board, issue: issue, from_list_id: nil, to_list_id: nil
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
it 'returns a not found 404 response for invalid board id' do
move user: user, board: 999, issue: issue, from_list_id: list1.id, to_list_id: list2.id
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a not found 404 response for invalid issue id' do
move user: user, board: board, issue: double(id: 999), from_list_id: list1.id, to_list_id: list2.id
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -227,7 +227,7 @@ describe Boards::IssuesController do
it 'returns a forbidden 403 response' do
move user: guest, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb
index b11fce0fa58..a2b432af23a 100644
--- a/spec/controllers/boards/lists_controller_spec.rb
+++ b/spec/controllers/boards/lists_controller_spec.rb
@@ -15,7 +15,7 @@ describe Boards::ListsController do
it 'returns a successful 200 response' do
read_board_list user: user, board: board
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.content_type).to eq 'application/json'
end
@@ -39,7 +39,7 @@ describe Boards::ListsController do
it 'returns a forbidden 403 response' do
read_board_list user: user, board: board
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -60,7 +60,7 @@ describe Boards::ListsController do
it 'returns a successful 200 response' do
create_board_list user: user, board: board, label_id: label.id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns the created list' do
@@ -75,7 +75,7 @@ describe Boards::ListsController do
it 'returns a not found 404 response' do
create_board_list user: user, board: board, label_id: nil
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -85,7 +85,7 @@ describe Boards::ListsController do
create_board_list user: user, board: board, label_id: label.id
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -96,7 +96,7 @@ describe Boards::ListsController do
create_board_list user: guest, board: board, label_id: label.id
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -119,7 +119,7 @@ describe Boards::ListsController do
it 'returns a successful 200 response' do
move user: user, board: board, list: planning, position: 1
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'moves the list to the desired position' do
@@ -133,7 +133,7 @@ describe Boards::ListsController do
it 'returns an unprocessable entity 422 response' do
move user: user, board: board, list: planning, position: 6
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
end
@@ -141,7 +141,7 @@ describe Boards::ListsController do
it 'returns a not found 404 response' do
move user: user, board: board, list: 999, position: 1
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -149,7 +149,7 @@ describe Boards::ListsController do
it 'returns a forbidden 403 response' do
move user: guest, board: board, list: planning, position: 6
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -172,7 +172,7 @@ describe Boards::ListsController do
it 'returns a successful 200 response' do
remove_board_list user: user, board: board, list: planning
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'removes list from board' do
@@ -184,7 +184,7 @@ describe Boards::ListsController do
it 'returns a not found 404 response' do
remove_board_list user: user, board: board, list: 999
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -192,7 +192,7 @@ describe Boards::ListsController do
it 'returns a forbidden 403 response' do
remove_board_list user: guest, board: board, list: planning
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -212,7 +212,7 @@ describe Boards::ListsController do
it 'returns a successful 200 response' do
generate_default_lists user: user, board: board
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns the defaults lists' do
@@ -228,7 +228,7 @@ describe Boards::ListsController do
generate_default_lists user: user, board: board
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
end
@@ -236,7 +236,7 @@ describe Boards::ListsController do
it 'returns a forbidden 403 response' do
generate_default_lists user: guest, board: board
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
diff --git a/spec/controllers/concerns/group_tree_spec.rb b/spec/controllers/concerns/group_tree_spec.rb
new file mode 100644
index 00000000000..ba84fbf8564
--- /dev/null
+++ b/spec/controllers/concerns/group_tree_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe GroupTree do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+
+ controller(ApplicationController) do
+ # `described_class` is not available in this context
+ include GroupTree # rubocop:disable RSpec/DescribedClass
+
+ def index
+ render_group_tree GroupsFinder.new(current_user).execute
+ end
+ end
+
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ it 'filters groups' do
+ other_group = create(:group, name: 'filter')
+ other_group.add_owner(user)
+
+ get :index, filter: 'filt', format: :json
+
+ expect(assigns(:groups)).to contain_exactly(other_group)
+ end
+
+ context 'for subgroups', :nested_groups do
+ it 'only renders root groups when no parent was given' do
+ create(:group, :public, parent: group)
+
+ get :index, format: :json
+
+ expect(assigns(:groups)).to contain_exactly(group)
+ end
+
+ it 'contains only the subgroup when a parent was given' do
+ subgroup = create(:group, :public, parent: group)
+
+ get :index, parent_id: group.id, format: :json
+
+ expect(assigns(:groups)).to contain_exactly(subgroup)
+ end
+
+ it 'allows filtering for subgroups and includes the parents for rendering' do
+ subgroup = create(:group, :public, parent: group, name: 'filter')
+
+ get :index, filter: 'filt', format: :json
+
+ expect(assigns(:groups)).to contain_exactly(group, subgroup)
+ end
+
+ it 'does not include groups the user does not have access to' do
+ parent = create(:group, :private)
+ subgroup = create(:group, :private, parent: parent, name: 'filter')
+ subgroup.add_developer(user)
+ _other_subgroup = create(:group, :private, parent: parent, name: 'filte')
+
+ get :index, filter: 'filt', format: :json
+
+ expect(assigns(:groups)).to contain_exactly(parent, subgroup)
+ end
+ end
+
+ context 'json content' do
+ it 'shows groups as json' do
+ get :index, format: :json
+
+ expect(json_response.first['id']).to eq(group.id)
+ end
+
+ context 'nested groups', :nested_groups do
+ it 'expands the tree when filtering' do
+ subgroup = create(:group, :public, parent: group, name: 'filter')
+
+ get :index, filter: 'filt', format: :json
+
+ children_response = json_response.first['children']
+
+ expect(json_response.first['id']).to eq(group.id)
+ expect(children_response.first['id']).to eq(subgroup.id)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index c9687af4dd2..d7825364ed5 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -12,71 +12,70 @@ describe IssuableCollections do
controller = klass.new
- allow(controller).to receive(:params).and_return(state: 'opened')
+ allow(controller).to receive(:params).and_return(ActionController::Parameters.new(params))
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
+ describe '#page_count_for_relation' do
+ let(:params) { { state: 'opened' } }
- allow(controller).to receive(:issues_finder)
- .and_return(finder)
+ it 'returns the number of pages' do
+ relation = double(:relation, limit_value: 20)
+ pages = controller.send(:page_count_for_relation, relation, 28)
- expect(controller.send(:issues_page_count, issues)).to eq(1)
+ expect(pages).to eq(2)
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)
+ describe '#filter_params' do
+ let(:params) do
+ {
+ assignee_id: '1',
+ assignee_username: 'user1',
+ author_id: '2',
+ author_username: 'user2',
+ authorized_only: 'true',
+ due_date: '2017-01-01',
+ group_id: '3',
+ iids: '4',
+ label_name: 'foo',
+ milestone_title: 'bar',
+ my_reaction_emoji: 'thumbsup',
+ non_archived: 'true',
+ project_id: '5',
+ scope: 'all',
+ search: 'baz',
+ sort: 'priority',
+ state: 'opened',
+ invalid_param: 'invalid_param'
+ }
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)
+ it 'filters params' do
+ allow(controller).to receive(:cookies).and_return({})
+
+ filtered_params = controller.send(:filter_params)
+
+ expect(filtered_params).to eq({
+ 'assignee_id' => '1',
+ 'assignee_username' => 'user1',
+ 'author_id' => '2',
+ 'author_username' => 'user2',
+ 'authorized_only' => 'true',
+ 'due_date' => '2017-01-01',
+ 'group_id' => '3',
+ 'iids' => '4',
+ 'label_name' => 'foo',
+ 'milestone_title' => 'bar',
+ 'my_reaction_emoji' => 'thumbsup',
+ 'non_archived' => 'true',
+ 'project_id' => '5',
+ 'scope' => 'all',
+ 'search' => 'baz',
+ 'sort' => 'priority',
+ 'state' => 'opened'
+ })
end
end
end
diff --git a/spec/controllers/concerns/lfs_request_spec.rb b/spec/controllers/concerns/lfs_request_spec.rb
new file mode 100644
index 00000000000..33b23db302a
--- /dev/null
+++ b/spec/controllers/concerns/lfs_request_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe LfsRequest do
+ include ProjectForksHelper
+
+ controller(Projects::GitHttpClientController) do
+ # `described_class` is not available in this context
+ include LfsRequest # rubocop:disable RSpec/DescribedClass
+
+ def show
+ storage_project
+
+ render nothing: true
+ end
+
+ def project
+ @project ||= Project.find(params[:id])
+ end
+
+ def download_request?
+ true
+ end
+
+ def ci?
+ false
+ end
+ end
+
+ let(:project) { create(:project, :public) }
+
+ before do
+ stub_lfs_setting(enabled: true)
+ end
+
+ describe '#storage_project' do
+ it 'assigns the project as storage project' do
+ get :show, id: project.id
+
+ expect(assigns(:storage_project)).to eq(project)
+ end
+
+ it 'assigns the source of a forked project' do
+ forked_project = fork_project(project)
+
+ get :show, id: forked_project.id
+
+ expect(assigns(:storage_project)).to eq(project)
+ end
+ end
+end
diff --git a/spec/controllers/dashboard/groups_controller_spec.rb b/spec/controllers/dashboard/groups_controller_spec.rb
new file mode 100644
index 00000000000..fb9d3efbac0
--- /dev/null
+++ b/spec/controllers/dashboard/groups_controller_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Dashboard::GroupsController do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'renders group trees' do
+ expect(described_class).to include(GroupTree)
+ end
+
+ it 'only includes projects the user is a member of' do
+ member_of_group = create(:group)
+ member_of_group.add_developer(user)
+ create(:group, :public)
+
+ get :index
+
+ expect(assigns(:groups)).to contain_exactly(member_of_group)
+ end
+end
diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb
index 2dcb67d50f4..2f3d7be9abe 100644
--- a/spec/controllers/dashboard/milestones_controller_spec.rb
+++ b/spec/controllers/dashboard/milestones_controller_spec.rb
@@ -32,7 +32,7 @@ describe Dashboard::MilestonesController do
it 'shows milestone page' do
view_milestone
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index 9df4ebf2fa0..f9faa4fa59a 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -18,19 +18,19 @@ describe Dashboard::TodosController do
get :index, project_id: unauthorized_project.id
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'renders 404 when given project does not exists' do
get :index, project_id: 999
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'renders 200 when filtering for "any project" todos' do
get :index, project_id: ''
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'renders 200 when user has access on given project' do
@@ -38,17 +38,17 @@ describe Dashboard::TodosController do
get :index, project_id: authorized_project.id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
context 'when using pagination' do
let(:last_page) { user.todos.page.total_pages }
- let!(:issues) { create_list(:issue, 2, project: project, assignees: [user]) }
+ let!(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
before do
issues.each { |issue| todo_service.new_issue(issue, user) }
- allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+ allow(Kaminari.config).to receive(:default_per_page).and_return(2)
end
it 'redirects to last_page if page number is larger than number of pages' do
@@ -61,7 +61,7 @@ describe Dashboard::TodosController do
get :index, page: last_page
expect(assigns(:todos).current_page).to eq(last_page)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'does not redirect to external sites when provided a host field' do
@@ -104,7 +104,7 @@ describe Dashboard::TodosController do
patch :restore, id: todo.id
expect(todo.reload).to be_pending
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq({ "count" => "1", "done_count" => "0" })
end
end
@@ -118,7 +118,7 @@ describe Dashboard::TodosController do
todos.each do |todo|
expect(todo.reload).to be_pending
end
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq({ 'count' => '2', 'done_count' => '0' })
end
end
diff --git a/spec/controllers/explore/groups_controller_spec.rb b/spec/controllers/explore/groups_controller_spec.rb
new file mode 100644
index 00000000000..9e0ad9ea86f
--- /dev/null
+++ b/spec/controllers/explore/groups_controller_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Explore::GroupsController do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'renders group trees' do
+ expect(described_class).to include(GroupTree)
+ end
+
+ it 'includes public projects' do
+ member_of_group = create(:group)
+ member_of_group.add_developer(user)
+ public_group = create(:group, :public)
+
+ get :index
+
+ expect(assigns(:groups)).to contain_exactly(member_of_group, public_group)
+ end
+end
diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb
new file mode 100644
index 00000000000..cb1b460fc0e
--- /dev/null
+++ b/spec/controllers/groups/children_controller_spec.rb
@@ -0,0 +1,297 @@
+require 'spec_helper'
+
+describe Groups::ChildrenController do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+ let!(:group_member) { create(:group_member, group: group, user: user) }
+
+ describe 'GET #index' do
+ context 'for projects' do
+ let!(:public_project) { create(:project, :public, namespace: group) }
+ let!(:private_project) { create(:project, :private, namespace: group) }
+
+ context 'as a user' do
+ before do
+ sign_in(user)
+ end
+
+ it 'shows all children' do
+ get :index, group_id: group.to_param, format: :json
+
+ expect(assigns(:children)).to contain_exactly(public_project, private_project)
+ end
+
+ context 'being member of private subgroup' do
+ it 'shows public and private children the user is member of' do
+ group_member.destroy!
+ private_project.add_guest(user)
+
+ get :index, group_id: group.to_param, format: :json
+
+ expect(assigns(:children)).to contain_exactly(public_project, private_project)
+ end
+ end
+ end
+
+ context 'as a guest' do
+ it 'shows the public children' do
+ get :index, group_id: group.to_param, format: :json
+
+ expect(assigns(:children)).to contain_exactly(public_project)
+ end
+ end
+ end
+
+ context 'for subgroups', :nested_groups do
+ let!(:public_subgroup) { create(:group, :public, parent: group) }
+ let!(:private_subgroup) { create(:group, :private, parent: group) }
+ let!(:public_project) { create(:project, :public, namespace: group) }
+ let!(:private_project) { create(:project, :private, namespace: group) }
+
+ context 'as a user' do
+ before do
+ sign_in(user)
+ end
+
+ it 'shows all children' do
+ get :index, group_id: group.to_param, format: :json
+
+ expect(assigns(:children)).to contain_exactly(public_subgroup, private_subgroup, public_project, private_project)
+ end
+
+ context 'being member of private subgroup' do
+ it 'shows public and private children the user is member of' do
+ group_member.destroy!
+ private_subgroup.add_guest(user)
+ private_project.add_guest(user)
+
+ get :index, group_id: group.to_param, format: :json
+
+ expect(assigns(:children)).to contain_exactly(public_subgroup, private_subgroup, public_project, private_project)
+ end
+ end
+ end
+
+ context 'as a guest' do
+ it 'shows the public children' do
+ get :index, group_id: group.to_param, format: :json
+
+ expect(assigns(:children)).to contain_exactly(public_subgroup, public_project)
+ end
+ end
+
+ context 'filtering children' do
+ it 'expands the tree for matching projects' do
+ project = create(:project, :public, namespace: public_subgroup, name: 'filterme')
+
+ get :index, group_id: group.to_param, filter: 'filter', format: :json
+
+ group_json = json_response.first
+ project_json = group_json['children'].first
+
+ expect(group_json['id']).to eq(public_subgroup.id)
+ expect(project_json['id']).to eq(project.id)
+ end
+
+ it 'expands the tree for matching subgroups' do
+ matched_group = create(:group, :public, parent: public_subgroup, name: 'filterme')
+
+ get :index, group_id: group.to_param, filter: 'filter', format: :json
+
+ group_json = json_response.first
+ matched_group_json = group_json['children'].first
+
+ expect(group_json['id']).to eq(public_subgroup.id)
+ expect(matched_group_json['id']).to eq(matched_group.id)
+ end
+
+ it 'merges the trees correctly' do
+ shared_subgroup = create(:group, :public, parent: group, path: 'hardware')
+ matched_project_1 = create(:project, :public, namespace: shared_subgroup, name: 'mobile-soc')
+
+ l2_subgroup = create(:group, :public, parent: shared_subgroup, path: 'broadcom')
+ l3_subgroup = create(:group, :public, parent: l2_subgroup, path: 'wifi-group')
+ matched_project_2 = create(:project, :public, namespace: l3_subgroup, name: 'mobile')
+
+ get :index, group_id: group.to_param, filter: 'mobile', format: :json
+
+ shared_group_json = json_response.first
+ expect(shared_group_json['id']).to eq(shared_subgroup.id)
+
+ matched_project_1_json = shared_group_json['children'].detect { |child| child['type'] == 'project' }
+ expect(matched_project_1_json['id']).to eq(matched_project_1.id)
+
+ l2_subgroup_json = shared_group_json['children'].detect { |child| child['type'] == 'group' }
+ expect(l2_subgroup_json['id']).to eq(l2_subgroup.id)
+
+ l3_subgroup_json = l2_subgroup_json['children'].first
+ expect(l3_subgroup_json['id']).to eq(l3_subgroup.id)
+
+ matched_project_2_json = l3_subgroup_json['children'].first
+ expect(matched_project_2_json['id']).to eq(matched_project_2.id)
+ end
+
+ it 'expands the tree upto a specified parent' do
+ subgroup = create(:group, :public, parent: group)
+ l2_subgroup = create(:group, :public, parent: subgroup)
+ create(:project, :public, namespace: l2_subgroup, name: 'test')
+
+ get :index, group_id: subgroup.to_param, filter: 'test', format: :json
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns an array with one element when only one result is matched' do
+ create(:project, :public, namespace: group, name: 'match')
+
+ get :index, group_id: group.to_param, filter: 'match', format: :json
+
+ expect(json_response).to be_kind_of(Array)
+ expect(json_response.size).to eq(1)
+ end
+
+ it 'returns an empty array when there are no search results' do
+ subgroup = create(:group, :public, parent: group)
+ l2_subgroup = create(:group, :public, parent: subgroup)
+ create(:project, :public, namespace: l2_subgroup, name: 'no-match')
+
+ get :index, group_id: subgroup.to_param, filter: 'test', format: :json
+
+ expect(json_response).to eq([])
+ end
+
+ it 'includes pagination headers' do
+ 2.times { |i| create(:group, :public, parent: public_subgroup, name: "filterme#{i}") }
+
+ get :index, group_id: group.to_param, filter: 'filter', per_page: 1, format: :json
+
+ expect(response).to include_pagination_headers
+ end
+ end
+
+ context 'queries per rendered element', :request_store do
+ # We need to make sure the following counts are preloaded
+ # otherwise they will cause an extra query
+ # 1. Count of visible projects in the element
+ # 2. Count of visible subgroups in the element
+ # 3. Count of members of a group
+ let(:expected_queries_per_group) { 0 }
+ let(:expected_queries_per_project) { 0 }
+
+ def get_list
+ get :index, group_id: group.to_param, format: :json
+ end
+
+ it 'queries the expected amount for a group row' do
+ control = ActiveRecord::QueryRecorder.new { get_list }
+
+ _new_group = create(:group, :public, parent: group)
+
+ expect { get_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_group)
+ end
+
+ it 'queries the expected amount for a project row' do
+ control = ActiveRecord::QueryRecorder.new { get_list }
+ _new_project = create(:project, :public, namespace: group)
+
+ expect { get_list }.not_to exceed_query_limit(control).with_threshold(expected_queries_per_project)
+ end
+
+ context 'when rendering hierarchies' do
+ # When loading hierarchies we load the all the ancestors for matched projects
+ # in 1 separate query
+ let(:extra_queries_for_hierarchies) { 1 }
+
+ def get_filtered_list
+ get :index, group_id: group.to_param, filter: 'filter', format: :json
+ end
+
+ it 'queries the expected amount when nested rows are increased for a group' do
+ matched_group = create(:group, :public, parent: group, name: 'filterme')
+
+ control = ActiveRecord::QueryRecorder.new { get_filtered_list }
+
+ matched_group.update!(parent: public_subgroup)
+
+ expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_for_hierarchies)
+ end
+
+ it 'queries the expected amount when a new group match is added' do
+ create(:group, :public, parent: public_subgroup, name: 'filterme')
+
+ control = ActiveRecord::QueryRecorder.new { get_filtered_list }
+
+ create(:group, :public, parent: public_subgroup, name: 'filterme2')
+ create(:group, :public, parent: public_subgroup, name: 'filterme3')
+
+ expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_for_hierarchies)
+ end
+
+ it 'queries the expected amount when nested rows are increased for a project' do
+ matched_project = create(:project, :public, namespace: group, name: 'filterme')
+
+ control = ActiveRecord::QueryRecorder.new { get_filtered_list }
+
+ matched_project.update!(namespace: public_subgroup)
+
+ expect { get_filtered_list }.not_to exceed_query_limit(control).with_threshold(extra_queries_for_hierarchies)
+ end
+ end
+ end
+ end
+
+ context 'pagination' do
+ let(:per_page) { 3 }
+
+ before do
+ allow(Kaminari.config).to receive(:default_per_page).and_return(per_page)
+ end
+
+ context 'with only projects' do
+ let!(:other_project) { create(:project, :public, namespace: group) }
+ let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group ) }
+
+ it 'has projects on the first page' do
+ get :index, group_id: group.to_param, sort: 'id_desc', format: :json
+
+ expect(assigns(:children)).to contain_exactly(*first_page_projects)
+ end
+
+ it 'has projects on the second page' do
+ get :index, group_id: group.to_param, sort: 'id_desc', page: 2, format: :json
+
+ expect(assigns(:children)).to contain_exactly(other_project)
+ end
+ end
+
+ context 'with subgroups and projects', :nested_groups do
+ let!(:first_page_subgroups) { create_list(:group, per_page, :public, parent: group) }
+ let!(:other_subgroup) { create(:group, :public, parent: group) }
+ let!(:next_page_projects) { create_list(:project, per_page, :public, namespace: group) }
+
+ it 'contains all subgroups' do
+ get :index, group_id: group.to_param, sort: 'id_asc', format: :json
+
+ expect(assigns(:children)).to contain_exactly(*first_page_subgroups)
+ end
+
+ it 'contains the project and group on the second page' do
+ get :index, group_id: group.to_param, sort: 'id_asc', page: 2, format: :json
+
+ expect(assigns(:children)).to contain_exactly(other_subgroup, *next_page_projects.take(per_page - 1))
+ end
+
+ context 'with a mixed first page' do
+ let!(:first_page_subgroups) { [create(:group, :public, parent: group)] }
+ let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group) }
+
+ it 'correctly calculates the counts' do
+ get :index, group_id: group.to_param, sort: 'id_asc', page: 2, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index cce53f6697c..9c6d584f59b 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -8,7 +8,7 @@ describe Groups::GroupMembersController do
it 'renders index with 200 status code' do
get :index, group_id: group
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:index)
end
end
@@ -30,7 +30,7 @@ describe Groups::GroupMembersController do
user_ids: group_user.id,
access_level: Gitlab::Access::GUEST
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(group.users).not_to include group_user
end
end
@@ -73,7 +73,7 @@ describe Groups::GroupMembersController do
it 'returns 403' do
delete :destroy, group_id: group, id: 42
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -86,7 +86,7 @@ describe Groups::GroupMembersController do
it 'returns 403' do
delete :destroy, group_id: group, id: member
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(group.members).to include member
end
end
@@ -123,7 +123,7 @@ describe Groups::GroupMembersController do
it 'returns 404' do
delete :leave, group_id: group
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -144,7 +144,7 @@ describe Groups::GroupMembersController do
it 'supports json request' do
delete :leave, group_id: group, format: :json
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['notice']).to eq "You left the \"#{group.name}\" group."
end
end
@@ -157,7 +157,7 @@ describe Groups::GroupMembersController do
it 'cannot removes himself from the group' do
delete :leave, group_id: group
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -204,7 +204,7 @@ describe Groups::GroupMembersController do
it 'returns 403' do
post :approve_access_request, group_id: group, id: 42
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -217,7 +217,7 @@ describe Groups::GroupMembersController do
it 'returns 403' do
post :approve_access_request, group_id: group, id: member
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(group.members).not_to include member
end
end
diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb
index 899d8ebd12b..da54aa9054c 100644
--- a/spec/controllers/groups/labels_controller_spec.rb
+++ b/spec/controllers/groups/labels_controller_spec.rb
@@ -16,7 +16,7 @@ describe Groups::LabelsController do
post :toggle_subscription, group_id: group.to_param, id: label.to_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index fbbc67f3ae0..c1aba46be04 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -35,7 +35,7 @@ describe Groups::MilestonesController do
it 'shows group milestones page' do
get :index, group_id: group.to_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
context 'as JSON' do
@@ -51,7 +51,7 @@ describe Groups::MilestonesController do
expect(milestones.count).to eq(2)
expect(milestones.first["title"]).to eq("group milestone")
expect(milestones.second["title"]).to eq("legacy")
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.content_type).to eq 'application/json'
end
end
@@ -153,7 +153,7 @@ describe Groups::MilestonesController do
it 'does not redirect' do
get :index, group_id: group.to_param
- expect(response).not_to have_http_status(301)
+ expect(response).not_to have_gitlab_http_status(301)
end
end
@@ -172,7 +172,7 @@ describe Groups::MilestonesController do
it 'does not redirect' do
get :show, group_id: group.to_param, id: title
- expect(response).not_to have_http_status(301)
+ expect(response).not_to have_gitlab_http_status(301)
end
end
@@ -242,7 +242,7 @@ describe Groups::MilestonesController do
group_id: group.to_param,
milestone: { title: title }
- expect(response).not_to have_http_status(404)
+ expect(response).not_to have_gitlab_http_status(404)
end
it 'does not redirect to the correct casing' do
@@ -250,7 +250,7 @@ describe Groups::MilestonesController do
group_id: group.to_param,
milestone: { title: title }
- expect(response).not_to have_http_status(301)
+ expect(response).not_to have_gitlab_http_status(301)
end
end
@@ -262,7 +262,7 @@ describe Groups::MilestonesController do
group_id: redirect_route.path,
milestone: { title: title }
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
index 2e0efb57c74..e9f0924caba 100644
--- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
@@ -13,7 +13,7 @@ describe Groups::Settings::CiCdController do
it 'renders show with 200 status code' do
get :show, group_id: group
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:show)
end
end
diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb
index 02f2fa46047..8ea98cd9e8f 100644
--- a/spec/controllers/groups/variables_controller_spec.rb
+++ b/spec/controllers/groups/variables_controller_spec.rb
@@ -48,7 +48,7 @@ describe Groups::VariablesController do
post :update, group_id: group,
id: variable.id, variable: { key: '?', value: variable.value }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template :show
end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index b0564e27a68..a9cfd964dd5 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -1,4 +1,4 @@
-require 'rails_helper'
+require 'spec_helper'
describe GroupsController do
let(:user) { create(:user) }
@@ -32,6 +32,31 @@ describe GroupsController do
end
end
+ describe 'GET #show' do
+ before do
+ sign_in(user)
+ project
+ end
+
+ context 'as html' do
+ it 'assigns whether or not a group has children' do
+ get :show, id: group.to_param
+
+ expect(assigns(:has_children)).to be_truthy
+ end
+ end
+
+ context 'as atom' do
+ it 'assigns events for all the projects in the group' do
+ create(:event, project: project)
+
+ get :show, id: group.to_param, format: :atom
+
+ expect(assigns(:events)).not_to be_empty
+ end
+ end
+ end
+
describe 'GET #new' do
context 'when creating subgroups', :nested_groups do
[true, false].each do |can_create_group_status|
@@ -150,42 +175,6 @@ describe GroupsController do
end
end
- describe 'GET #subgroups', :nested_groups do
- let!(:public_subgroup) { create(:group, :public, parent: group) }
- let!(:private_subgroup) { create(:group, :private, parent: group) }
-
- context 'as a user' do
- before do
- sign_in(user)
- end
-
- it 'shows all subgroups' do
- get :subgroups, id: group.to_param
-
- expect(assigns(:nested_groups)).to contain_exactly(public_subgroup, private_subgroup)
- end
-
- context 'being member of private subgroup' do
- it 'shows public and private subgroups the user is member of' do
- group_member.destroy!
- private_subgroup.add_guest(user)
-
- get :subgroups, id: group.to_param
-
- expect(assigns(:nested_groups)).to contain_exactly(public_subgroup, private_subgroup)
- end
- end
- end
-
- context 'as a guest' do
- it 'shows the public subgroups' do
- get :subgroups, id: group.to_param
-
- expect(assigns(:nested_groups)).to contain_exactly(public_subgroup)
- end
- end
- end
-
describe 'GET #issues' do
let(:issue_1) { create(:issue, project: project) }
let(:issue_2) { create(:issue, project: project) }
@@ -274,7 +263,7 @@ describe GroupsController do
it 'updates the path successfully' do
post :update, id: group.to_param, group: { path: 'new_path' }
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(controller).to set_flash[:notice]
end
@@ -345,7 +334,7 @@ describe GroupsController do
it 'does not redirect' do
get :issues, id: group.to_param
- expect(response).not_to have_http_status(301)
+ expect(response).not_to have_gitlab_http_status(301)
end
end
@@ -364,7 +353,7 @@ describe GroupsController do
it 'does not redirect' do
get :show, id: group.to_param
- expect(response).not_to have_http_status(301)
+ expect(response).not_to have_gitlab_http_status(301)
end
end
@@ -425,62 +414,62 @@ describe GroupsController do
end
end
end
- end
- context 'for a POST request' do
- context 'when requesting the canonical path with different casing' do
- it 'does not 404' do
- post :update, id: group.to_param.upcase, group: { path: 'new_path' }
+ context 'for a POST request' do
+ context 'when requesting the canonical path with different casing' do
+ it 'does not 404' do
+ post :update, id: group.to_param.upcase, group: { path: 'new_path' }
- expect(response).not_to have_http_status(404)
- end
+ expect(response).not_to have_gitlab_http_status(404)
+ end
- it 'does not redirect to the correct casing' do
- post :update, id: group.to_param.upcase, group: { path: 'new_path' }
+ it 'does not redirect to the correct casing' do
+ post :update, id: group.to_param.upcase, group: { path: 'new_path' }
- expect(response).not_to have_http_status(301)
+ expect(response).not_to have_gitlab_http_status(301)
+ end
end
- end
- context 'when requesting a redirected path' do
- let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
- it 'returns not found' do
- post :update, id: redirect_route.path, group: { path: 'new_path' }
+ it 'returns not found' do
+ post :update, id: redirect_route.path, group: { path: 'new_path' }
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
- end
- context 'for a DELETE request' do
- context 'when requesting the canonical path with different casing' do
- it 'does not 404' do
- delete :destroy, id: group.to_param.upcase
+ context 'for a DELETE request' do
+ context 'when requesting the canonical path with different casing' do
+ it 'does not 404' do
+ delete :destroy, id: group.to_param.upcase
- expect(response).not_to have_http_status(404)
- end
+ expect(response).not_to have_gitlab_http_status(404)
+ end
- it 'does not redirect to the correct casing' do
- delete :destroy, id: group.to_param.upcase
+ it 'does not redirect to the correct casing' do
+ delete :destroy, id: group.to_param.upcase
- expect(response).not_to have_http_status(301)
+ expect(response).not_to have_gitlab_http_status(301)
+ end
end
- end
- context 'when requesting a redirected path' do
- let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
+ context 'when requesting a redirected path' do
+ let(:redirect_route) { group.redirect_routes.create(path: 'old-path') }
- it 'returns not found' do
- delete :destroy, id: redirect_route.path
+ it 'returns not found' do
+ delete :destroy, id: redirect_route.path
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
end
- end
- def group_moved_message(redirect_route, group)
- "Group '#{redirect_route.path}' was moved to '#{group.full_path}'. Please update any links and bookmarks that may still have the old path."
+ def group_moved_message(redirect_route, group)
+ "Group '#{redirect_route.path}' was moved to '#{group.full_path}'. Please update any links and bookmarks that may still have the old path."
+ end
end
end
diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb
index 03da6287774..2cead1770c9 100644
--- a/spec/controllers/health_check_controller_spec.rb
+++ b/spec/controllers/health_check_controller_spec.rb
@@ -100,7 +100,7 @@ describe HealthCheckController do
it 'supports failure plaintext response' do
get :index
- expect(response).to have_http_status(500)
+ expect(response).to have_gitlab_http_status(500)
expect(response.content_type).to eq 'text/plain'
expect(response.body).to include('The server is on fire')
end
@@ -108,7 +108,7 @@ describe HealthCheckController do
it 'supports failure json response' do
get :index, format: :json
- expect(response).to have_http_status(500)
+ expect(response).to have_gitlab_http_status(500)
expect(response.content_type).to eq 'application/json'
expect(json_response['healthy']).to be false
expect(json_response['message']).to include('The server is on fire')
@@ -117,7 +117,7 @@ describe HealthCheckController do
it 'supports failure xml response' do
get :index, format: :xml
- expect(response).to have_http_status(500)
+ expect(response).to have_gitlab_http_status(500)
expect(response.content_type).to eq 'application/xml'
expect(xml_response['healthy']).to be false
expect(xml_response['message']).to include('The server is on fire')
@@ -126,7 +126,7 @@ describe HealthCheckController do
it 'supports failure responses for specific checks' do
get :index, checks: 'email', format: :json
- expect(response).to have_http_status(500)
+ expect(response).to have_gitlab_http_status(500)
expect(response.content_type).to eq 'application/json'
expect(json_response['healthy']).to be false
expect(json_response['message']).to include('Email is on fire')
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index d3489324a9c..f75048f422c 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -100,7 +100,7 @@ describe HelpController do
context 'for UI Development Kit' do
it 'renders found' do
get :ui
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 45c3fa075ef..9bbd97ec305 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -21,9 +21,9 @@ describe Import::GithubController do
describe "GET callback" do
it "updates access token" do
token = "asdasd12345"
- allow_any_instance_of(Gitlab::GithubImport::Client)
+ allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
.to receive(:get_token).and_return(token)
- allow_any_instance_of(Gitlab::GithubImport::Client)
+ allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
.to receive(:github_options).and_return({})
stub_omniauth_provider('github')
diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb
index e00403118a0..6c09ca7dc66 100644
--- a/spec/controllers/invites_controller_spec.rb
+++ b/spec/controllers/invites_controller_spec.rb
@@ -15,7 +15,7 @@ describe InvitesController do
get :accept, id: token
member.reload
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(member.user).to eq(user)
expect(flash[:notice]).to include 'You have been granted'
end
@@ -26,7 +26,7 @@ describe InvitesController do
get :decline, id: token
expect {member.reload}.to raise_error ActiveRecord::RecordNotFound
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(flash[:notice]).to include 'You have declined the invitation to join'
end
end
diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb
index 7b0976e3e67..9e8a37171ec 100644
--- a/spec/controllers/metrics_controller_spec.rb
+++ b/spec/controllers/metrics_controller_spec.rb
@@ -59,17 +59,6 @@ describe MetricsController do
expect(response.body).to match(/^redis_shared_state_ping_latency_seconds [0-9\.]+$/)
end
- it 'returns file system check metrics' do
- get :index
-
- expect(response.body).to match(/^filesystem_access_latency_seconds{shard="default"} [0-9\.]+$/)
- expect(response.body).to match(/^filesystem_accessible{shard="default"} 1$/)
- expect(response.body).to match(/^filesystem_write_latency_seconds{shard="default"} [0-9\.]+$/)
- expect(response.body).to match(/^filesystem_writable{shard="default"} 1$/)
- expect(response.body).to match(/^filesystem_read_latency_seconds{shard="default"} [0-9\.]+$/)
- expect(response.body).to match(/^filesystem_readable{shard="default"} 1$/)
- end
-
context 'prometheus metrics are disabled' do
before do
allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(false)
@@ -78,7 +67,8 @@ describe MetricsController do
it 'returns proper response' do
get :index
- expect(response.status).to eq(404)
+ expect(response.status).to eq(200)
+ expect(response.body).to eq("# Metrics are disabled, see: http://test.host/help/administration/monitoring/prometheus/gitlab_metrics#gitlab-prometheus-metrics\n")
end
end
end
diff --git a/spec/controllers/notification_settings_controller_spec.rb b/spec/controllers/notification_settings_controller_spec.rb
index bef815ee1f7..9014b8b5084 100644
--- a/spec/controllers/notification_settings_controller_spec.rb
+++ b/spec/controllers/notification_settings_controller_spec.rb
@@ -110,7 +110,7 @@ describe NotificationSettingsController do
project_id: private_project.id,
notification_setting: { level: :participating }
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -172,7 +172,7 @@ describe NotificationSettingsController do
id: notification_setting,
notification_setting: { level: :participating }
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
index 552899eb36c..b38652e7ab9 100644
--- a/spec/controllers/oauth/applications_controller_spec.rb
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -12,7 +12,7 @@ describe Oauth::ApplicationsController do
it 'shows list of applications' do
get :index
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'redirects back to profile page if OAuth applications are disabled' do
@@ -21,7 +21,7 @@ describe Oauth::ApplicationsController do
get :index
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(profile_path)
end
end
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
index ac7f73c6e81..004b463e745 100644
--- a/spec/controllers/oauth/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -28,7 +28,7 @@ describe Oauth::AuthorizationsController do
it 'returns 200 code and renders error view' do
get :new
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template('doorkeeper/authorizations/error')
end
end
@@ -37,7 +37,7 @@ describe Oauth::AuthorizationsController do
it 'returns 200 code and renders view' do
get :new, params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template('doorkeeper/authorizations/new')
end
@@ -48,7 +48,7 @@ describe Oauth::AuthorizationsController do
get :new, params
expect(request.session['user_return_to']).to be_nil
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
end
diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb
index cdaa88bbf5d..4d31cfedbd2 100644
--- a/spec/controllers/passwords_controller_spec.rb
+++ b/spec/controllers/passwords_controller_spec.rb
@@ -1,18 +1,20 @@
require 'spec_helper'
describe PasswordsController do
- describe '#prevent_ldap_reset' do
+ describe '#check_password_authentication_available' do
before do
@request.env["devise.mapping"] = Devise.mappings[:user]
end
- context 'when password authentication is disabled' do
- it 'allows password reset' do
- stub_application_setting(password_authentication_enabled: false)
+ context 'when password authentication is disabled for the web interface and Git' do
+ it 'prevents a password reset' do
+ stub_application_setting(password_authentication_enabled_for_web: false)
+ stub_application_setting(password_authentication_enabled_for_git: false)
post :create
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
+ expect(flash[:alert]).to eq 'Password authentication is unavailable.'
end
end
@@ -22,7 +24,7 @@ describe PasswordsController do
it 'prevents a password reset' do
post :create, user: { email: user.email }
- expect(flash[:alert]).to eq('Cannot reset password for LDAP user.')
+ expect(flash[:alert]).to eq 'Password authentication is unavailable.'
end
end
end
diff --git a/spec/controllers/profiles/accounts_controller_spec.rb b/spec/controllers/profiles/accounts_controller_spec.rb
index d387aba227b..f8d9d7e39ee 100644
--- a/spec/controllers/profiles/accounts_controller_spec.rb
+++ b/spec/controllers/profiles/accounts_controller_spec.rb
@@ -11,7 +11,7 @@ describe Profiles::AccountsController do
it 'renders 404 if someone tries to unlink a non existent provider' do
delete :unlink, provider: 'github'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
[:saml, :cas3].each do |provider|
@@ -23,7 +23,7 @@ describe Profiles::AccountsController do
delete :unlink, provider: provider.to_s
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(user.reload.identities).to include(identity)
end
end
@@ -38,7 +38,7 @@ describe Profiles::AccountsController do
delete :unlink, provider: provider.to_s
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(user.reload.identities).not_to include(identity)
end
end
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index ce5040ff02b..d380978b86e 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -1,9 +1,10 @@
require('spec_helper')
-describe ProfilesController do
- describe "PUT update" do
- it "allows an email update from a user without an external email address" do
- user = create(:user)
+describe ProfilesController, :request_store do
+ let(:user) { create(:user) }
+
+ describe 'PUT update' do
+ it 'allows an email update from a user without an external email address' do
sign_in(user)
put :update,
@@ -29,7 +30,7 @@ describe ProfilesController do
expect(user.unconfirmed_email).to eq nil
end
- it "ignores an email update from a user with an external email address" do
+ it 'ignores an email update from a user with an external email address' do
stub_omniauth_setting(sync_profile_from_provider: ['ldap'])
stub_omniauth_setting(sync_profile_attributes: true)
@@ -46,7 +47,7 @@ describe ProfilesController do
expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com')
end
- it "ignores an email and name update but allows a location update from a user with external email and name, but not external location" do
+ it 'ignores an email and name update but allows a location update from a user with external email and name, but not external location' do
stub_omniauth_setting(sync_profile_from_provider: ['ldap'])
stub_omniauth_setting(sync_profile_attributes: true)
@@ -65,4 +66,35 @@ describe ProfilesController do
expect(ldap_user.location).to eq('City, Country')
end
end
+
+ describe 'PUT update_username' do
+ let(:namespace) { user.namespace }
+ let(:project) { create(:project_empty_repo, namespace: namespace) }
+ let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:new_username) { 'renamedtosomethingelse' }
+
+ it 'allows username change' do
+ sign_in(user)
+
+ put :update_username,
+ user: { username: new_username }
+
+ user.reload
+
+ expect(response.status).to eq(302)
+ expect(user.username).to eq(new_username)
+ end
+
+ it 'moves dependent projects to new namespace' do
+ sign_in(user)
+
+ put :update_username,
+ user: { username: new_username }
+
+ user.reload
+
+ expect(response.status).to eq(302)
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{new_username}/#{project.path}.git")).to be_truthy
+ end
+ end
end
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index d0992719171..d1051741430 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -60,7 +60,7 @@ describe Projects::ArtifactsController do
it 'renders the file view' do
get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt'
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
@@ -78,7 +78,7 @@ describe Projects::ArtifactsController do
it 'renders the file view' do
get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt'
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('projects/artifacts/file')
end
end
@@ -106,7 +106,7 @@ describe Projects::ArtifactsController do
it 'does not redirect the request' do
get :file, namespace_id: private_project.namespace, project_id: private_project, job_id: job, path: 'ci_artifacts.txt'
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('projects/artifacts/file')
end
end
@@ -143,7 +143,7 @@ describe Projects::ArtifactsController do
context 'cannot find the job' do
shared_examples 'not found' do
- it { expect(response).to have_http_status(:not_found) }
+ it { expect(response).to have_gitlab_http_status(:not_found) }
end
context 'has no such ref' do
diff --git a/spec/controllers/projects/badges_controller_spec.rb b/spec/controllers/projects/badges_controller_spec.rb
index d68200164e4..e7cddf8cfbf 100644
--- a/spec/controllers/projects/badges_controller_spec.rb
+++ b/spec/controllers/projects/badges_controller_spec.rb
@@ -13,13 +13,13 @@ describe Projects::BadgesController do
it 'requests the pipeline badge successfully' do
get_badge(:pipeline)
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
end
it 'requests the coverage badge successfully' do
get_badge(:coverage)
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
end
def get_badge(badge)
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
index c086b386381..54282aa4001 100644
--- a/spec/controllers/projects/blame_controller_spec.rb
+++ b/spec/controllers/projects/blame_controller_spec.rb
@@ -28,7 +28,7 @@ describe Projects::BlameController do
context "invalid file" do
let(:id) { 'master/files/ruby/missing_file.rb'}
- it { expect(response).to have_http_status(404) }
+ it { expect(response).to have_gitlab_http_status(404) }
end
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index fb76b7fdf38..6a1c07b4a0b 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -153,7 +153,7 @@ describe Projects::BlobController do
end
it 'redirects to blob show' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -167,7 +167,7 @@ describe Projects::BlobController do
end
it 'redirects to blob show' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 9e2e9a39481..84cde33d944 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -45,7 +45,7 @@ describe Projects::BoardsController do
it 'returns a not found 404 response' do
list_boards
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -85,7 +85,7 @@ describe Projects::BoardsController do
it 'returns a not found 404 response' do
read_board board: board
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -95,7 +95,7 @@ describe Projects::BoardsController do
read_board board: another_board
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 5e0b57e9b2e..d731200f70f 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -62,7 +62,7 @@ describe Projects::BranchesController do
let(:branch) { "feature%2Ftest" }
let(:ref) { "<script>alert('ref');</script>" }
it { is_expected.to render_template('new') }
- it { project.repository.branch_names.include?('feature/test') }
+ it { project.repository.branch_exists?('feature/test') }
end
end
@@ -113,22 +113,38 @@ describe Projects::BranchesController do
expect(response).to redirect_to project_tree_path(project, branch)
end
- it 'redirects to autodeploy setup page' do
- result = { status: :success, branch: double(name: branch) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'redirects to autodeploy setup page' do
+ result = { status: :success, branch: double(name: branch) }
- project.services << build(:kubernetes_service)
+ expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
+ expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
- expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
- expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
- post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- branch_name: branch,
- issue_iid: issue.iid
+ expect(response.location).to include(project_new_blob_path(project, branch))
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ before do
+ project.services << build(:kubernetes_service)
+ end
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ before do
+ create(:cluster, :provided_by_gcp, projects: [project])
+ end
- expect(response.location).to include(project_new_blob_path(project, branch))
- expect(response).to have_http_status(302)
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
@@ -161,7 +177,7 @@ describe Projects::BranchesController do
it 'returns a successful 200 response' do
create_branch name: 'my-branch', ref: 'master'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns the created branch' do
@@ -175,7 +191,7 @@ describe Projects::BranchesController do
it 'returns an unprocessable entity 422 response' do
create_branch name: "<script>alert('merge');</script>", ref: "<script>alert('ref');</script>"
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
end
@@ -202,7 +218,7 @@ describe Projects::BranchesController do
namespace_id: project.namespace,
project_id: project
- expect(response).to have_http_status(303)
+ expect(response).to have_gitlab_http_status(303)
end
end
@@ -226,28 +242,28 @@ describe Projects::BranchesController do
context "valid branch name, valid source" do
let(:branch) { "feature" }
- it { expect(response).to have_http_status(200) }
+ it { expect(response).to have_gitlab_http_status(200) }
it { expect(response.body).to be_blank }
end
context "valid branch name with unencoded slashes" do
let(:branch) { "improve/awesome" }
- it { expect(response).to have_http_status(200) }
+ it { expect(response).to have_gitlab_http_status(200) }
it { expect(response.body).to be_blank }
end
context "valid branch name with encoded slashes" do
let(:branch) { "improve%2Fawesome" }
- it { expect(response).to have_http_status(200) }
+ it { expect(response).to have_gitlab_http_status(200) }
it { expect(response.body).to be_blank }
end
context "invalid branch name, valid ref" do
let(:branch) { "no-branch" }
- it { expect(response).to have_http_status(404) }
+ it { expect(response).to have_gitlab_http_status(404) }
it { expect(response.body).to be_blank }
end
end
@@ -263,7 +279,7 @@ describe Projects::BranchesController do
expect(json_response).to eql("message" => 'Branch was removed')
end
- it { expect(response).to have_http_status(200) }
+ it { expect(response).to have_gitlab_http_status(200) }
end
context 'valid branch name with unencoded slashes' do
@@ -273,7 +289,7 @@ describe Projects::BranchesController do
expect(json_response).to eql('message' => 'Branch was removed')
end
- it { expect(response).to have_http_status(200) }
+ it { expect(response).to have_gitlab_http_status(200) }
end
context "valid branch name with encoded slashes" do
@@ -283,7 +299,7 @@ describe Projects::BranchesController do
expect(json_response).to eql('message' => 'Branch was removed')
end
- it { expect(response).to have_http_status(200) }
+ it { expect(response).to have_gitlab_http_status(200) }
end
context 'invalid branch name, valid ref' do
@@ -293,7 +309,7 @@ describe Projects::BranchesController do
expect(json_response).to eql('message' => 'No such branch')
end
- it { expect(response).to have_http_status(404) }
+ it { expect(response).to have_gitlab_http_status(404) }
end
end
@@ -341,7 +357,7 @@ describe Projects::BranchesController do
it 'responds with status 404' do
destroy_all_merged
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -379,7 +395,7 @@ describe Projects::BranchesController do
project_id: project,
format: :html
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb
new file mode 100644
index 00000000000..8b460646059
--- /dev/null
+++ b/spec/controllers/projects/clusters/applications_controller_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe Projects::Clusters::ApplicationsController do
+ include AccessMatchersForController
+
+ def current_application
+ Clusters::Cluster::APPLICATIONS[application]
+ end
+
+ describe 'POST create' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let(:application) { 'helm' }
+ let(:params) { { application: application, id: cluster.id } }
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it 'schedule an application installation' do
+ expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once
+
+ expect { go }.to change { current_application.count }
+ expect(response).to have_http_status(:no_content)
+ expect(cluster.application_helm).to be_scheduled
+ end
+
+ context 'when cluster do not exists' do
+ before do
+ cluster.destroy!
+ end
+
+ it 'return 404' do
+ expect { go }.not_to change { current_application.count }
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when application is unknown' do
+ let(:application) { 'unkwnown-app' }
+
+ it 'return 404' do
+ go
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when application is already installing' do
+ before do
+ create(:cluster_applications_helm, :installing, cluster: cluster)
+ end
+
+ it 'returns 400' do
+ go
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ end
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ post :create, params.merge(namespace_id: project.namespace, project_id: project)
+ end
+ end
+end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 7985028d73b..ca2bcb2b5ae 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -1,68 +1,108 @@
require 'spec_helper'
describe Projects::ClustersController do
- set(:user) { create(:user) }
- set(:project) { create(:project) }
- let(:role) { :master }
+ include AccessMatchersForController
+ include GoogleApi::CloudPlatformHelpers
- before do
- project.team << [user, role]
+ describe 'GET index' do
+ describe 'functionality' do
+ let(:user) { create(:user) }
- sign_in(user)
- end
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
- describe 'GET index' do
- subject do
- get :index, namespace_id: project.namespace,
- project_id: project
- end
+ context 'when project has a cluster' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- context 'when cluster is already created' do
- let!(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ it { expect(go).to redirect_to(project_cluster_path(project, project.cluster)) }
+ end
- it 'redirects to show a cluster' do
- subject
+ context 'when project does not have a cluster' do
+ let(:project) { create(:project) }
- expect(response).to redirect_to(project_cluster_path(project, cluster))
+ it { expect(go).to redirect_to(new_project_cluster_path(project)) }
end
end
- context 'when we do not have cluster' do
- it 'redirects to create a cluster' do
- subject
+ describe 'security' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- expect(response).to redirect_to(new_project_cluster_path(project))
- end
+ def go
+ get :index, namespace_id: project.namespace.to_param, project_id: project
end
end
describe 'GET login' do
- render_views
+ let(:project) { create(:project) }
- subject do
- get :login, namespace_id: project.namespace,
- project_id: project
- end
-
- context 'when we do have omniauth configured' do
- it 'shows login button' do
- subject
+ describe 'functionality' do
+ let(:user) { create(:user) }
- expect(response.body).to include('auth_buttons/signin_with_google')
+ before do
+ project.add_master(user)
+ sign_in(user)
end
- end
- context 'when we do not have omniauth configured' do
- before do
- stub_omniauth_setting(providers: [])
+ context 'when omniauth has been configured' do
+ let(:key) { 'secere-key' }
+
+ let(:session_key_for_redirect_uri) do
+ GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key)
+ end
+
+ before do
+ allow(SecureRandom).to receive(:hex).and_return(key)
+ end
+
+ it 'has authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to include(key)
+ expect(session[session_key_for_redirect_uri]).to eq(providers_gcp_new_project_clusters_url(project))
+ end
end
- it 'shows notice message' do
- subject
+ context 'when omniauth has not configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'does not have authorize_url' do
+ go
- expect(response.body).to include('Ask your GitLab administrator if you want to use this service.')
+ expect(assigns(:authorize_url)).to be_nil
+ end
end
end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :login, namespace_id: project.namespace, project_id: project
+ end
end
shared_examples 'requires to login' do
@@ -73,236 +113,336 @@ describe Projects::ClustersController do
end
end
- describe 'GET new' do
- render_views
+ describe 'GET new_gcp' do
+ let(:project) { create(:project) }
- subject do
- get :new, namespace_id: project.namespace,
- project_id: project
- end
+ describe 'functionality' do
+ let(:user) { create(:user) }
- context 'when logged' do
before do
- make_logged_in
+ project.add_master(user)
+ sign_in(user)
end
- it 'shows a creation form' do
- subject
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
- expect(response.body).to include('Create cluster')
+ it 'has new object' do
+ go
+
+ expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster)
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(go).to redirect_to(login_project_clusters_path(project)) }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(go).to redirect_to(login_project_clusters_path(project)) }
end
end
- context 'when not logged' do
- it_behaves_like 'requires to login'
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :new_gcp, namespace_id: project.namespace, project_id: project
end
end
describe 'POST create' do
- subject do
- post :create, params.merge(namespace_id: project.namespace,
- project_id: project)
+ let(:project) { create(:project) }
+
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: '111'
+ }
+ }
+ }
end
- context 'when not logged' do
- let(:params) { {} }
+ describe 'functionality' do
+ let(:user) { create(:user) }
- it_behaves_like 'requires to login'
- end
-
- context 'when logged in' do
before do
- make_logged_in
+ project.add_master(user)
+ sign_in(user)
end
- context 'when all required parameters are set' do
- let(:params) do
- {
- cluster: {
- gcp_cluster_name: 'new-cluster',
- gcp_project_id: '111'
- }
- }
- end
-
+ context 'when access token is valid' do
before do
- expect(ClusterProvisionWorker).to receive(:perform_async) { }
+ stub_google_api_validate_token
end
- it 'creates a new cluster' do
- expect { subject }.to change { Gcp::Cluster.count }
-
- expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ context 'when creates a cluster on gke' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ end
end
end
- context 'when not all required parameters are set' do
- render_views
-
- let(:params) do
- {
- cluster: {
- project_namespace: 'some namespace'
- }
- }
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
end
- it 'shows an error message' do
- expect { subject }.not_to change { Gcp::Cluster.count }
+ it 'redirects to login page' do
+ expect(go).to redirect_to(login_project_clusters_path(project))
+ end
+ end
- expect(response).to render_template(:new)
+ context 'when access token is not stored in session' do
+ it 'redirects to login page' do
+ expect(go).to redirect_to(login_project_clusters_path(project))
end
end
end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ post :create, params.merge(namespace_id: project.namespace, project_id: project)
+ end
end
describe 'GET status' do
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ let(:project) { cluster.project }
- subject do
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it "responds with matching schema" do
+ go
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('cluster_status')
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
get :status, namespace_id: project.namespace,
project_id: project,
id: cluster,
format: :json
end
-
- it "responds with matching schema" do
- subject
-
- expect(response).to have_http_status(:ok)
- expect(response).to match_response_schema('cluster_status')
- end
end
describe 'GET show' do
- render_views
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ describe 'functionality' do
+ let(:user) { create(:user) }
- subject do
- get :show, namespace_id: project.namespace,
- project_id: project,
- id: cluster
- end
-
- context 'when logged as master' do
- it "allows to update cluster" do
- subject
-
- expect(response).to have_http_status(:ok)
- expect(response.body).to include("Save")
+ before do
+ project.add_master(user)
+ sign_in(user)
end
- it "allows remove integration" do
- subject
+ it "renders view" do
+ go
- expect(response).to have_http_status(:ok)
- expect(response.body).to include("Remove integration")
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:cluster)).to eq(cluster)
end
end
- context 'when logged as developer' do
- let(:role) { :developer }
-
- it "does not allow to access page" do
- subject
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- expect(response).to have_http_status(:not_found)
- end
+ def go
+ get :show, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
end
end
describe 'PUT update' do
- render_views
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- let(:service) { project.build_kubernetes_service }
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project, service: service) }
- let(:params) { {} }
+ describe 'functionality' do
+ let(:user) { create(:user) }
- subject do
- put :update, params.merge(namespace_id: project.namespace,
- project_id: project,
- id: cluster)
- end
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
- context 'when logged as master' do
- context 'when valid params are used' do
+ context 'when update enabled' do
let(:params) do
{
cluster: { enabled: false }
}
end
- it "redirects back to show page" do
- subject
+ it "updates and redirects back to show page" do
+ go
+ cluster.reload
expect(response).to redirect_to(project_cluster_path(project, project.cluster))
expect(flash[:notice]).to eq('Cluster was successfully updated.')
+ expect(cluster.enabled).to be_falsey
end
- end
- context 'when invalid params are used' do
- let(:params) do
- {
- cluster: { project_namespace: 'my Namespace 321321321 #' }
- }
- end
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
- it "rejects changes" do
- subject
+ it "rejects changes" do
+ go
- expect(response).to have_http_status(:ok)
- expect(response).to render_template(:show)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(cluster.enabled).to be_truthy
+ end
end
end
end
- context 'when logged as developer' do
- let(:role) { :developer }
+ describe 'security' do
+ let(:params) do
+ {
+ cluster: { enabled: false }
+ }
+ end
- it "does not allow to update cluster" do
- subject
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- expect(response).to have_http_status(:not_found)
- end
+ def go
+ put :update, params.merge(namespace_id: project.namespace,
+ project_id: project,
+ id: cluster)
end
end
describe 'delete update' do
- let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- subject do
- delete :destroy, namespace_id: project.namespace,
- project_id: project,
- id: cluster
- end
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
- context 'when logged as master' do
- it "redirects back to clusters list" do
- subject
+ it "destroys and redirects back to clusters list" do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
expect(response).to redirect_to(project_clusters_path(project))
expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
end
- end
- context 'when logged as developer' do
- let(:role) { :developer }
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+
+ it "destroys and redirects back to clusters list" do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
+ end
+
+ context 'when provider is user' do
+ let(:cluster) { create(:cluster, :project, :provided_by_user) }
- it "does not allow to destroy cluster" do
- subject
+ it "destroys and redirects back to clusters list" do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(0)
- expect(response).to have_http_status(:not_found)
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
end
end
- end
- def make_logged_in
- session[GoogleApi::CloudPlatform::Client.session_key_for_token] = '1234'
- session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = in_hour.to_i.to_s
- end
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
- def in_hour
- Time.now + 1.hour
+ def go
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
+ end
end
end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index df53863482d..fd90c0d8bad 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
describe Projects::CommitController do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
let(:commit) { project.commit("master") }
let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
let(:master_pickable_commit) { project.commit(master_pickable_sha) }
before do
sign_in(user)
- project.team << [user, :master]
+ project.add_master(user)
end
describe 'GET show' do
@@ -134,8 +134,8 @@ describe Projects::CommitController do
end
end
- describe "GET branches" do
- it "contains branch and tags information" do
+ describe 'GET branches' do
+ it 'contains branch and tags information' do
commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
get(:branches,
@@ -143,8 +143,26 @@ describe Projects::CommitController do
project_id: project,
id: commit.id)
- expect(assigns(:branches)).to include("master", "feature_conflict")
- expect(assigns(:tags)).to include("v1.1.0")
+ expect(assigns(:branches)).to include('master', 'feature_conflict')
+ expect(assigns(:branches_limit_exceeded)).to be_falsey
+ expect(assigns(:tags)).to include('v1.1.0')
+ expect(assigns(:tags_limit_exceeded)).to be_falsey
+ end
+
+ it 'returns :limit_exceeded when number of branches/tags reach a threshhold' do
+ commit = project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ allow_any_instance_of(Repository).to receive(:branch_count).and_return(1001)
+ allow_any_instance_of(Repository).to receive(:tag_count).and_return(1001)
+
+ get(:branches,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: commit.id)
+
+ expect(assigns(:branches)).to eq([])
+ expect(assigns(:branches_limit_exceeded)).to be_truthy
+ expect(assigns(:tags)).to eq([])
+ expect(assigns(:tags_limit_exceeded)).to be_truthy
end
end
@@ -157,7 +175,7 @@ describe Projects::CommitController do
id: commit.id)
expect(response).not_to be_success
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -206,7 +224,7 @@ describe Projects::CommitController do
id: master_pickable_commit.id)
expect(response).not_to be_success
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -286,7 +304,7 @@ describe Projects::CommitController do
end
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -298,7 +316,7 @@ describe Projects::CommitController do
end
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -309,7 +327,7 @@ describe Projects::CommitController do
end
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -356,7 +374,7 @@ describe Projects::CommitController do
end
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index e26731fb691..c459d732507 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -10,9 +10,36 @@ describe Projects::CommitsController do
end
describe "GET show" do
- context "when the ref name ends in .atom" do
- render_views
+ render_views
+
+ context 'with file path' do
+ before do
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: id)
+ end
+
+ context "valid branch, valid file" do
+ let(:id) { 'master/README.md' }
+
+ it { is_expected.to respond_with(:success) }
+ end
+
+ context "valid branch, invalid file" do
+ let(:id) { 'master/invalid-path.rb' }
+ it { is_expected.to respond_with(:not_found) }
+ end
+
+ context "invalid branch, valid file" do
+ let(:id) { 'invalid-branch/README.md' }
+
+ it { is_expected.to respond_with(:not_found) }
+ end
+ end
+
+ context "when the ref name ends in .atom" do
context "when the ref does not exist with the suffix" do
it "renders as atom" do
get(:show,
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index b4f9fd9b7a2..fe5818da0bc 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -133,7 +133,7 @@ describe Projects::CompareController do
end
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -145,7 +145,7 @@ describe Projects::CompareController do
end
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -156,7 +156,7 @@ describe Projects::CompareController do
end
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -166,7 +166,7 @@ describe Projects::CompareController do
end
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
index 3daff1eeea3..3164fd5c143 100644
--- a/spec/controllers/projects/deployments_controller_spec.rb
+++ b/spec/controllers/projects/deployments_controller_spec.rb
@@ -67,7 +67,7 @@ describe Projects::DeploymentsController do
it 'returns a empty response 204 resposne' do
get :metrics, deployment_params(id: deployment.id)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(response.body).to eq('')
end
end
@@ -142,7 +142,7 @@ describe Projects::DeploymentsController do
it 'returns a empty response 204 response' do
get :additional_metrics, deployment_params(id: deployment.id, format: :json)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(response.body).to eq('')
end
end
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
index fe62898fa9b..3bf676637a2 100644
--- a/spec/controllers/projects/discussions_controller_spec.rb
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -25,7 +25,7 @@ describe Projects::DiscussionsController do
it "returns status 404" do
post :resolve, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -42,7 +42,7 @@ describe Projects::DiscussionsController do
it "returns status 404" do
post :resolve, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -69,7 +69,7 @@ describe Projects::DiscussionsController do
it "returns status 200" do
post :resolve, request_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
@@ -86,7 +86,7 @@ describe Projects::DiscussionsController do
it "returns status 404" do
delete :unresolve, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -103,7 +103,7 @@ describe Projects::DiscussionsController do
it "returns status 404" do
delete :unresolve, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -117,7 +117,7 @@ describe Projects::DiscussionsController do
it "returns status 200" do
delete :unresolve, request_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 5a95f4f6199..ff9ab53d8c3 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -19,7 +19,7 @@ describe Projects::EnvironmentsController do
it 'responds with status code 200' do
get :index, environment_params
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
end
end
@@ -59,7 +59,7 @@ describe Projects::EnvironmentsController do
end
it 'sets the polling interval header' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Poll-Interval']).to eq("3000")
end
end
@@ -137,7 +137,7 @@ describe Projects::EnvironmentsController do
params[:id] = 12345
get :show, params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -155,7 +155,7 @@ describe Projects::EnvironmentsController do
patch_params = environment_params.merge(environment: { external_url: 'https://git.gitlab.com' })
patch :update, patch_params
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
@@ -166,7 +166,7 @@ describe Projects::EnvironmentsController do
patch :stop, environment_params(format: :json)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -179,7 +179,7 @@ describe Projects::EnvironmentsController do
patch :stop, environment_params(format: :json)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq(
{ 'redirect_url' =>
project_job_url(project, action) })
@@ -193,7 +193,7 @@ describe Projects::EnvironmentsController do
patch :stop, environment_params(format: :json)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq(
{ 'redirect_url' =>
project_environment_url(project, environment) })
@@ -206,7 +206,7 @@ describe Projects::EnvironmentsController do
it 'responds with a status code 200' do
get :terminal, environment_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'loads the terminals for the enviroment' do
@@ -220,7 +220,7 @@ describe Projects::EnvironmentsController do
it 'responds with a status code 404' do
get :terminal, environment_params(id: 666)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -244,7 +244,7 @@ describe Projects::EnvironmentsController do
get :terminal_websocket_authorize, environment_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.headers["Content-Type"]).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(response.body).to eq('{"workhorse":"response"}')
end
@@ -254,7 +254,7 @@ describe Projects::EnvironmentsController do
it 'returns 404' do
get :terminal_websocket_authorize, environment_params(id: 666)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -290,7 +290,7 @@ describe Projects::EnvironmentsController do
it 'returns a metrics JSON document' do
get :metrics, environment_params(format: :json)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(json_response).to eq({})
end
end
@@ -330,7 +330,7 @@ describe Projects::EnvironmentsController do
it 'returns a metrics JSON document' do
get :additional_metrics, environment_params(format: :json)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(json_response).to eq({})
end
end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index dc8290c438e..1bedb8ebdff 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -89,7 +89,7 @@ describe Projects::ForksController do
get_new
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -118,7 +118,7 @@ describe Projects::ForksController do
post_create
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(namespace_project_import_path(user.namespace, project))
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 053bd73fee3..4dbbaecdd6d 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -20,7 +20,7 @@ describe Projects::IssuesController do
get :index, namespace_id: project.namespace, project_id: project
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -28,7 +28,7 @@ describe Projects::IssuesController do
it 'renders the "index" template' do
get :index, namespace_id: project.namespace, project_id: project
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:index)
end
end
@@ -45,7 +45,7 @@ describe Projects::IssuesController do
it "returns index" do
get :index, namespace_id: project.namespace, project_id: project
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "returns 301 if request path doesn't match project path" do
@@ -59,7 +59,7 @@ describe Projects::IssuesController do
project.save!
get :index, namespace_id: project.namespace, project_id: project
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -89,7 +89,7 @@ describe Projects::IssuesController do
page: last_page.to_param
expect(assigns(:issues).current_page).to eq(last_page)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'does not redirect to external sites when provided a host field' do
@@ -166,7 +166,7 @@ describe Projects::IssuesController do
get :new, namespace_id: project.namespace, project_id: project
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -174,7 +174,7 @@ describe Projects::IssuesController do
it 'renders the "new" template' do
get :new, namespace_id: project.namespace, project_id: project
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:new)
end
end
@@ -224,7 +224,7 @@ describe Projects::IssuesController do
it 'moves issue to another project' do
move_issue
- expect(response).to have_http_status :ok
+ expect(response).to have_gitlab_http_status :ok
expect(another_project.issues).not_to be_empty
end
end
@@ -233,7 +233,7 @@ describe Projects::IssuesController do
it 'responds with 404' do
move_issue
- expect(response).to have_http_status :not_found
+ expect(response).to have_gitlab_http_status :not_found
end
end
@@ -248,6 +248,45 @@ describe Projects::IssuesController do
end
end
+ describe 'PUT #update' do
+ subject do
+ put :update,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: issue.to_param,
+ issue: { title: 'New title' }, format: :json
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when user has access to update issue' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'updates the issue' do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(issue.reload.title).to eq('New title')
+ end
+ end
+
+ context 'when user does not have access to update issue' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'responds with 404' do
+ subject
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+
describe 'Confidential Issues' do
let(:project) { create(:project_empty_repo, :public) }
let(:assignee) { create(:assignee) }
@@ -329,14 +368,14 @@ describe Projects::IssuesController do
sign_out(:user)
go(id: unescaped_parameter_value.to_param)
- expect(response).to have_http_status :not_found
+ expect(response).to have_gitlab_http_status :not_found
end
it 'returns 404 for non project members' do
sign_in(non_member)
go(id: unescaped_parameter_value.to_param)
- expect(response).to have_http_status :not_found
+ expect(response).to have_gitlab_http_status :not_found
end
it 'returns 404 for project members with guest role' do
@@ -344,21 +383,21 @@ describe Projects::IssuesController do
project.team << [member, :guest]
go(id: unescaped_parameter_value.to_param)
- expect(response).to have_http_status :not_found
+ expect(response).to have_gitlab_http_status :not_found
end
it "returns #{http_status[:success]} for author" do
sign_in(author)
go(id: unescaped_parameter_value.to_param)
- expect(response).to have_http_status http_status[:success]
+ expect(response).to have_gitlab_http_status http_status[:success]
end
it "returns #{http_status[:success]} for assignee" do
sign_in(assignee)
go(id: request_forgery_timing_attack.to_param)
- expect(response).to have_http_status http_status[:success]
+ expect(response).to have_gitlab_http_status http_status[:success]
end
it "returns #{http_status[:success]} for project members" do
@@ -366,14 +405,14 @@ describe Projects::IssuesController do
project.team << [member, :developer]
go(id: unescaped_parameter_value.to_param)
- expect(response).to have_http_status http_status[:success]
+ expect(response).to have_gitlab_http_status http_status[:success]
end
it "returns #{http_status[:success]} for admin" do
sign_in(admin)
go(id: unescaped_parameter_value.to_param)
- expect(response).to have_http_status http_status[:success]
+ expect(response).to have_gitlab_http_status http_status[:success]
end
end
@@ -475,7 +514,7 @@ describe Projects::IssuesController do
it 'returns 422 status' do
update_issue
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
end
@@ -495,7 +534,7 @@ describe Projects::IssuesController do
end
it 'returns 200 status' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'accepts an issue after recaptcha is verified' do
@@ -553,10 +592,33 @@ describe Projects::IssuesController do
it 'returns 200' do
go(id: issue.iid)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
+
+ describe 'GET #edit' do
+ it_behaves_like 'restricted action', success: 200
+
+ def go(id:)
+ get :edit,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: id
+ end
+ end
+
+ describe 'PUT #update' do
+ it_behaves_like 'restricted action', success: 302
+
+ def go(id:)
+ put :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: id,
+ issue: { title: 'New title' }
+ end
+ end
end
describe 'POST #create' do
@@ -778,7 +840,7 @@ describe Projects::IssuesController do
it "rejects a developer to destroy an issue" do
delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -794,12 +856,12 @@ describe Projects::IssuesController do
it "deletes the issue" do
delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./)
end
it 'delegates the update of the todos count cache to TodoService' do
- expect_any_instance_of(TodoService).to receive(:destroy_issue).with(issue, owner).once
+ expect_any_instance_of(TodoService).to receive(:destroy_issuable).with(issue, owner).once
delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
end
@@ -818,7 +880,7 @@ describe Projects::IssuesController do
project_id: project, id: issue.iid, name: "thumbsup")
end.to change { issue.award_emoji.count }.by(1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -850,47 +912,48 @@ describe Projects::IssuesController do
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
-
- context 'with cross-reference system note', :request_store do
- let(:new_issue) { create(:issue) }
- let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" }
-
+ context 'when authenticated' do
before do
- create(:discussion_note_on_issue, :system, noteable: issue, project: issue.project, note: cross_reference)
+ project.add_developer(user)
+ sign_in(user)
end
- it 'filters notes that the user should not see' do
+ it 'returns discussion json' do
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
- expect(JSON.parse(response.body).count).to eq(1)
+ expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes individual_note])
end
- it 'does not result in N+1 queries' do
- # Instantiate the controller variables to ensure QueryRecorder has an accurate base count
- get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
+ context 'with cross-reference system note', :request_store do
+ let(:new_issue) { create(:issue) }
+ let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" }
- RequestStore.clear!
+ before do
+ create(:discussion_note_on_issue, :system, noteable: issue, project: issue.project, note: cross_reference)
+ end
- control_count = ActiveRecord::QueryRecorder.new do
+ it 'filters notes that the user should not see' do
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
- end.count
- RequestStore.clear!
+ expect(JSON.parse(response.body).count).to eq(1)
+ end
+
+ it 'does not result in N+1 queries' do
+ # Instantiate the controller variables to ensure QueryRecorder has an accurate base count
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
+
+ RequestStore.clear!
- create_list(:discussion_note_on_issue, 2, :system, noteable: issue, project: issue.project, note: cross_reference)
+ control_count = ActiveRecord::QueryRecorder.new do
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
+ end.count
- expect { get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid }.not_to exceed_query_limit(control_count)
+ RequestStore.clear!
+
+ create_list(:discussion_note_on_issue, 2, :system, noteable: issue, project: issue.project, note: cross_reference)
+
+ expect { get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid }.not_to exceed_query_limit(control_count)
+ end
end
end
end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index d01339a0b88..7490f8fefce 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -20,7 +20,7 @@ describe Projects::JobsController do
end
it 'has only pending builds' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:builds).first.status).to eq('pending')
end
end
@@ -33,7 +33,7 @@ describe Projects::JobsController do
end
it 'has only running jobs' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:builds).first.status).to eq('running')
end
end
@@ -46,7 +46,7 @@ describe Projects::JobsController do
end
it 'has only finished jobs' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:builds).first.status).to eq('success')
end
end
@@ -62,7 +62,7 @@ describe Projects::JobsController do
end
it 'redirects to the page' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:builds).current_page).to eq(last_page)
end
end
@@ -107,7 +107,7 @@ describe Projects::JobsController do
end
it 'has a job' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:build).id).to eq(job.id)
end
end
@@ -118,7 +118,7 @@ describe Projects::JobsController do
end
it 'renders not_found' do
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@@ -136,7 +136,7 @@ describe Projects::JobsController do
end
it 'exposes needed information' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(json_response['raw_path']).to match(/jobs\/\d+\/raw\z/)
expect(json_response.dig('merge_request', 'path')).to match(/merge_requests\/\d+\z/)
expect(json_response['new_issue_path'])
@@ -163,7 +163,7 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, :trace, pipeline: pipeline) }
it 'returns a trace' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
expect(json_response['html']).to eq('BUILD TRACE')
@@ -174,7 +174,7 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, pipeline: pipeline) }
it 'returns no traces' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
expect(json_response['html']).to be_nil
@@ -185,7 +185,7 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, :unicode_trace, pipeline: pipeline) }
it 'returns a trace with Unicode' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
expect(json_response['html']).to include("ヾ(´༎ຶД༎ຶ`)ノ")
@@ -212,7 +212,7 @@ describe Projects::JobsController do
end
it 'return a detailed job status in json' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon
@@ -232,7 +232,7 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, :retryable, pipeline: pipeline) }
it 'redirects to the retried job page' do
- expect(response).to have_http_status(:found)
+ expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(namespace_project_job_path(id: Ci::Build.last.id))
end
end
@@ -241,7 +241,7 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, pipeline: pipeline) }
it 'renders unprocessable_entity' do
- expect(response).to have_http_status(:unprocessable_entity)
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
@@ -268,7 +268,7 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, :playable, pipeline: pipeline) }
it 'redirects to the played job page' do
- expect(response).to have_http_status(:found)
+ expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(namespace_project_job_path(id: job.id))
end
@@ -281,7 +281,7 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, pipeline: pipeline) }
it 'renders unprocessable_entity' do
- expect(response).to have_http_status(:unprocessable_entity)
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
@@ -304,7 +304,7 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, :cancelable, pipeline: pipeline) }
it 'redirects to the canceled job page' do
- expect(response).to have_http_status(:found)
+ expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(namespace_project_job_path(id: job.id))
end
@@ -317,7 +317,7 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, :canceled, pipeline: pipeline) }
it 'returns unprocessable_entity' do
- expect(response).to have_http_status(:unprocessable_entity)
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
@@ -342,7 +342,7 @@ describe Projects::JobsController do
end
it 'redirects to a index page' do
- expect(response).to have_http_status(:found)
+ expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(namespace_project_jobs_path)
end
@@ -359,7 +359,7 @@ describe Projects::JobsController do
end
it 'redirects to a index page' do
- expect(response).to have_http_status(:found)
+ expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(namespace_project_jobs_path)
end
end
@@ -371,8 +371,10 @@ describe Projects::JobsController do
end
describe 'POST erase' do
+ let(:role) { :master }
+
before do
- project.add_developer(user)
+ project.team << [user, role]
sign_in(user)
post_erase
@@ -382,7 +384,7 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, :erasable, :trace, pipeline: pipeline) }
it 'redirects to the erased job page' do
- expect(response).to have_http_status(:found)
+ expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(namespace_project_job_path(id: job.id))
end
@@ -400,7 +402,28 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, :erased, pipeline: pipeline) }
it 'returns unprocessable_entity' do
- expect(response).to have_http_status(:unprocessable_entity)
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+
+ context 'when user is developer' do
+ let(:role) { :developer }
+ let(:job) { create(:ci_build, :erasable, :trace, pipeline: pipeline, user: triggered_by) }
+
+ context 'when triggered by same user' do
+ let(:triggered_by) { user }
+
+ it 'has successful status' do
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
+
+ context 'when triggered by different user' do
+ let(:triggered_by) { create(:user) }
+
+ it 'does not have successful status' do
+ expect(response).not_to have_gitlab_http_status(:found)
+ end
end
end
@@ -420,7 +443,7 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, :trace, pipeline: pipeline) }
it 'send a trace file' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type).to eq 'text/plain; charset=utf-8'
expect(response.body).to eq 'BUILD TRACE'
end
@@ -430,7 +453,7 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, pipeline: pipeline) }
it 'returns not_found' do
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index f4e2dca883d..cf83f2f3265 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -78,7 +78,7 @@ describe Projects::LabelsController do
it 'creates labels' do
post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
@@ -86,7 +86,7 @@ describe Projects::LabelsController do
it 'creates labels' do
post :generate, namespace_id: project.namespace.to_param, project_id: project
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
end
@@ -97,7 +97,7 @@ describe Projects::LabelsController do
toggle_subscription(label)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'allows user to toggle subscription on group labels' do
@@ -105,7 +105,7 @@ describe Projects::LabelsController do
toggle_subscription(group_label)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
def toggle_subscription(label)
@@ -121,7 +121,7 @@ describe Projects::LabelsController do
it 'denies access' do
post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -170,7 +170,7 @@ describe Projects::LabelsController do
it 'does not redirect' do
get :index, namespace_id: project.namespace, project_id: project.to_param
- expect(response).not_to have_http_status(301)
+ expect(response).not_to have_gitlab_http_status(301)
end
end
@@ -203,13 +203,13 @@ describe Projects::LabelsController do
it 'does not 404' do
post :generate, namespace_id: project.namespace, project_id: project
- expect(response).not_to have_http_status(404)
+ expect(response).not_to have_gitlab_http_status(404)
end
it 'does not redirect to the correct casing' do
post :generate, namespace_id: project.namespace, project_id: project
- expect(response).not_to have_http_status(301)
+ expect(response).not_to have_gitlab_http_status(301)
end
end
@@ -219,7 +219,7 @@ describe Projects::LabelsController do
it 'returns not found' do
post :generate, namespace_id: project.namespace, project_id: project.to_param + 'old'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
index 4eea7041d29..33d48ff94d1 100644
--- a/spec/controllers/projects/mattermosts_controller_spec.rb
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -20,7 +20,7 @@ describe Projects::MattermostsController do
namespace_id: project.namespace.to_param,
project_id: project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
index 393d38c6e6b..2d7647a6e12 100644
--- a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
@@ -17,8 +17,8 @@ describe Projects::MergeRequests::ConflictsController do
describe 'GET show' do
context 'when the conflicts cannot be resolved in the UI' do
before do
- allow_any_instance_of(Gitlab::Conflict::Parser)
- .to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
+ allow(Gitlab::Git::Conflict::Parser).to receive(:parse)
+ .and_raise(Gitlab::Git::Conflict::Parser::UnmergeableFile)
get :show,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
@@ -28,7 +28,7 @@ describe Projects::MergeRequests::ConflictsController do
end
it 'returns a 200 status code' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
end
it 'returns JSON with a message' do
@@ -109,14 +109,14 @@ describe Projects::MergeRequests::ConflictsController do
context 'when the conflicts cannot be resolved in the UI' do
before do
- allow_any_instance_of(Gitlab::Conflict::Parser)
- .to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
+ allow(Gitlab::Git::Conflict::Parser).to receive(:parse)
+ .and_raise(Gitlab::Git::Conflict::Parser::UnmergeableFile)
conflict_for_path('files/ruby/regex.rb')
end
it 'returns a 404 status code' do
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -126,7 +126,7 @@ describe Projects::MergeRequests::ConflictsController do
end
it 'returns a 404 status code' do
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -138,7 +138,7 @@ describe Projects::MergeRequests::ConflictsController do
end
it 'returns a 200 status code' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
end
it 'returns the file in JSON format' do
@@ -198,7 +198,7 @@ describe Projects::MergeRequests::ConflictsController do
end
it 'returns an OK response' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
end
end
@@ -224,7 +224,7 @@ describe Projects::MergeRequests::ConflictsController do
end
it 'returns a 400 error' do
- expect(response).to have_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
end
it 'has a message with the name of the first missing section' do
@@ -254,7 +254,7 @@ describe Projects::MergeRequests::ConflictsController do
end
it 'returns a 400 error' do
- expect(response).to have_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
end
it 'has a message with the name of the missing file' do
@@ -292,7 +292,7 @@ describe Projects::MergeRequests::ConflictsController do
end
it 'returns a 400 error' do
- expect(response).to have_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
end
it 'has a message with the path of the problem file' do
diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
index fc4cec53374..7fdddc02fd3 100644
--- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
@@ -112,7 +112,7 @@ describe Projects::MergeRequests::CreationsController do
end
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index 7260350d5fb..18a70bec103 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -119,7 +119,7 @@ describe Projects::MergeRequests::DiffsController do
end
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -131,7 +131,7 @@ describe Projects::MergeRequests::DiffsController do
end
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -142,7 +142,7 @@ describe Projects::MergeRequests::DiffsController do
end
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -155,7 +155,7 @@ describe Projects::MergeRequests::DiffsController do
end
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 707e7c32283..bfdad85c082 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -83,15 +83,15 @@ describe Projects::MergeRequestsController do
end
describe 'as json' do
- context 'with basic param' do
+ context 'with basic serializer param' do
it 'renders basic MR entity as json' do
- go(basic: true, format: :json)
+ go(serializer: 'basic', format: :json)
expect(response).to match_response_schema('entities/merge_request_basic')
end
end
- context 'without basic param' do
+ context 'without basic serializer param' do
it 'renders the merge request in the json format' do
go(format: :json)
@@ -143,7 +143,7 @@ describe Projects::MergeRequestsController do
get_merge_requests(last_page)
expect(assigns(:merge_requests).current_page).to eq(last_page)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'does not redirect to external sites when provided a host field' do
@@ -186,17 +186,23 @@ describe Projects::MergeRequestsController do
end
describe 'PUT update' do
+ def update_merge_request(mr_params, additional_params = {})
+ params = {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: merge_request.iid,
+ merge_request: mr_params
+ }.merge(additional_params)
+
+ put :update, params
+ end
+
context 'changing the assignee' do
it 'limits the attributes exposed on the assignee' do
assignee = create(:user)
project.add_developer(assignee)
- put :update,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- merge_request: { assignee_id: assignee.id },
- format: :json
+ update_merge_request({ assignee_id: assignee.id }, format: :json)
body = JSON.parse(response.body)
expect(body['assignee'].keys)
@@ -204,6 +210,20 @@ describe Projects::MergeRequestsController do
end
end
+ context 'when user does not have access to update issue' do
+ before do
+ reporter = create(:user)
+ project.add_reporter(reporter)
+ sign_in(reporter)
+ end
+
+ it 'responds with 404' do
+ update_merge_request(title: 'New title')
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
context 'there is no source project' do
let(:project) { create(:project, :repository) }
let(:forked_project) { fork_project_with_submodules(project) }
@@ -214,13 +234,7 @@ describe Projects::MergeRequestsController do
end
it 'closes MR without errors' do
- post :update,
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid,
- merge_request: {
- state_event: 'close'
- }
+ update_merge_request(state_event: 'close')
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
expect(merge_request.reload.closed?).to be_truthy
@@ -229,13 +243,7 @@ describe Projects::MergeRequestsController do
it 'allows editing of a closed merge request' do
merge_request.close!
- put :update,
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid,
- merge_request: {
- title: 'New title'
- }
+ update_merge_request(title: 'New title')
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
expect(merge_request.reload.title).to eq 'New title'
@@ -244,13 +252,7 @@ describe Projects::MergeRequestsController do
it 'does not allow to update target branch closed merge request' do
merge_request.close!
- put :update,
- namespace_id: project.namespace,
- project_id: project,
- id: merge_request.iid,
- merge_request: {
- target_branch: 'new_branch'
- }
+ update_merge_request(target_branch: 'new_branch')
expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
end
@@ -278,7 +280,7 @@ describe Projects::MergeRequestsController do
end
it 'returns 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -434,7 +436,7 @@ describe Projects::MergeRequestsController do
it "denies access to users unless they're admin or project owner" do
delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context "when the user is owner" do
@@ -449,12 +451,12 @@ describe Projects::MergeRequestsController do
it "deletes the merge request" do
delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./)
end
it 'delegates the update of the todos count cache to TodoService' do
- expect_any_instance_of(TodoService).to receive(:destroy_merge_request).with(merge_request, owner).once
+ expect_any_instance_of(TodoService).to receive(:destroy_issuable).with(merge_request, owner).once
delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
end
@@ -539,7 +541,7 @@ describe Projects::MergeRequestsController do
subject
end
- it { is_expected.to have_http_status(:success) }
+ it { is_expected.to have_gitlab_http_status(:success) }
it 'renders MergeRequest as JSON' do
subject
@@ -636,7 +638,7 @@ describe Projects::MergeRequestsController do
end
it 'return a detailed head_pipeline status in json' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon
@@ -650,7 +652,7 @@ describe Projects::MergeRequestsController do
end
it 'return empty' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_empty
end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 62f1fb1f697..209979e642d 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -27,7 +27,7 @@ describe Projects::MilestonesController do
it 'shows milestone page' do
view_milestone
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -86,4 +86,32 @@ describe Projects::MilestonesController do
expect(last_note).to eq('removed milestone')
end
end
+
+ describe '#promote' do
+ context 'promotion succeeds' do
+ before do
+ group = create(:group)
+ group.add_developer(user)
+ milestone.project.update(namespace: group)
+ end
+
+ it 'shows group milestone' do
+ post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
+
+ group_milestone = assigns(:milestone)
+
+ expect(response).to redirect_to(group_milestone_path(project.group, group_milestone.iid))
+ expect(flash[:notice]).to eq('Milestone has been promoted to group milestone.')
+ end
+ end
+
+ context 'promotion fails' do
+ it 'shows project milestone' do
+ post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
+
+ expect(response).to redirect_to(project_milestone_path(project, milestone))
+ expect(flash[:alert]).to eq('Promotion failed - Project does not belong to a group.')
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 135fd6449ff..37e9f863fc4 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -59,6 +59,7 @@ describe Projects::NotesController do
expect(note_json[:id]).to eq(note.id)
expect(note_json[:discussion_html]).not_to be_nil
expect(note_json[:diff_discussion_html]).to be_nil
+ expect(note_json[:discussion_line_code]).to be_nil
end
end
@@ -74,6 +75,7 @@ describe Projects::NotesController do
expect(note_json[:id]).to eq(note.id)
expect(note_json[:discussion_html]).not_to be_nil
expect(note_json[:diff_discussion_html]).not_to be_nil
+ expect(note_json[:discussion_line_code]).not_to be_nil
end
end
@@ -92,6 +94,7 @@ describe Projects::NotesController do
expect(note_json[:id]).to eq(note.id)
expect(note_json[:discussion_html]).not_to be_nil
expect(note_json[:diff_discussion_html]).to be_nil
+ expect(note_json[:discussion_line_code]).to be_nil
end
end
@@ -104,6 +107,20 @@ describe Projects::NotesController do
expect(note_json[:id]).to eq(note.id)
expect(note_json[:discussion_html]).to be_nil
expect(note_json[:diff_discussion_html]).to be_nil
+ expect(note_json[:discussion_line_code]).to be_nil
+ end
+
+ context 'when user cannot read commit' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :download_code, project).and_return(false)
+ end
+
+ it 'renders 404' do
+ get :index, params
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
end
@@ -120,6 +137,7 @@ describe Projects::NotesController do
expect(note_json[:html]).not_to be_nil
expect(note_json[:discussion_html]).to be_nil
expect(note_json[:diff_discussion_html]).to be_nil
+ expect(note_json[:discussion_line_code]).to be_nil
end
end
@@ -180,13 +198,13 @@ describe Projects::NotesController do
it "returns status 302 for html" do
post :create, request_params
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
it "returns status 200 for json" do
post :create, request_params.merge(format: :json)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
context 'when merge_request_diff_head_sha present' do
@@ -205,7 +223,7 @@ describe Projects::NotesController do
it "returns status 302 for html" do
post :create, request_params
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
@@ -240,7 +258,7 @@ describe Projects::NotesController do
it 'returns a 404' do
post_create(note_project_id: Project.maximum(:id).succ)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -248,7 +266,7 @@ describe Projects::NotesController do
it 'returns a 404' do
post_create
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -278,7 +296,7 @@ describe Projects::NotesController do
request_params[:note][:noteable_id] = 9999
post :create, request_params.merge(format: :json)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -286,13 +304,13 @@ describe Projects::NotesController do
it 'returns 302 status for html' do
post :create, request_params
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
it 'returns 200 status for json' do
post :create, request_params.merge(format: :json)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'creates a new note' do
@@ -308,7 +326,7 @@ describe Projects::NotesController do
it 'returns 404 status' do
post :create, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'does not create a new note' do
@@ -318,6 +336,29 @@ describe Projects::NotesController do
end
end
+ describe 'PUT update' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: note,
+ format: :json,
+ note: {
+ note: "New comment"
+ }
+ }
+ end
+
+ before do
+ sign_in(note.author)
+ project.team << [note.author, :developer]
+ end
+
+ it "updates the note" do
+ expect { put :update, request_params }.to change { note.reload.note }
+ end
+ end
+
describe 'DELETE destroy' do
let(:request_params) do
{
@@ -337,7 +378,7 @@ describe Projects::NotesController do
it "returns status 200 for html" do
delete :destroy, request_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "deletes the note" do
@@ -354,7 +395,7 @@ describe Projects::NotesController do
it "returns status 404" do
delete :destroy, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -370,7 +411,7 @@ describe Projects::NotesController do
post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
end.to change { note.award_emoji.count }.by(1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "removes the already awarded emoji" do
@@ -380,7 +421,7 @@ describe Projects::NotesController do
post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
end.to change { AwardEmoji.count }.by(-1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -398,7 +439,7 @@ describe Projects::NotesController do
it "returns status 404" do
post :resolve, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -415,7 +456,7 @@ describe Projects::NotesController do
it "returns status 404" do
post :resolve, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -442,7 +483,7 @@ describe Projects::NotesController do
it "returns status 200" do
post :resolve, request_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
@@ -459,7 +500,7 @@ describe Projects::NotesController do
it "returns status 404" do
delete :unresolve, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -476,7 +517,7 @@ describe Projects::NotesController do
it "returns status 404" do
delete :unresolve, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -490,7 +531,7 @@ describe Projects::NotesController do
it "returns status 200" do
delete :unresolve, request_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb
index 83c7744a231..4705c50de7e 100644
--- a/spec/controllers/projects/pages_controller_spec.rb
+++ b/spec/controllers/projects/pages_controller_spec.rb
@@ -21,7 +21,7 @@ describe Projects::PagesController do
it 'returns 200 status' do
get :show, request_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
context 'when the project is in a subgroup' do
@@ -31,7 +31,7 @@ describe Projects::PagesController do
it 'returns a 404 status code' do
get :show, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -40,7 +40,7 @@ describe Projects::PagesController do
it 'returns 302 status' do
delete :destroy, request_params
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
@@ -53,7 +53,7 @@ describe Projects::PagesController do
it 'returns 404 status' do
get :show, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -61,7 +61,7 @@ describe Projects::PagesController do
it 'returns 404 status' do
delete :destroy, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb
index ad4d7da3bdd..e9e7d357d9c 100644
--- a/spec/controllers/projects/pages_domains_controller_spec.rb
+++ b/spec/controllers/projects/pages_domains_controller_spec.rb
@@ -26,7 +26,7 @@ describe Projects::PagesDomainsController do
it "displays the 'show' page" do
get(:show, request_params.merge(id: pages_domain.domain))
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template('show')
end
end
@@ -35,7 +35,7 @@ describe Projects::PagesDomainsController do
it "displays the 'new' page" do
get(:new, request_params)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template('new')
end
end
@@ -69,7 +69,7 @@ describe Projects::PagesDomainsController do
it 'returns 404 status' do
get(:show, request_params.merge(id: pages_domain.domain))
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -77,7 +77,7 @@ describe Projects::PagesDomainsController do
it 'returns 404 status' do
get :new, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -85,7 +85,7 @@ describe Projects::PagesDomainsController do
it "returns 404 status" do
post(:create, request_params.merge(pages_domain: pages_domain_params))
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -93,7 +93,7 @@ describe Projects::PagesDomainsController do
it "deletes the pages domain" do
delete(:destroy, request_params.merge(id: pages_domain.domain))
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index 4ac0559c679..4e52e261920 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -15,7 +15,7 @@ describe Projects::PipelineSchedulesController do
it 'renders the index view' do
visit_pipelines_schedules
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
@@ -35,7 +35,7 @@ describe Projects::PipelineSchedulesController do
end
it 'only shows active pipeline schedules' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:schedules)).to include(pipeline_schedule)
expect(assigns(:schedules)).not_to include(inactive_pipeline_schedule)
end
@@ -57,7 +57,7 @@ describe Projects::PipelineSchedulesController do
it 'initializes a pipeline schedule model' do
get :new, namespace_id: project.namespace.to_param, project_id: project
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:schedule)).to be_a_new(Ci::PipelineSchedule)
end
end
@@ -87,7 +87,7 @@ describe Projects::PipelineSchedulesController do
.to change { Ci::PipelineSchedule.count }.by(1)
.and change { Ci::PipelineScheduleVariable.count }.by(1)
- expect(response).to have_http_status(:found)
+ expect(response).to have_gitlab_http_status(:found)
Ci::PipelineScheduleVariable.last.tap do |v|
expect(v.key).to eq("AAA")
@@ -158,7 +158,7 @@ describe Projects::PipelineSchedulesController do
expect { go }.to change { Ci::PipelineScheduleVariable.count }.by(1)
pipeline_schedule.reload
- expect(response).to have_http_status(:found)
+ expect(response).to have_gitlab_http_status(:found)
expect(pipeline_schedule.variables.last.key).to eq('AAA')
expect(pipeline_schedule.variables.last.value).to eq('AAA123')
end
@@ -324,7 +324,7 @@ describe Projects::PipelineSchedulesController do
it 'loads the pipeline schedule' do
get :edit, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:schedule)).to eq(pipeline_schedule)
end
end
@@ -376,7 +376,7 @@ describe Projects::PipelineSchedulesController do
end
it 'does not delete the pipeline schedule' do
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -391,7 +391,7 @@ describe Projects::PipelineSchedulesController do
delete :destroy, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id
end.to change { project.pipeline_schedules.count }.by(-1)
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 167e80ed9cd..1604a2da485 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -3,33 +3,37 @@ require 'spec_helper'
describe Projects::PipelinesController do
include ApiHelpers
- let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
+ set(:user) { create(:user) }
+ set(:project) { create(:project, :public, :repository) }
let(:feature) { ProjectFeature::DISABLED }
before do
stub_not_protect_default_branch
project.add_developer(user)
- project.project_feature.update(
- builds_access_level: feature)
+ project.project_feature.update(builds_access_level: feature)
sign_in(user)
end
describe 'GET index.json' do
before do
- create(:ci_empty_pipeline, status: 'pending', project: project)
- create(:ci_empty_pipeline, status: 'running', project: project)
- create(:ci_empty_pipeline, status: 'created', project: project)
- create(:ci_empty_pipeline, status: 'success', project: project)
+ branch_head = project.commit
+ parent = branch_head.parent
- get :index, namespace_id: project.namespace,
- project_id: project,
- format: :json
+ create(:ci_empty_pipeline, status: 'pending', project: project, sha: branch_head.id)
+ create(:ci_empty_pipeline, status: 'running', project: project, sha: branch_head.id)
+ create(:ci_empty_pipeline, status: 'created', project: project, sha: parent.id)
+ create(:ci_empty_pipeline, status: 'success', project: project, sha: parent.id)
+ end
+
+ subject do
+ get :index, namespace_id: project.namespace, project_id: project, format: :json
end
it 'returns JSON with serialized pipelines' do
- expect(response).to have_http_status(:ok)
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('pipeline')
expect(json_response).to include('pipelines')
@@ -39,6 +43,12 @@ describe Projects::PipelinesController do
expect(json_response['count']['pending']).to eq 1
expect(json_response['count']['finished']).to eq 1
end
+
+ context 'when performing gitaly calls', :request_store do
+ it 'limits the Gitaly requests' do
+ expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(8)
+ end
+ end
end
describe 'GET show JSON' do
@@ -47,7 +57,7 @@ describe Projects::PipelinesController do
it 'returns the pipeline' do
get_pipeline_json
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to be_an(Array)
expect(json_response['id']).to be(pipeline.id)
expect(json_response['details']).to have_key 'stages'
@@ -101,7 +111,7 @@ describe Projects::PipelinesController do
end
it 'returns html source for stage dropdown' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('projects/pipelines/_stage')
expect(json_response).to include('html')
end
@@ -113,7 +123,7 @@ describe Projects::PipelinesController do
end
it 'responds with not found' do
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -138,7 +148,7 @@ describe Projects::PipelinesController do
end
it 'return a detailed pipeline status in json' do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon
@@ -161,14 +171,14 @@ describe Projects::PipelinesController do
let(:feature) { ProjectFeature::ENABLED }
it 'retries a pipeline without returning any content' do
- expect(response).to have_http_status(:no_content)
+ expect(response).to have_gitlab_http_status(:no_content)
expect(build.reload).to be_retried
end
end
context 'when builds are disabled' do
it 'fails to retry pipeline' do
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@@ -188,14 +198,14 @@ describe Projects::PipelinesController do
let(:feature) { ProjectFeature::ENABLED }
it 'cancels a pipeline without returning any content' do
- expect(response).to have_http_status(:no_content)
+ expect(response).to have_gitlab_http_status(:no_content)
expect(pipeline.reload).to be_canceled
end
end
context 'when builds are disabled' do
it 'fails to retry pipeline' do
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb
index ee46ad00947..b2d83a02290 100644
--- a/spec/controllers/projects/pipelines_settings_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb
@@ -12,20 +12,23 @@ describe Projects::PipelinesSettingsController do
end
describe 'PATCH update' do
- before do
+ subject do
patch :update,
namespace_id: project.namespace.to_param,
project_id: project,
- project: {
- auto_devops_attributes: params
- }
+ project: { auto_devops_attributes: params,
+ run_auto_devops_pipeline_implicit: 'false',
+ run_auto_devops_pipeline_explicit: auto_devops_pipeline }
end
context 'when updating the auto_devops settings' do
let(:params) { { enabled: '', domain: 'mepmep.md' } }
+ let(:auto_devops_pipeline) { 'false' }
it 'redirects to the settings page' do
- expect(response).to have_http_status(302)
+ subject
+
+ expect(response).to have_gitlab_http_status(302)
expect(flash[:notice]).to eq("Pipelines settings for '#{project.name}' were successfully updated.")
end
@@ -33,11 +36,32 @@ describe Projects::PipelinesSettingsController do
let(:params) { { enabled: '' } }
it 'allows enabled to be set to nil' do
+ subject
project_auto_devops.reload
expect(project_auto_devops.enabled).to be_nil
end
end
+
+ context 'when run_auto_devops_pipeline is true' do
+ let(:auto_devops_pipeline) { 'true' }
+
+ it 'queues a CreatePipelineWorker' do
+ expect(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args)
+
+ subject
+ end
+ end
+
+ context 'when run_auto_devops_pipeline is not true' do
+ let(:auto_devops_pipeline) { 'false' }
+
+ it 'does not queue a CreatePipelineWorker' do
+ expect(CreatePipelineWorker).not_to receive(:perform_async).with(project.id, user.id, :web, any_args)
+
+ subject
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 3cb1bec5ea2..a34dc27a5ed 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -8,7 +8,7 @@ describe Projects::ProjectMembersController do
it 'should have the project_members address with a 200 status code' do
get :index, namespace_id: project.namespace, project_id: project
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -30,7 +30,7 @@ describe Projects::ProjectMembersController do
user_ids: project_user.id,
access_level: Gitlab::Access::GUEST
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(project.users).not_to include project_user
end
end
@@ -79,7 +79,7 @@ describe Projects::ProjectMembersController do
project_id: project,
id: 42
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -94,7 +94,7 @@ describe Projects::ProjectMembersController do
project_id: project,
id: member
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(project.members).to include member
end
end
@@ -137,7 +137,7 @@ describe Projects::ProjectMembersController do
delete :leave, namespace_id: project.namespace,
project_id: project
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -168,7 +168,7 @@ describe Projects::ProjectMembersController do
delete :leave, namespace_id: project.namespace,
project_id: project
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -221,7 +221,7 @@ describe Projects::ProjectMembersController do
project_id: project,
id: 42
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -236,7 +236,7 @@ describe Projects::ProjectMembersController do
project_id: project,
id: member
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(project.members).not_to include member
end
end
diff --git a/spec/controllers/projects/prometheus_controller_spec.rb b/spec/controllers/projects/prometheus_controller_spec.rb
index 8407a53272a..bbfe78d305a 100644
--- a/spec/controllers/projects/prometheus_controller_spec.rb
+++ b/spec/controllers/projects/prometheus_controller_spec.rb
@@ -24,7 +24,7 @@ describe Projects::PrometheusController do
it 'returns no content response' do
get :active_metrics, project_params(format: :json)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end
end
@@ -38,7 +38,7 @@ describe Projects::PrometheusController do
it 'returns no content response' do
get :active_metrics, project_params(format: :json)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq(sample_response.deep_stringify_keys)
end
end
@@ -47,7 +47,7 @@ describe Projects::PrometheusController do
it 'returns not found response' do
get :active_metrics, project_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index b4eaab29fed..3a0c3faa7b4 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -13,7 +13,7 @@ describe Projects::RawController do
project_id: public_project,
id: id)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.header['Content-Disposition'])
.to eq('inline')
@@ -30,7 +30,7 @@ describe Projects::RawController do
project_id: public_project,
id: id)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('image/jpeg')
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
@@ -59,7 +59,7 @@ describe Projects::RawController do
project_id: public_project,
id: id)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -70,7 +70,7 @@ describe Projects::RawController do
project_id: public_project,
id: id)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -86,7 +86,7 @@ describe Projects::RawController do
project_id: public_project,
id: id)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.header['Content-Disposition'])
.to eq('inline')
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index 3a3e7467ef2..748ae040928 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -23,12 +23,15 @@ describe Projects::RefsController do
xhr :get,
:logs_tree,
namespace_id: project.namespace.to_param,
- project_id: project, id: 'master',
- path: 'foo/bar/baz.html', format: format
+ project_id: project,
+ id: 'master',
+ path: 'foo/bar/baz.html',
+ format: format
end
it 'never throws MissingTemplate' do
expect { default_get }.not_to raise_error
+ expect { xhr_get(:json) }.not_to raise_error
expect { xhr_get }.not_to raise_error
end
@@ -42,5 +45,12 @@ describe Projects::RefsController do
xhr_get(:js)
expect(response).to be_success
end
+
+ it 'renders JSON' do
+ xhr_get(:json)
+
+ expect(response).to be_success
+ expect(json_response).to be_kind_of(Array)
+ end
end
end
diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb
index 5d9d5351687..17769a14def 100644
--- a/spec/controllers/projects/registry/repositories_controller_spec.rb
+++ b/spec/controllers/projects/registry/repositories_controller_spec.rb
@@ -35,7 +35,7 @@ describe Projects::Registry::RepositoriesController do
it 'successfully renders container repositories' do
go_to_index
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
end
it 'creates a root container repository' do
@@ -46,7 +46,7 @@ describe Projects::Registry::RepositoriesController do
it 'json has a list of projects' do
go_to_index(format: :json)
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('registry/repositories')
end
end
@@ -59,7 +59,7 @@ describe Projects::Registry::RepositoriesController do
it 'successfully renders container repositories' do
go_to_index
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
end
it 'does not ensure root container repository' do
@@ -69,7 +69,7 @@ describe Projects::Registry::RepositoriesController do
it 'responds with json if asked' do
go_to_index(format: :json)
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_kind_of(Array)
end
end
@@ -89,7 +89,7 @@ describe Projects::Registry::RepositoriesController do
it 'deletes a repository' do
expect { delete_repository(repository) }.to change { ContainerRepository.all.count }.by(-1)
- expect(response).to have_http_status(:no_content)
+ expect(response).to have_gitlab_http_status(:no_content)
end
end
end
@@ -100,7 +100,7 @@ describe Projects::Registry::RepositoriesController do
it 'responds with 404' do
go_to_index
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
it 'does not ensure root container repository' do
diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb
index bb702ebeb23..7fee8fd44ff 100644
--- a/spec/controllers/projects/registry/tags_controller_spec.rb
+++ b/spec/controllers/projects/registry/tags_controller_spec.rb
@@ -30,7 +30,7 @@ describe Projects::Registry::TagsController do
it 'receive a list of tags' do
get_tags
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('registry/tags')
expect(response).to include_pagination_headers
end
@@ -44,7 +44,7 @@ describe Projects::Registry::TagsController do
it 'receive a list of tags' do
get_tags
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('registry/tags')
expect(response).to include_pagination_headers
end
@@ -58,7 +58,7 @@ describe Projects::Registry::TagsController do
it 'does not receive a list of tags' do
get_tags
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index f712d1e0d63..8b777eb68ca 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -35,7 +35,7 @@ describe Projects::RepositoriesController do
it "renders Not Found" do
get :archive, namespace_id: project.namespace, project_id: project, ref: "master", format: "zip"
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb
index 2b6f988fd9c..89a13f3c976 100644
--- a/spec/controllers/projects/runners_controller_spec.rb
+++ b/spec/controllers/projects/runners_controller_spec.rb
@@ -29,7 +29,7 @@ describe Projects::RunnersController do
runner.reload
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(runner.description).to eq(new_desc)
end
end
@@ -38,7 +38,7 @@ describe Projects::RunnersController do
it 'destroys the runner' do
delete :destroy, params
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(Ci::Runner.find_by(id: runner.id)).to be_nil
end
end
@@ -53,7 +53,7 @@ describe Projects::RunnersController do
runner.reload
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(runner.active).to eq(true)
end
end
@@ -68,7 +68,7 @@ describe Projects::RunnersController do
runner.reload
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(runner.active).to eq(false)
end
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index efba9cc7306..a907da2b60f 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -19,7 +19,7 @@ describe Projects::ServicesController do
put :test, namespace_id: project.namespace, project_id: project, id: service.to_param
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index a8f4b79b64c..b8fe0f46f57 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -13,7 +13,7 @@ describe Projects::Settings::CiCdController do
it 'renders show with 200 status code' do
get :show, namespace_id: project.namespace, project_id: project
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:show)
end
end
diff --git a/spec/controllers/projects/settings/integrations_controller_spec.rb b/spec/controllers/projects/settings/integrations_controller_spec.rb
index e0f9a5b24a6..3068837f394 100644
--- a/spec/controllers/projects/settings/integrations_controller_spec.rb
+++ b/spec/controllers/projects/settings/integrations_controller_spec.rb
@@ -13,7 +13,7 @@ describe Projects::Settings::IntegrationsController do
it 'renders show with 200 status code' do
get :show, namespace_id: project.namespace, project_id: project
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:show)
end
end
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
index f73471f8ca8..3a4014b7768 100644
--- a/spec/controllers/projects/settings/repository_controller_spec.rb
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -13,7 +13,7 @@ describe Projects::Settings::RepositoryController do
it 'renders show with 200 status code' do
get :show, namespace_id: project.namespace, project_id: project
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:show)
end
end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 3a1550aa730..e7c0b484ede 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -29,7 +29,7 @@ describe Projects::SnippetsController do
project_id: project, page: last_page.to_param
expect(assigns(:snippets).current_page).to eq(last_page)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -41,7 +41,7 @@ describe Projects::SnippetsController do
get :index, namespace_id: project.namespace, project_id: project
expect(assigns(:snippets)).not_to include(project_snippet)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -54,7 +54,7 @@ describe Projects::SnippetsController do
get :index, namespace_id: project.namespace, project_id: project
expect(assigns(:snippets)).to include(project_snippet)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -67,7 +67,7 @@ describe Projects::SnippetsController do
get :index, namespace_id: project.namespace, project_id: project
expect(assigns(:snippets)).to include(project_snippet)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
@@ -316,7 +316,7 @@ describe Projects::SnippetsController do
it 'responds with status 404' do
get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -329,7 +329,7 @@ describe Projects::SnippetsController do
get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
expect(assigns(:snippet)).to eq(project_snippet)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -342,7 +342,7 @@ describe Projects::SnippetsController do
get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param
expect(assigns(:snippet)).to eq(project_snippet)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
@@ -352,7 +352,7 @@ describe Projects::SnippetsController do
it 'responds with status 404' do
get action, namespace_id: project.namespace, project_id: project, id: 42
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -364,7 +364,7 @@ describe Projects::SnippetsController do
it 'responds with status 404' do
get action, namespace_id: project.namespace, project_id: project, id: 42
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/todos_controller_spec.rb b/spec/controllers/projects/todos_controller_spec.rb
index 41d211ed1bb..4622e27e60f 100644
--- a/spec/controllers/projects/todos_controller_spec.rb
+++ b/spec/controllers/projects/todos_controller_spec.rb
@@ -28,13 +28,13 @@ describe Projects::TodosController do
go
end.to change { user.todos.count }.by(1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns todo path and pending count' do
go
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['count']).to eq 1
expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/)
end
@@ -47,7 +47,7 @@ describe Projects::TodosController do
go
end.to change { user.todos.count }.by(0)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'does not create todo for issue when user not logged in' do
@@ -55,7 +55,7 @@ describe Projects::TodosController do
go
end.to change { user.todos.count }.by(0)
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
@@ -68,7 +68,7 @@ describe Projects::TodosController do
it "doesn't create todo" do
expect { go }.not_to change { user.todos.count }
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -96,13 +96,13 @@ describe Projects::TodosController do
go
end.to change { user.todos.count }.by(1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns todo path and pending count' do
go
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['count']).to eq 1
expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/)
end
@@ -115,7 +115,7 @@ describe Projects::TodosController do
go
end.to change { user.todos.count }.by(0)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'does not create todo for merge request user has no access to' do
@@ -123,7 +123,7 @@ describe Projects::TodosController do
go
end.to change { user.todos.count }.by(0)
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
@@ -136,7 +136,7 @@ describe Projects::TodosController do
it "doesn't create todo" do
expect { go }.not_to change { user.todos.count }
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index 775f3998f5d..65b821c9486 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -64,7 +64,7 @@ describe Projects::TreeController do
context "valid SHA commit ID with path" do
let(:id) { '6d39438/.gitignore' }
- it { expect(response).to have_http_status(302) }
+ it { expect(response).to have_gitlab_http_status(302) }
end
end
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index 488bcf31371..c2550b1efa7 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -18,7 +18,7 @@ describe Projects::UploadsController do
namespace_id: project.namespace.to_param,
project_id: project,
format: :json
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
end
@@ -90,7 +90,7 @@ describe Projects::UploadsController do
it "responds with status 200" do
go
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -98,7 +98,7 @@ describe Projects::UploadsController do
it "responds with status 404" do
go
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -117,7 +117,7 @@ describe Projects::UploadsController do
it "responds with status 200" do
go
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -125,7 +125,7 @@ describe Projects::UploadsController do
it "responds with status 404" do
go
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -151,7 +151,7 @@ describe Projects::UploadsController do
it "responds with status 200" do
go
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -192,7 +192,7 @@ describe Projects::UploadsController do
it "responds with status 200" do
go
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -200,7 +200,7 @@ describe Projects::UploadsController do
it "responds with status 404" do
go
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -220,7 +220,7 @@ describe Projects::UploadsController do
it "responds with status 200" do
go
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -228,7 +228,7 @@ describe Projects::UploadsController do
it "responds with status 404" do
go
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -237,7 +237,7 @@ describe Projects::UploadsController do
it "responds with status 404" do
go
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
index 6957fb43c19..d065cd00d00 100644
--- a/spec/controllers/projects/variables_controller_spec.rb
+++ b/spec/controllers/projects/variables_controller_spec.rb
@@ -50,7 +50,7 @@ describe Projects::VariablesController do
post :update, namespace_id: project.namespace.to_param, project_id: project,
id: variable.id, variable: { key: '?', value: variable.value }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template :show
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index d7148e888ab..e7ab714c550 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -24,7 +24,7 @@ describe ProjectsController do
get :new, namespace_id: group.id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template('new')
end
end
@@ -33,7 +33,7 @@ describe ProjectsController do
it 'responds with status 404' do
get :new, namespace_id: group.id
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(response).not_to render_template('new')
end
end
@@ -141,7 +141,7 @@ describe ProjectsController do
end
end
- context 'when the storage is not available', broken_storage: true do
+ context 'when the storage is not available', :broken_storage do
set(:project) { create(:project, :broken_storage) }
before do
@@ -152,7 +152,7 @@ describe ProjectsController do
it 'renders a 503' do
get :show, namespace_id: project.namespace, id: project
- expect(response).to have_http_status(503)
+ expect(response).to have_gitlab_http_status(503)
end
end
@@ -222,6 +222,14 @@ describe ProjectsController do
get :show, namespace_id: public_project.namespace, id: public_project
expect(response).to render_template('_files')
end
+
+ it "renders the readme view" do
+ allow(controller).to receive(:current_user).and_return(user)
+ allow(user).to receive(:project_view).and_return('readme')
+
+ get :show, namespace_id: public_project.namespace, id: public_project
+ expect(response).to render_template('_readme')
+ end
end
context "when the url contains .atom" do
@@ -249,7 +257,7 @@ describe ProjectsController do
get :show, namespace_id: project.namespace, id: project, format: :git
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(namespace_project_path)
end
end
@@ -272,7 +280,7 @@ describe ProjectsController do
expect(project.path).to include 'renamed_path'
expect(assigns(:repository).path).to include project.path
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
@@ -288,7 +296,7 @@ describe ProjectsController do
.not_to change { project.reload.path }
expect(controller).to set_flash[:alert].to(/container registry tags/)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -304,7 +312,7 @@ describe ProjectsController do
id: project.id,
project: params
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
params.each do |param, value|
expect(project.public_send(param)).to eq(value)
end
@@ -337,7 +345,7 @@ describe ProjectsController do
project.reload
expect(project.namespace).to eq(new_namespace)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
context 'when new namespace is empty' do
@@ -356,7 +364,7 @@ describe ProjectsController do
project.reload
expect(project.namespace).to eq(old_namespace)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(flash[:alert]).to eq 'Please select a new namespace for your project.'
end
end
@@ -373,7 +381,7 @@ describe ProjectsController do
delete :destroy, namespace_id: project.namespace, id: project
expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound)
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(dashboard_projects_path)
end
@@ -412,7 +420,7 @@ describe ProjectsController do
end
it 'has http status 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'changes the user incoming email token' do
@@ -488,20 +496,21 @@ describe ProjectsController do
delete(:remove_fork,
namespace_id: project.namespace,
id: project, format: :js)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
describe "GET refs" do
let(:public_project) { create(:project, :public, :repository) }
- it "gets a list of branches and tags" do
- get :refs, namespace_id: public_project.namespace, id: public_project
+ it 'gets a list of branches and tags' do
+ get :refs, namespace_id: public_project.namespace, id: public_project, sort: 'updated_desc'
parsed_body = JSON.parse(response.body)
- expect(parsed_body["Branches"]).to include("master")
- expect(parsed_body["Tags"]).to include("v1.0.0")
- expect(parsed_body["Commits"]).to be_nil
+ expect(parsed_body['Branches']).to include('master')
+ expect(parsed_body['Tags'].first).to eq('v1.1.0')
+ expect(parsed_body['Tags'].last).to eq('v1.0.0')
+ expect(parsed_body['Commits']).to be_nil
end
it "gets a list of branches, tags and commits" do
@@ -536,7 +545,7 @@ describe ProjectsController do
get :show, namespace_id: public_project.namespace, id: public_project
expect(assigns(:project)).to eq(public_project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -575,13 +584,13 @@ describe ProjectsController do
it 'does not 404' do
post :toggle_star, namespace_id: public_project.namespace, id: public_project.path.upcase
- expect(response).not_to have_http_status(404)
+ expect(response).not_to have_gitlab_http_status(404)
end
it 'does not redirect to the correct casing' do
post :toggle_star, namespace_id: public_project.namespace, id: public_project.path.upcase
- expect(response).not_to have_http_status(301)
+ expect(response).not_to have_gitlab_http_status(301)
end
end
@@ -591,7 +600,7 @@ describe ProjectsController do
it 'returns not found' do
post :toggle_star, namespace_id: 'foo', id: 'bar'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -605,13 +614,13 @@ describe ProjectsController do
it 'does not 404' do
delete :destroy, namespace_id: project.namespace, id: project.path.upcase
- expect(response).not_to have_http_status(404)
+ expect(response).not_to have_gitlab_http_status(404)
end
it 'does not redirect to the correct casing' do
delete :destroy, namespace_id: project.namespace, id: project.path.upcase
- expect(response).not_to have_http_status(301)
+ expect(response).not_to have_gitlab_http_status(301)
end
end
@@ -621,7 +630,7 @@ describe ProjectsController do
it 'returns not found' do
delete :destroy, namespace_id: 'foo', id: 'bar'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -638,7 +647,7 @@ describe ProjectsController do
it 'returns 302' do
get :export, namespace_id: project.namespace, id: project
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
@@ -650,7 +659,7 @@ describe ProjectsController do
it 'returns 404' do
get :export, namespace_id: project.namespace, id: project
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -666,7 +675,7 @@ describe ProjectsController do
it 'returns 302' do
get :download_export, namespace_id: project.namespace, id: project
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
@@ -678,7 +687,7 @@ describe ProjectsController do
it 'returns 404' do
get :download_export, namespace_id: project.namespace, id: project
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -694,7 +703,7 @@ describe ProjectsController do
it 'returns 302' do
post :remove_export, namespace_id: project.namespace, id: project
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
@@ -706,7 +715,7 @@ describe ProjectsController do
it 'returns 404' do
post :remove_export, namespace_id: project.namespace, id: project
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -722,7 +731,7 @@ describe ProjectsController do
it 'returns 302' do
post :generate_new_export, namespace_id: project.namespace, id: project
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
end
end
@@ -734,7 +743,7 @@ describe ProjectsController do
it 'returns 404' do
post :generate_new_export, namespace_id: project.namespace, id: project
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 1d3ddfbd220..346944fd5b0 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -118,7 +118,8 @@ describe RegistrationsController do
context 'user does not require password confirmation' do
before do
- stub_application_setting(password_authentication_enabled: false)
+ stub_application_setting(password_authentication_enabled_for_web: false)
+ stub_application_setting(password_authentication_enabled_for_git: false)
end
it 'fails if username confirmation is not provided' do
diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb
index 31593ce7311..54a9af92f07 100644
--- a/spec/controllers/sent_notifications_controller_spec.rb
+++ b/spec/controllers/sent_notifications_controller_spec.rb
@@ -69,7 +69,7 @@ describe SentNotificationsController do
end
it 'returns a 404' do
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index a22fd8eaf9b..55bd4352bd3 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -19,7 +19,7 @@ describe SessionsController do
it 'redirects to :omniauth_authorize_path' do
get(:new)
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to('/saml')
end
end
@@ -28,7 +28,7 @@ describe SessionsController do
it 'responds with 200' do
get(:new, auto_sign_in: 'false')
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb
index 225753333ee..e6148ea1734 100644
--- a/spec/controllers/snippets/notes_controller_spec.rb
+++ b/spec/controllers/snippets/notes_controller_spec.rb
@@ -20,7 +20,7 @@ describe Snippets::NotesController do
end
it "returns status 200" do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "returns not empty array of notes" do
@@ -37,7 +37,7 @@ describe Snippets::NotesController do
it "returns status 404" do
get :index, { snippet_id: internal_snippet }
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -49,7 +49,7 @@ describe Snippets::NotesController do
it "returns status 200" do
get :index, { snippet_id: internal_snippet }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
@@ -63,7 +63,7 @@ describe Snippets::NotesController do
it "returns status 404" do
get :index, { snippet_id: private_snippet }
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -75,7 +75,7 @@ describe Snippets::NotesController do
it "returns status 404" do
get :index, { snippet_id: private_snippet }
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -89,7 +89,7 @@ describe Snippets::NotesController do
it "returns status 200" do
get :index, { snippet_id: private_snippet }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "returns 1 note" do
@@ -134,7 +134,7 @@ describe Snippets::NotesController do
it "returns status 200" do
delete :destroy, request_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "deletes the note" do
@@ -162,7 +162,7 @@ describe Snippets::NotesController do
it "returns status 404" do
delete :destroy, request_params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "does not update the note" do
@@ -182,7 +182,7 @@ describe Snippets::NotesController do
it "toggles the award emoji" do
expect { subject }.to change { note.award_emoji.count }.by(1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "removes the already awarded emoji when it exists" do
@@ -190,7 +190,7 @@ describe Snippets::NotesController do
expect { subject }.to change { AwardEmoji.count }.by(-1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index be273acb69b..9effe47ab05 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -40,7 +40,7 @@ describe SnippetsController do
it 'responds with status 200' do
get :new
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -69,7 +69,7 @@ describe SnippetsController do
it 'responds with status 404' do
get :show, id: other_personal_snippet.to_param
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -78,7 +78,7 @@ describe SnippetsController do
get :show, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
@@ -104,7 +104,7 @@ describe SnippetsController do
get :show, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -129,7 +129,7 @@ describe SnippetsController do
get :show, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -138,7 +138,7 @@ describe SnippetsController do
get :show, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
@@ -152,7 +152,7 @@ describe SnippetsController do
it 'responds with status 404' do
get :show, id: 'doesntexist'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -432,7 +432,7 @@ describe SnippetsController do
it 'responds with status 404' do
get :raw, id: other_personal_snippet.to_param
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -443,7 +443,7 @@ describe SnippetsController do
it 'responds with status 200' do
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'has expected headers' do
@@ -475,7 +475,7 @@ describe SnippetsController do
get :raw, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -500,7 +500,7 @@ describe SnippetsController do
get :raw, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
context 'CRLF line ending' do
@@ -527,7 +527,7 @@ describe SnippetsController do
get :raw, id: personal_snippet.to_param
expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
@@ -541,7 +541,7 @@ describe SnippetsController do
it 'responds with status 404' do
get :raw, id: 'doesntexist'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index b29f3d861be..7e42e43345c 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -18,7 +18,7 @@ describe UploadsController do
it "returns 401 when the user is not logged in" do
post :create, model: model, id: snippet.id, format: :json
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it "returns 404 when user can't comment on a snippet" do
@@ -27,7 +27,7 @@ describe UploadsController do
sign_in(user)
post :create, model: model, id: private_snippet.id, format: :json
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -39,7 +39,7 @@ describe UploadsController do
it "returns an error without file" do
post :create, model: model, id: snippet.id, format: :json
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
it "returns an error with invalid model" do
@@ -50,7 +50,7 @@ describe UploadsController do
it "returns 404 status when object not found" do
post :create, model: model, id: 9999, format: :json
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context 'with valid image' do
@@ -174,7 +174,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
@@ -190,7 +190,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
@@ -214,7 +214,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
@@ -233,7 +233,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
@@ -285,7 +285,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
@@ -301,7 +301,7 @@ describe UploadsController do
it "responds with status 404" do
get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -316,7 +316,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
@@ -335,7 +335,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
@@ -378,7 +378,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
@@ -394,7 +394,7 @@ describe UploadsController do
it "responds with status 404" do
get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -414,7 +414,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
@@ -433,7 +433,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
@@ -485,7 +485,7 @@ describe UploadsController do
it "responds with status 200" do
get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
@@ -501,7 +501,7 @@ describe UploadsController do
it "responds with status 404" do
get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -516,7 +516,7 @@ describe UploadsController do
it 'responds with status 200' do
get :show, model: 'appearance', mounted_as: 'header_logo', id: appearance.id, filename: 'dk.png'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
@@ -535,7 +535,7 @@ describe UploadsController do
it 'responds with status 200' do
get :show, model: 'appearance', mounted_as: 'logo', id: appearance.id, filename: 'dk.png'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 2cecd2646fc..01ab59aa363 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -24,7 +24,7 @@ describe UsersController do
it 'renders the show template' do
get :show, username: user.username
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template('show')
end
end
@@ -49,7 +49,7 @@ describe UsersController do
it 'renders show' do
get :show, username: user.username
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template('show')
end
end
@@ -70,7 +70,7 @@ describe UsersController do
it 'renders 404' do
get :show, username: 'nonexistent'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -82,7 +82,7 @@ describe UsersController do
get :calendar, username: user.username, format: :json
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
context 'forked project' do
@@ -139,7 +139,7 @@ describe UsersController do
context 'format html' do
it 'renders snippets page' do
get :snippets, username: user.username
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to render_template('show')
end
end
@@ -147,7 +147,7 @@ describe UsersController do
context 'format json' do
it 'response with snippets json data' do
get :snippets, username: user.username, format: :json
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(JSON.parse(response.body)).to have_key('html')
end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index c2b59239af9..cf38066dedc 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -119,7 +119,7 @@ FactoryGirl.define do
finished_at nil
end
- factory :ci_build_tag do
+ trait :tag do
tag true
end
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
new file mode 100644
index 00000000000..fab37195113
--- /dev/null
+++ b/spec/factories/clusters/applications/helm.rb
@@ -0,0 +1,35 @@
+FactoryGirl.define do
+ factory :cluster_applications_helm, class: Clusters::Applications::Helm do
+ cluster factory: %i(cluster provided_by_gcp)
+
+ trait :not_installable do
+ status(-2)
+ end
+
+ trait :installable do
+ status 0
+ end
+
+ trait :scheduled do
+ status 1
+ end
+
+ trait :installing do
+ status 2
+ end
+
+ trait :installed do
+ status 3
+ end
+
+ trait :errored do
+ status(-1)
+ status_reason 'something went wrong'
+ end
+
+ trait :timeouted do
+ installing
+ updated_at ClusterWaitForAppInstallationWorker::TIMEOUT.ago
+ end
+ end
+end
diff --git a/spec/factories/clusters/applications/ingress.rb b/spec/factories/clusters/applications/ingress.rb
new file mode 100644
index 00000000000..b103a980655
--- /dev/null
+++ b/spec/factories/clusters/applications/ingress.rb
@@ -0,0 +1,35 @@
+FactoryGirl.define do
+ factory :cluster_applications_ingress, class: Clusters::Applications::Ingress do
+ cluster factory: %i(cluster provided_by_gcp)
+
+ trait :not_installable do
+ status(-2)
+ end
+
+ trait :installable do
+ status 0
+ end
+
+ trait :scheduled do
+ status 1
+ end
+
+ trait :installing do
+ status 2
+ end
+
+ trait :installed do
+ status 3
+ end
+
+ trait :errored do
+ status(-1)
+ status_reason 'something went wrong'
+ end
+
+ trait :timeouted do
+ installing
+ updated_at ClusterWaitForAppInstallationWorker::TIMEOUT.ago
+ end
+ end
+end
diff --git a/spec/factories/clusters/cluster.rb b/spec/factories/clusters/cluster.rb
new file mode 100644
index 00000000000..c4261178f2d
--- /dev/null
+++ b/spec/factories/clusters/cluster.rb
@@ -0,0 +1,39 @@
+FactoryGirl.define do
+ factory :cluster, class: Clusters::Cluster do
+ user
+ name 'test-cluster'
+
+ trait :project do
+ after(:create) do |cluster, evaluator|
+ cluster.projects << create(:project)
+ end
+ end
+
+ trait :provided_by_user do
+ provider_type :user
+ platform_type :kubernetes
+
+ platform_kubernetes do
+ create(:cluster_platform_kubernetes, :configured)
+ end
+ end
+
+ trait :provided_by_gcp do
+ provider_type :gcp
+ platform_type :kubernetes
+
+ before(:create) do |cluster, evaluator|
+ cluster.platform_kubernetes = build(:cluster_platform_kubernetes, :configured)
+ cluster.provider_gcp = build(:cluster_provider_gcp, :created)
+ end
+ end
+
+ trait :providing_by_gcp do
+ provider_type :gcp
+
+ provider_gcp do
+ create(:cluster_provider_gcp, :creating)
+ end
+ end
+ end
+end
diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb
new file mode 100644
index 00000000000..8b3e6ff35fa
--- /dev/null
+++ b/spec/factories/clusters/platforms/kubernetes.rb
@@ -0,0 +1,20 @@
+FactoryGirl.define do
+ factory :cluster_platform_kubernetes, class: Clusters::Platforms::Kubernetes do
+ cluster
+ namespace nil
+ api_url 'https://kubernetes.example.com'
+ token 'a' * 40
+
+ trait :configured do
+ api_url 'https://kubernetes.example.com'
+ token 'a' * 40
+ username 'xxxxxx'
+ password 'xxxxxx'
+
+ after(:create) do |platform_kubernetes, evaluator|
+ pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))
+ platform_kubernetes.ca_cert = File.read(pem_file)
+ end
+ end
+ end
+end
diff --git a/spec/factories/clusters/providers/gcp.rb b/spec/factories/clusters/providers/gcp.rb
new file mode 100644
index 00000000000..a815410512a
--- /dev/null
+++ b/spec/factories/clusters/providers/gcp.rb
@@ -0,0 +1,32 @@
+FactoryGirl.define do
+ factory :cluster_provider_gcp, class: Clusters::Providers::Gcp do
+ cluster
+ gcp_project_id 'test-gcp-project'
+
+ trait :scheduled do
+ access_token 'access_token_123'
+ end
+
+ trait :creating do
+ access_token 'access_token_123'
+
+ after(:build) do |gcp, evaluator|
+ gcp.make_creating('operation-123')
+ end
+ end
+
+ trait :created do
+ endpoint '111.111.111.111'
+
+ after(:build) do |gcp, evaluator|
+ gcp.make_created
+ end
+ end
+
+ trait :errored do
+ after(:build) do |gcp, evaluator|
+ gcp.make_errored('Something wrong')
+ end
+ end
+ end
+end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 169590deb8e..abbe37df90e 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -1,6 +1,7 @@
FactoryGirl.define do
factory :commit_status, class: CommitStatus do
name 'default'
+ stage 'test'
status 'success'
description 'commit status'
pipeline factory: :ci_pipeline_with_one_job
diff --git a/spec/factories/fork_network_members.rb b/spec/factories/fork_network_members.rb
new file mode 100644
index 00000000000..509c4e1fa1c
--- /dev/null
+++ b/spec/factories/fork_network_members.rb
@@ -0,0 +1,8 @@
+FactoryGirl.define do
+ factory :fork_network_member do
+ association :project
+ association :fork_network
+
+ forked_from_project { fork_network.root_project }
+ end
+end
diff --git a/spec/factories/gcp/cluster.rb b/spec/factories/gcp/cluster.rb
deleted file mode 100644
index 630e40da888..00000000000
--- a/spec/factories/gcp/cluster.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-FactoryGirl.define do
- factory :gcp_cluster, class: Gcp::Cluster do
- project
- user
- enabled true
- gcp_project_id 'gcp-project-12345'
- gcp_cluster_name 'test-cluster'
- gcp_cluster_zone 'us-central1-a'
- gcp_cluster_size 1
- gcp_machine_type 'n1-standard-4'
-
- trait :with_kubernetes_service do
- after(:create) do |cluster, evaluator|
- create(:kubernetes_service, project: cluster.project).tap do |service|
- cluster.update(service: service)
- end
- end
- end
-
- trait :custom_project_namespace do
- project_namespace 'sample-app'
- end
-
- trait :created_on_gke do
- status_event :make_created
- endpoint '111.111.111.111'
- ca_cert 'xxxxxx'
- kubernetes_token 'xxxxxx'
- username 'xxxxxx'
- password 'xxxxxx'
- end
-
- trait :errored do
- status_event :make_errored
- status_reason 'general error'
- end
- end
-end
diff --git a/spec/factories/group_custom_attributes.rb b/spec/factories/group_custom_attributes.rb
new file mode 100644
index 00000000000..7ff5f376e8b
--- /dev/null
+++ b/spec/factories/group_custom_attributes.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :group_custom_attribute do
+ group
+ sequence(:key) { |n| "key#{n}" }
+ sequence(:value) { |n| "value#{n}" }
+ end
+end
diff --git a/spec/factories/instance_configuration.rb b/spec/factories/instance_configuration.rb
new file mode 100644
index 00000000000..406c7c3caf1
--- /dev/null
+++ b/spec/factories/instance_configuration.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :instance_configuration do
+ skip_create
+ end
+end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 2c732aaf4ed..cc6cef63b47 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -73,14 +73,20 @@ FactoryGirl.define do
merge_user author
end
+ trait :remove_source_branch do
+ merge_params do
+ { 'force_remove_source_branch' => '1' }
+ end
+ end
+
after(:build) do |merge_request|
target_project = merge_request.target_project
source_project = merge_request.source_project
- # Fake `write_ref` if we don't have repository
+ # Fake `fetch_ref!` if we don't have repository
# We have too many existing tests replying on this behaviour
unless [target_project, source_project].all?(&:repository_exists?)
- allow(merge_request).to receive(:write_ref)
+ allow(merge_request).to receive(:fetch_ref!)
end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index f0d05504b7e..ab4ae123429 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -130,6 +130,7 @@ FactoryGirl.define do
before(:create) do |note, evaluator|
discussion = evaluator.in_reply_to
next unless discussion
+
discussion = discussion.to_discussion if discussion.is_a?(Note)
next unless discussion
diff --git a/spec/factories/project_custom_attributes.rb b/spec/factories/project_custom_attributes.rb
new file mode 100644
index 00000000000..5eedeb86304
--- /dev/null
+++ b/spec/factories/project_custom_attributes.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :project_custom_attribute do
+ project
+ sequence(:key) { |n| "key#{n}" }
+ sequence(:value) { |n| "value#{n}" }
+ end
+end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index c2674ce2d11..ccf63f3ffa4 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -38,6 +38,8 @@ FactoryGirl.define do
active true
properties(
url: 'https://jira.example.com',
+ username: 'jira_user',
+ password: 'my-secret-password',
project_key: 'jira-key'
)
end
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 2144f6ba635..766cd4b0090 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Admin::AbuseReports", js: true do
+describe "Admin::AbuseReports", :js do
let(:user) { create(:user) }
context 'as an admin' do
diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb
index cbccf370514..9cb351282a0 100644
--- a/spec/features/admin/admin_broadcast_messages_spec.rb
+++ b/spec/features/admin/admin_broadcast_messages_spec.rb
@@ -40,7 +40,7 @@ feature 'Admin Broadcast Messages' do
expect(page).not_to have_content 'Migration to new server'
end
- scenario 'Live preview a customized broadcast message', js: true do
+ scenario 'Live preview a customized broadcast message', :js do
fill_in 'broadcast_message_message', with: "Live **Markdown** previews. :tada:"
page.within('.broadcast-message-preview') do
diff --git a/spec/features/admin/admin_disables_two_factor_spec.rb b/spec/features/admin/admin_disables_two_factor_spec.rb
index e214ae6b78d..2abdd3c9ef2 100644
--- a/spec/features/admin/admin_disables_two_factor_spec.rb
+++ b/spec/features/admin/admin_disables_two_factor_spec.rb
@@ -1,13 +1,13 @@
require 'rails_helper'
feature 'Admin disables 2FA for a user' do
- scenario 'successfully', js: true do
+ scenario 'successfully', :js do
sign_in(create(:admin))
user = create(:user, :two_factor)
edit_user(user)
page.within('.two-factor-status') do
- click_link 'Disable'
+ accept_confirm { click_link 'Disable' }
end
page.within('.two-factor-status') do
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 3768727d8ae..a5f22848031 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -52,7 +52,7 @@ feature 'Admin Groups' do
expect_selected_visibility(internal)
end
- scenario 'when entered in group path, it auto filled the group name', js: true do
+ scenario 'when entered in group path, it auto filled the group name', :js do
visit admin_groups_path
click_link "New group"
group_path = 'gitlab'
@@ -81,7 +81,7 @@ feature 'Admin Groups' do
expect_selected_visibility(group.visibility_level)
end
- scenario 'edit group path does not change group name', js: true do
+ scenario 'edit group path does not change group name', :js do
group = create(:group, :private)
visit admin_group_edit_path(group)
@@ -93,7 +93,7 @@ feature 'Admin Groups' do
end
end
- describe 'add user into a group', js: true do
+ describe 'add user into a group', :js do
shared_context 'adds user into a group' do
it do
visit admin_group_path(group)
@@ -124,7 +124,7 @@ feature 'Admin Groups' do
group.add_user(:user, Gitlab::Access::OWNER)
end
- it 'adds admin a to a group as developer', js: true do
+ it 'adds admin a to a group as developer', :js do
visit group_group_members_path(group)
page.within '.users-group-form' do
@@ -141,7 +141,7 @@ feature 'Admin Groups' do
end
end
- describe 'admin remove himself from a group', js: true do
+ describe 'admin remove himself from a group', :js do
it 'removes admin from the group' do
group.add_user(current_user, Gitlab::Access::DEVELOPER)
@@ -152,7 +152,7 @@ feature 'Admin Groups' do
expect(page).to have_content('Developer')
end
- find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
+ accept_confirm { find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click }
visit group_group_members_path(group)
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index 37fd3e171eb..4430fc15501 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature "Admin Health Check", feature: true, broken_storage: true do
+feature "Admin Health Check", :feature, :broken_storage do
include StubENV
before do
@@ -65,9 +65,11 @@ feature "Admin Health Check", feature: true, broken_storage: true do
it 'shows storage failure information' do
hostname = Gitlab::Environment.hostname
+ maximum_failures = Gitlab::CurrentSettings.current_application_settings
+ .circuitbreaker_failure_count_threshold
expect(page).to have_content('broken: failed storage access attempt on host:')
- expect(page).to have_content("#{hostname}: 1 of 10 failures.")
+ expect(page).to have_content("#{hostname}: 1 of #{maximum_failures} failures.")
end
it 'allows resetting storage failures' do
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 91f08dbad5d..eec44549a03 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -62,19 +62,19 @@ describe 'Admin::Hooks', :js do
it 'from hooks list page' do
visit admin_hooks_path
- expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1)
+ expect { accept_confirm { find(:link, 'Remove').send_keys(:return) } }.to change(SystemHook, :count).by(-1)
end
it 'from hook edit page' do
visit admin_hooks_path
click_link 'Edit'
- expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1)
+ expect { accept_confirm { find(:link, 'Remove').send_keys(:return) } }.to change(SystemHook, :count).by(-1)
end
end
end
- describe 'Test', js: true do
+ describe 'Test', :js do
before do
WebMock.stub_request(:post, @system_hook.url)
visit admin_hooks_path
diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb
index ae9b47299e6..de406d7d966 100644
--- a/spec/features/admin/admin_labels_spec.rb
+++ b/spec/features/admin/admin_labels_spec.rb
@@ -30,10 +30,10 @@ RSpec.describe 'admin issues labels' do
end
end
- it 'deletes all labels', js: true do
+ it 'deletes all labels', :js do
page.within '.labels' do
page.all('.btn-remove').each do |remove|
- remove.click
+ accept_confirm { remove.click }
wait_for_requests
end
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index f4f2505d436..94b12105088 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -28,7 +28,7 @@ describe "Admin::Projects" do
expect(page).not_to have_content(archived_project.name)
end
- it 'renders all projects', js: true do
+ it 'renders all projects', :js do
find(:css, '#sort-projects-dropdown').click
click_link 'Show archived projects'
@@ -37,7 +37,7 @@ describe "Admin::Projects" do
expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
end
- it 'renders only archived projects', js: true do
+ it 'renders only archived projects', :js do
find(:css, '#sort-projects-dropdown').click
click_link 'Show archived projects only'
@@ -74,7 +74,7 @@ describe "Admin::Projects" do
.to receive(:move_uploads_to_new_namespace).and_return(true)
end
- it 'transfers project to group web', js: true do
+ it 'transfers project to group web', :js do
visit admin_project_path(project)
click_button 'Search for Namespace'
@@ -91,7 +91,7 @@ describe "Admin::Projects" do
project.team << [user, :master]
end
- it 'adds admin a to a project as developer', js: true do
+ it 'adds admin a to a project as developer', :js do
visit project_project_members_path(project)
page.within '.users-project-form' do
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index c490dce7ab0..1218ea52227 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -73,7 +73,7 @@ feature 'Admin updates settings' do
context 'sign-in restrictions', :js do
it 'de-activates oauth sign-in source' do
- find('.btn', text: 'GitLab.com').click
+ find('input#application_setting_enabled_oauth_sign_in_sources_[value=gitlab]').send_keys(:return)
expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active')
end
@@ -95,6 +95,29 @@ feature 'Admin updates settings' do
expect(find_field('ED25519 SSH keys').value).to eq(forbidden)
end
+ scenario 'Change Performance Bar settings' do
+ group = create(:group)
+
+ check 'Enable the Performance Bar'
+ fill_in 'Allowed group', with: group.path
+
+ click_on 'Save'
+
+ expect(page).to have_content 'Application settings saved successfully'
+
+ expect(find_field('Enable the Performance Bar')).to be_checked
+ expect(find_field('Allowed group').value).to eq group.path
+
+ uncheck 'Enable the Performance Bar'
+
+ click_on 'Save'
+
+ expect(page).to have_content 'Application settings saved successfully'
+
+ expect(find_field('Enable the Performance Bar')).not_to be_checked
+ expect(find_field('Allowed group').value).to be_nil
+ end
+
def check_all_events
page.check('Active')
page.check('Push')
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index 034682dae27..e16eae219a4 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Admin > Users > Impersonation Tokens', js: true do
+describe 'Admin > Users > Impersonation Tokens', :js do
let(:admin) { create(:admin) }
let!(:user) { create(:user) }
@@ -24,7 +24,7 @@ describe 'Admin > Users > Impersonation Tokens', js: true do
fill_in "Name", with: name
# Set date to 1st of next month
- find_field("Expires at").trigger('focus')
+ find_field("Expires at").click
find(".pika-next").click
click_on "1"
@@ -60,7 +60,7 @@ describe 'Admin > Users > Impersonation Tokens', js: true do
it "allows revocation of an active impersonation token" do
visit admin_user_impersonation_tokens_path(user_id: user.username)
- click_on "Revoke"
+ accept_confirm { click_on "Revoke" }
expect(page).to have_selector(".settings-message")
expect(no_personal_access_tokens_message).to have_text("This user has no active Impersonation Tokens.")
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index e2e2b13cf8a..a69b428d117 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -167,19 +167,36 @@ describe "Admin::Users" do
it 'sees impersonation log out icon' do
icon = first('.fa.fa-user-secret')
- expect(icon).not_to eql nil
+ expect(icon).not_to be nil
end
it 'logs out of impersonated user back to original user' do
find(:css, 'li.impersonation a').click
- expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(current_user.username)
+ expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username)
end
it 'is redirected back to the impersonated users page in the admin after stopping' do
find(:css, 'li.impersonation a').click
- expect(current_path).to eql "/admin/users/#{another_user.username}"
+ expect(current_path).to eq("/admin/users/#{another_user.username}")
+ end
+ end
+
+ context 'when impersonating a user with an expired password' do
+ before do
+ another_user.update(password_expires_at: Time.now - 5.minutes)
+ click_link 'Impersonate'
+ end
+
+ it 'does not redirect to password change page' do
+ expect(current_path).to eq('/')
+ end
+
+ it 'is redirected back to the impersonated users page in the admin after stopping' do
+ find(:css, 'li.impersonation a').click
+
+ expect(current_path).to eq("/admin/users/#{another_user.username}")
end
end
end
@@ -288,9 +305,9 @@ describe "Admin::Users" do
end
end
- it 'allows group membership to be revoked', js: true do
+ it 'allows group membership to be revoked', :js do
page.within(first('.group_member')) do
- find('.btn-remove').click
+ accept_confirm { find('.btn-remove').click }
end
wait_for_requests
@@ -309,7 +326,7 @@ describe "Admin::Users" do
end
end
- describe 'remove users secondary email', js: true do
+ describe 'remove users secondary email', :js do
let!(:secondary_email) do
create :email, email: 'secondary@example.com', user: user
end
@@ -319,7 +336,7 @@ describe "Admin::Users" do
expect(page).to have_content("Secondary email: #{secondary_email.email}")
- find("#remove_email_#{secondary_email.id}").click
+ accept_confirm { find("#remove_email_#{secondary_email.id}").click }
expect(page).not_to have_content(secondary_email.email)
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index c2b7543a690..f1ac73ff819 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -32,12 +32,12 @@ feature 'Admin uses repository checks' do
end
end
- scenario 'to clear all repository checks', js: true do
+ scenario 'to clear all repository checks', :js do
visit admin_application_settings_path
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
- click_link 'Clear all repository checks'
+ accept_confirm { find(:link, 'Clear all repository checks').send_keys(:return) }
expect(page).to have_content('Started asynchronous removal of all repository check states.')
end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index 5aae2dbaf91..89c9d377003 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -13,8 +13,10 @@ describe "Dashboard Issues Feed" do
end
describe "atom feed" do
- it "renders atom feed via private token" do
- visit issues_dashboard_path(:atom, private_token: user.private_token)
+ it "renders atom feed via personal access token" do
+ personal_access_token = create(:personal_access_token, user: user)
+
+ visit issues_dashboard_path(:atom, private_token: personal_access_token.token)
expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{user.name} issues")
diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb
index 321c8a2a670..2c0c331b6db 100644
--- a/spec/features/atom/dashboard_spec.rb
+++ b/spec/features/atom/dashboard_spec.rb
@@ -4,9 +4,11 @@ describe "Dashboard Feed" do
describe "GET /" do
let!(:user) { create(:user, name: "Jonh") }
- context "projects atom feed via private token" do
+ context "projects atom feed via personal access token" do
it "renders projects atom feed" do
- visit dashboard_projects_path(:atom, private_token: user.private_token)
+ personal_access_token = create(:personal_access_token, user: user)
+
+ visit dashboard_projects_path(:atom, private_token: personal_access_token.token)
expect(body).to have_selector('feed title')
end
end
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index 3eeb4d35131..4102ac0588a 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -28,10 +28,12 @@ describe 'Issues Feed' do
end
end
- context 'when authenticated via private token' do
+ context 'when authenticated via personal access token' do
it 'renders atom feed' do
+ personal_access_token = create(:personal_access_token, user: user)
+
visit project_issues_path(project, :atom,
- private_token: user.private_token)
+ private_token: personal_access_token.token)
expect(response_headers['Content-Type'])
.to have_content('application/atom+xml')
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 9ce687afb31..2b934d81674 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -4,9 +4,11 @@ describe "User Feed" do
describe "GET /" do
let!(:user) { create(:user) }
- context 'user atom feed via private token' do
+ context 'user atom feed via personal access token' do
it "renders user atom feed" do
- visit user_path(user, :atom, private_token: user.private_token)
+ personal_access_token = create(:personal_access_token, user: user)
+
+ visit user_path(user, :atom, private_token: personal_access_token.token)
expect(body).to have_selector('feed title')
end
end
diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb
index dff6f96b663..7a395f62511 100644
--- a/spec/features/auto_deploy_spec.rb
+++ b/spec/features/auto_deploy_spec.rb
@@ -4,52 +4,74 @@ describe 'Auto deploy' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
- before do
- create :kubernetes_service, project: project
- project.team << [user, :master]
- sign_in user
- end
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ context 'when no deployment service is active' do
+ before do
+ trun_off
+ end
- context 'when no deployment service is active' do
- before do
- project.kubernetes_service.update!(active: false)
+ it 'does not show a button to set up auto deploy' do
+ visit project_path(project)
+ expect(page).to have_no_content('Set up auto deploy')
+ end
end
- it 'does not show a button to set up auto deploy' do
- visit project_path(project)
- expect(page).to have_no_content('Set up auto deploy')
+ context 'when a deployment service is active' do
+ before do
+ trun_on
+ visit project_path(project)
+ end
+
+ it 'shows a button to set up auto deploy' do
+ expect(page).to have_link('Set up auto deploy')
+ end
+
+ it 'includes OpenShift as an available template', :js do
+ click_link 'Set up auto deploy'
+ click_button 'Apply a GitLab CI Yaml template'
+
+ within '.gitlab-ci-yml-selector' do
+ expect(page).to have_content('OpenShift')
+ end
+ end
+
+ it 'creates a merge request using "auto-deploy" branch', :js do
+ click_link 'Set up auto deploy'
+ click_button 'Apply a GitLab CI Yaml template'
+ within '.gitlab-ci-yml-selector' do
+ click_on 'OpenShift'
+ end
+ wait_for_requests
+ click_button 'Commit changes'
+
+ expect(page).to have_content('New Merge Request From auto-deploy into master')
+ end
end
end
- context 'when a deployment service is active' do
+ context 'when user configured kubernetes from Integration > Kubernetes' do
before do
- project.kubernetes_service.update!(active: true)
- visit project_path(project)
+ create :kubernetes_service, project: project
+ project.team << [user, :master]
+ sign_in user
end
- it 'shows a button to set up auto deploy' do
- expect(page).to have_link('Set up auto deploy')
- end
+ let(:trun_on) { project.deployment_platform.update!(active: true) }
+ let(:trun_off) { project.deployment_platform.update!(active: false) }
- it 'includes OpenShift as an available template', js: true do
- click_link 'Set up auto deploy'
- click_button 'Apply a GitLab CI Yaml template'
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
- within '.gitlab-ci-yml-selector' do
- expect(page).to have_content('OpenShift')
- end
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ before do
+ create(:cluster, :provided_by_gcp, projects: [project])
+ project.team << [user, :master]
+ sign_in user
end
- it 'creates a merge request using "auto-deploy" branch', js: true do
- click_link 'Set up auto deploy'
- click_button 'Apply a GitLab CI Yaml template'
- within '.gitlab-ci-yml-selector' do
- click_on 'OpenShift'
- end
- wait_for_requests
- click_button 'Commit changes'
+ let(:trun_on) { project.deployment_platform.cluster.update!(enabled: true) }
+ let(:trun_off) { project.deployment_platform.cluster.update!(enabled: false) }
- expect(page).to have_content('New Merge Request From auto-deploy into master')
- end
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index c480b5b7e34..e4cfcea45a5 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -101,7 +101,7 @@ describe 'Issue Boards add issue modal', :js do
click_button 'Cancel'
end
- first('.board-delete').click
+ accept_confirm { first('.board-delete').click }
click_button('Add issues')
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 33aca6cb527..e8d779f5772 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -1,7 +1,8 @@
require 'rails_helper'
-describe 'Issue Boards', js: true do
+describe 'Issue Boards', :js do
include DragTo
+ include MobileHelpers
let(:group) { create(:group, :nested) }
let(:project) { create(:project, :public, namespace: group) }
@@ -13,7 +14,7 @@ describe 'Issue Boards', js: true do
project.team << [user, :master]
project.team << [user2, :master]
- page.driver.set_cookie('sidebar_collapsed', 'true')
+ set_cookie('sidebar_collapsed', 'true')
sign_in(user)
end
@@ -135,7 +136,7 @@ describe 'Issue Boards', js: true do
it 'allows user to delete board' do
page.within(find('.board:nth-child(2)')) do
- find('.board-delete').click
+ accept_confirm { find('.board-delete').click }
end
wait_for_requests
@@ -150,7 +151,7 @@ describe 'Issue Boards', js: true do
find('.dropdown-menu-close').click
page.within(find('.board:nth-child(2)')) do
- find('.board-delete').click
+ accept_confirm { find('.board-delete').click }
end
wait_for_requests
@@ -171,12 +172,14 @@ describe 'Issue Boards', js: true do
expect(page).to have_selector('.card', count: 20)
expect(page).to have_content('Showing 20 of 58 issues')
+ find('.board .board-list')
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
wait_for_requests
expect(page).to have_selector('.card', count: 40)
expect(page).to have_content('Showing 40 of 58 issues')
+ find('.board .board-list')
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
wait_for_requests
@@ -377,7 +380,7 @@ describe 'Issue Boards', js: true do
end
it 'filters by milestone' do
- set_filter("milestone", "\"#{milestone.title}\"")
+ set_filter("milestone", "\"#{milestone.title}")
click_filter_link(milestone.title)
submit_filter
@@ -398,7 +401,7 @@ describe 'Issue Boards', js: true do
end
it 'filters by label with space after reload' do
- set_filter("label", "\"#{accepting.title}\"")
+ set_filter("label", "\"#{accepting.title}")
click_filter_link(accepting.title)
submit_filter
@@ -449,11 +452,13 @@ describe 'Issue Boards', js: true do
expect(page).to have_selector('.card', count: 20)
expect(page).to have_content('Showing 20 of 51 issues')
+ find('.board .board-list')
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
expect(page).to have_selector('.card', count: 40)
expect(page).to have_content('Showing 40 of 51 issues')
+ find('.board .board-list')
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
expect(page).to have_selector('.card', count: 51)
@@ -517,7 +522,7 @@ describe 'Issue Boards', js: true do
end
it 'allows user to use keyboard shortcuts' do
- find('.boards-list').native.send_keys('i')
+ find('body').native.send_keys('i')
expect(page).to have_content('New Issue')
end
end
@@ -534,7 +539,7 @@ describe 'Issue Boards', js: true do
end
it 'does not show create new list' do
- expect(page).not_to have_selector('.js-new-board-list')
+ expect(page).not_to have_button('.js-new-board-list')
end
it 'does not allow dragging' do
@@ -559,6 +564,9 @@ describe 'Issue Boards', js: true do
end
def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0)
+ # ensure there is enough horizontal space for four boards
+ resize_window(2000, 800)
+
drag_to(selector: selector,
scrollable: '#board-app',
list_from_index: list_from_index,
diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb
index 61b53aa5d1e..435de3861cf 100644
--- a/spec/features/boards/keyboard_shortcut_spec.rb
+++ b/spec/features/boards/keyboard_shortcut_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Issue Boards shortcut', js: true do
+describe 'Issue Boards shortcut', :js do
let(:project) { create(:project) }
before do
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index f67372337ec..5ac4d87e90b 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Issue Boards new issue', js: true do
+describe 'Issue Boards new issue', :js do
let(:project) { create(:project, :public) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, position: 0) }
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index c3bf50ef9d1..205900615c4 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
-describe 'Issue Boards', js: true do
+describe 'Issue Boards', :js do
+ include BoardHelpers
+
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:project) { create(:project, :public) }
@@ -49,7 +51,7 @@ describe 'Issue Boards', js: true do
expect(page).to have_selector('.issue-boards-sidebar')
- find('.gutter-toggle').trigger('click')
+ find('.gutter-toggle').click
expect(page).not_to have_selector('.issue-boards-sidebar')
end
@@ -169,7 +171,7 @@ describe 'Issue Boards', js: true do
end
page.within(find('.board:nth-child(2)')) do
- find('.card:nth-child(2)').trigger('click')
+ find('.card:nth-child(2)').click
end
page.within('.assignee') do
@@ -309,32 +311,50 @@ describe 'Issue Boards', js: true do
expect(card).to have_selector('.label', count: 1)
expect(card).not_to have_content(stretch.title)
end
+
+ it 'creates new label' do
+ click_card(card)
+
+ page.within('.labels') do
+ click_link 'Edit'
+ click_link 'Create new label'
+ fill_in 'new_label_name', with: 'test label'
+ first('.suggest-colors-dropdown a').click
+ click_button 'Create'
+ wait_for_requests
+
+ expect(page).to have_link 'test label'
+ end
+ end
end
context 'subscription' do
it 'changes issue subscription' do
click_card(card)
+ wait_for_requests
- page.within('.subscription') do
+ page.within('.subscriptions') do
click_button 'Subscribe'
wait_for_requests
- expect(page).to have_content("Unsubscribe")
+
+ expect(page).to have_content('Unsubscribe')
end
end
- end
- def click_card(card)
- page.within(card) do
- first('.card-number').click
- end
+ it 'has "Unsubscribe" button when already subscribed' do
+ create(:subscription, user: user, project: project, subscribable: issue2, subscribed: true)
+ visit project_board_path(project, board)
+ wait_for_requests
- wait_for_sidebar
- end
+ click_card(card)
+ wait_for_requests
+
+ page.within('.subscriptions') do
+ click_button 'Unsubscribe'
+ wait_for_requests
- def wait_for_sidebar
- # loop until the CSS transition is complete
- Timeout.timeout(0.5) do
- loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
+ expect(page).to have_content('Subscribe')
+ end
end
end
end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 4fc6956d111..a9530becb65 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -63,8 +63,8 @@ feature 'Contributions Calendar', :js do
Event.create(note_comment_params)
end
- def selected_day_activities
- find('.user-calendar-activities').text
+ def selected_day_activities(visible: true)
+ find('.user-calendar-activities', visible: visible).text
end
before do
@@ -112,7 +112,7 @@ feature 'Contributions Calendar', :js do
end
it 'hides calendar day activities' do
- expect(selected_day_activities).to be_empty
+ expect(selected_day_activities(visible: false)).to be_empty
end
end
end
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb
index af4cc00162a..9bc23baf6cf 100644
--- a/spec/features/ci_lint_spec.rb
+++ b/spec/features/ci_lint_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'CI Lint', js: true do
+describe 'CI Lint', :js do
before do
sign_in(create(:user))
end
@@ -10,6 +10,7 @@ describe 'CI Lint', js: true do
visit ci_lint_path
# Ace editor updates a hidden textarea and it happens asynchronously
# `sleep 0.1` is actually needed here because of this
+ find('#ci-editor')
execute_script("ace.edit('ci-editor').setValue(" + yaml_content.to_json + ");")
sleep 0.1
click_on 'Validate'
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 479fb713297..98586ddbd81 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe 'Commits' do
- include CiStatusHelper
-
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
@@ -33,7 +31,7 @@ describe 'Commits' do
describe 'Commit builds' do
before do
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
end
it { expect(page).to have_content pipeline.sha[0..7] }
@@ -79,7 +77,7 @@ describe 'Commits' do
describe 'Commit builds', :js do
before do
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
end
it 'shows pipeline`s data' do
@@ -95,7 +93,7 @@ describe 'Commits' do
end
it do
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
click_on 'Download artifacts'
expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type)
end
@@ -103,7 +101,7 @@ describe 'Commits' do
describe 'Cancel all builds' do
it 'cancels commit', :js do
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
click_on 'Cancel running'
expect(page).to have_content 'canceled'
end
@@ -111,7 +109,7 @@ describe 'Commits' do
describe 'Cancel build' do
it 'cancels build', :js do
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
find('.js-btn-cancel-pipeline').click
expect(page).to have_content 'canceled'
end
@@ -120,13 +118,13 @@ describe 'Commits' do
describe '.gitlab-ci.yml not found warning' do
context 'ci builds enabled' do
it "does not show warning" do
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
end
it 'shows warning' do
stub_ci_pipeline_yaml_file(nil)
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
expect(page).to have_content '.gitlab-ci.yml not found in this commit'
end
end
@@ -135,7 +133,7 @@ describe 'Commits' do
before do
stub_ci_builds_disabled
stub_ci_pipeline_yaml_file(nil)
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
end
it 'does not show warning' do
@@ -149,7 +147,7 @@ describe 'Commits' do
before do
project.team << [user, :reporter]
build.update_attributes(artifacts_file: artifacts_file)
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
end
it 'Renders header', :js do
@@ -171,7 +169,7 @@ describe 'Commits' do
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
public_builds: false)
build.update_attributes(artifacts_file: artifacts_file)
- visit ci_status_path(pipeline)
+ visit pipeline_path(pipeline)
end
it do
@@ -202,5 +200,12 @@ describe 'Commits' do
expect(page).to have_content("committed #{commit.committed_date.strftime("%b %d, %Y")}")
end
end
+
+ it 'shows the ref switcher with the multi-file editor enabled', :js do
+ set_cookie('new_repo', 'true')
+ visit project_commits_path(project, branch_name)
+
+ expect(find('.js-project-refs-dropdown')).to have_content branch_name
+ end
end
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index 45213dc6995..bef2aa9e0e5 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Container Registry", js: true do
+describe "Container Registry", :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
@@ -47,7 +47,7 @@ describe "Container Registry", js: true do
scenario 'user removes a specific tag from container repository' do
visit_container_registry
- find('.js-toggle-repo').trigger('click')
+ find('.js-toggle-repo').click
wait_for_requests
expect_any_instance_of(ContainerRegistry::Tag)
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index ebcd0ba0dcd..1fcb8d5bc67 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Copy as GFM', js: true do
+describe 'Copy as GFM', :js do
include MarkupHelper
include RepoHelpers
include ActionView::Helpers::JavaScriptHelper
@@ -664,7 +664,7 @@ describe 'Copy as GFM', js: true do
def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil)
js = <<-JS.strip_heredoc
(function(html) {
- var transformer = window.gl.CopyAsGFM[#{transformer.inspect}];
+ var transformer = window.CopyAsGFM[#{transformer.inspect}];
var node = document.createElement('div');
$(html).each(function() { node.appendChild(this) });
@@ -678,7 +678,7 @@ describe 'Copy as GFM', js: true do
node = transformer(node, target);
if (!node) return null;
- return window.gl.CopyAsGFM.nodeToGFM(node);
+ return window.CopyAsGFM.nodeToGFM(node);
})("#{escape_javascript(html)}")
JS
page.evaluate_script(js)
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index bfe9dac3bd4..177cd50dd72 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Cycle Analytics', js: true do
+feature 'Cycle Analytics', :js do
let(:user) { create(:user) }
let(:guest) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb
index 08d8cc7922b..8bab501134b 100644
--- a/spec/features/dashboard/active_tab_spec.rb
+++ b/spec/features/dashboard/active_tab_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Active Tab', js: true do
+RSpec.describe 'Dashboard Active Tab', :js do
before do
sign_in(create(:user))
end
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index b6dce1b8ec4..349f9a47112 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Tooltips on .timeago dates', js: true do
+feature 'Tooltips on .timeago dates', :js do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:created_date) { Date.yesterday.to_time }
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
index 60a16830cdc..1c7932e7964 100644
--- a/spec/features/dashboard/group_spec.rb
+++ b/spec/features/dashboard/group_spec.rb
@@ -5,9 +5,15 @@ RSpec.describe 'Dashboard Group' do
sign_in(create(:user))
end
- it 'creates new group', js: true do
+ it 'defaults sort dropdown to last created' do
visit dashboard_groups_path
- find('.btn-new').trigger('click')
+
+ expect(page).to have_button('Last created')
+ end
+
+ it 'creates new group', :js do
+ visit dashboard_groups_path
+ find('.btn-new').click
new_path = 'Samurai'
new_description = 'Tokugawa Shogunate'
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index 533df7a325c..d92c002b4e7 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -1,57 +1,81 @@
require 'spec_helper'
feature 'Dashboard Groups page', :js do
- let!(:user) { create :user }
- let!(:group) { create(:group) }
- let!(:nested_group) { create(:group, :nested) }
- let!(:another_group) { create(:group) }
+ let(:user) { create :user }
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, :nested) }
+ let(:another_group) { create(:group) }
+
+ def click_group_caret(group)
+ within("#group-#{group.id}") do
+ first('.folder-caret').click
+ end
+ wait_for_requests
+ end
it 'shows groups user is member of' do
group.add_owner(user)
nested_group.add_owner(user)
+ expect(another_group).to be_persisted
+
+ sign_in(user)
+ visit dashboard_groups_path
+ wait_for_requests
+
+ expect(page).to have_content(group.name)
+
+ expect(page).not_to have_content(another_group.name)
+ end
+
+ it 'shows subgroups the user is member of', :nested_groups do
+ group.add_owner(user)
+ nested_group.add_owner(user)
sign_in(user)
visit dashboard_groups_path
+ wait_for_requests
- expect(page).to have_content(group.full_name)
- expect(page).to have_content(nested_group.full_name)
- expect(page).not_to have_content(another_group.full_name)
+ expect(page).to have_content(nested_group.parent.name)
+ click_group_caret(nested_group.parent)
+ expect(page).to have_content(nested_group.name)
end
- describe 'when filtering groups' do
+ describe 'when filtering groups', :nested_groups do
before do
group.add_owner(user)
nested_group.add_owner(user)
+ expect(another_group).to be_persisted
sign_in(user)
visit dashboard_groups_path
end
- it 'filters groups' do
- fill_in 'filter_groups', with: group.name
+ it 'expands when filtering groups' do
+ fill_in 'filter', with: nested_group.name
wait_for_requests
- expect(page).to have_content(group.full_name)
- expect(page).not_to have_content(nested_group.full_name)
- expect(page).not_to have_content(another_group.full_name)
+ expect(page).not_to have_content(group.name)
+ expect(page).to have_content(nested_group.parent.name)
+ expect(page).to have_content(nested_group.name)
+ expect(page).not_to have_content(another_group.name)
end
it 'resets search when user cleans the input' do
- fill_in 'filter_groups', with: group.name
+ fill_in 'filter', with: group.name
wait_for_requests
- fill_in 'filter_groups', with: ''
+ fill_in 'filter', with: ''
wait_for_requests
- expect(page).to have_content(group.full_name)
- expect(page).to have_content(nested_group.full_name)
- expect(page).not_to have_content(another_group.full_name)
+ expect(page).to have_content(group.name)
+ expect(page).to have_content(nested_group.parent.name)
+ expect(page).not_to have_content(another_group.name)
expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2
end
end
- describe 'group with subgroups' do
+ describe 'group with subgroups', :nested_groups do
let!(:subgroup) { create(:group, :public, parent: group) }
before do
@@ -64,33 +88,35 @@ feature 'Dashboard Groups page', :js do
end
it 'shows subgroups inside of its parent group' do
- expect(page).to have_selector('.groups-list-tree-container .group-list-tree', count: 2)
- expect(page).to have_selector(".groups-list-tree-container #group-#{group.id} #group-#{subgroup.id}", count: 1)
+ expect(page).to have_selector("#group-#{group.id}")
+ click_group_caret(group)
+ expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}")
end
it 'can toggle parent group' do
- # Expanded by default
- expect(page).to have_selector("#group-#{group.id} .fa-caret-down", count: 1)
- expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right")
+ # Collapsed by default
+ expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down", count: 1)
+ expect(page).to have_selector("#group-#{group.id} .fa-caret-right")
- # Collapse
- find("#group-#{group.id}").trigger('click')
+ # expand
+ click_group_caret(group)
- expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down")
- expect(page).to have_selector("#group-#{group.id} .fa-caret-right", count: 1)
- expect(page).not_to have_selector("#group-#{group.id} #group-#{subgroup.id}")
+ expect(page).to have_selector("#group-#{group.id} .fa-caret-down")
+ expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right", count: 1)
+ expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}")
- # Expand
- find("#group-#{group.id}").trigger('click')
+ # collapse
+ click_group_caret(group)
- expect(page).to have_selector("#group-#{group.id} .fa-caret-down", count: 1)
- expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right")
- expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}")
+ expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down", count: 1)
+ expect(page).to have_selector("#group-#{group.id} .fa-caret-right")
+ expect(page).not_to have_selector("#group-#{group.id} #group-#{subgroup.id}")
end
end
describe 'when using pagination' do
- let(:group2) { create(:group) }
+ let(:group) { create(:group, created_at: 5.days.ago) }
+ let(:group2) { create(:group, created_at: 2.days.ago) }
before do
group.add_owner(user)
@@ -102,12 +128,9 @@ feature 'Dashboard Groups page', :js do
visit dashboard_groups_path
end
- it 'shows pagination' do
- expect(page).to have_selector('.gl-pagination')
+ it 'loads results for next page' do
expect(page).to have_selector('.gl-pagination .page', count: 2)
- end
- it 'loads results for next page' do
# Check first page
expect(page).to have_content(group2.full_name)
expect(page).to have_selector("#group-#{group2.id}")
@@ -115,7 +138,7 @@ feature 'Dashboard Groups page', :js do
expect(page).not_to have_selector("#group-#{group.id}")
# Go to next page
- find(".gl-pagination .page:not(.active) a").trigger('click')
+ find(".gl-pagination .page:not(.active) a").click
wait_for_requests
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 795335aa106..5b4c00b3c7e 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Dashboard Issues' do
expect(page).not_to have_content(other_issue.title)
end
- it 'shows checkmark when unassigned is selected for assignee', js: true do
+ it 'shows checkmark when unassigned is selected for assignee', :js do
find('.js-assignee-search').click
find('li', text: 'Unassigned').click
find('.js-assignee-search').click
@@ -32,8 +32,8 @@ RSpec.describe 'Dashboard Issues' do
expect(find('li[data-user-id="0"] a.is-active')).to be_visible
end
- it 'shows issues when current user is author', js: true do
- find('#assignee_id', visible: false).set('')
+ it 'shows issues when current user is author', :js do
+ execute_script("document.querySelector('#assignee_id').value=''")
find('.js-author-search', match: :first).click
expect(find('li[data-user-id="null"] a.is-active')).to be_visible
@@ -70,8 +70,8 @@ RSpec.describe 'Dashboard Issues' do
end
describe 'new issue dropdown' do
- it 'shows projects only with issues feature enabled', js: true do
- find('.new-project-item-select-button').trigger('click')
+ it 'shows projects only with issues feature enabled', :js do
+ find('.new-project-item-select-button').click
page.within('.select2-results') do
expect(page).to have_content(project.name_with_namespace)
@@ -79,19 +79,21 @@ RSpec.describe 'Dashboard Issues' do
end
end
- it 'shows the new issue page', js: true do
- find('.new-project-item-select-button').trigger('click')
+ it 'shows the new issue page', :js do
+ find('.new-project-item-select-button').click
wait_for_requests
project_path = "/#{project.path_with_namespace}"
project_json = { name: project.name_with_namespace, url: project_path }.to_json
- # similate selection, and prevent overlap by dropdown menu
+ # simulate selection, and prevent overlap by dropdown menu
+ first('.project-item-select', visible: false)
execute_script("$('.project-item-select').val('#{project_json}').trigger('change');")
+ find('#select2-drop-mask', visible: false)
execute_script("$('#select2-drop-mask').remove();")
- find('.new-project-item-link').trigger('click')
+ find('.new-project-item-link').click
expect(page).to have_current_path("#{project_path}/issues/new")
diff --git a/spec/features/dashboard/label_filter_spec.rb b/spec/features/dashboard/label_filter_spec.rb
index b1a207682c3..6802974c2ee 100644
--- a/spec/features/dashboard/label_filter_spec.rb
+++ b/spec/features/dashboard/label_filter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Dashboard > label filter', js: true do
+describe 'Dashboard > label filter', :js do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:project2) { create(:project, name: 'test2', path: 'test2', namespace: user.namespace) }
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index d26428ec286..991d360ccaf 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -24,8 +24,8 @@ feature 'Dashboard Merge Requests' do
visit merge_requests_dashboard_path
end
- it 'shows projects only with merge requests feature enabled', js: true do
- find('.new-project-item-select-button').trigger('click')
+ it 'shows projects only with merge requests feature enabled', :js do
+ find('.new-project-item-select-button').click
page.within('.select2-results') do
expect(page).to have_content(project.name_with_namespace)
@@ -89,7 +89,7 @@ feature 'Dashboard Merge Requests' do
expect(page).not_to have_content(other_merge_request.title)
end
- it 'shows authored merge requests', js: true do
+ it 'shows authored merge requests', :js do
filter_item_select('Any Assignee', '.js-assignee-search')
filter_item_select(current_user.to_reference, '.js-author-search')
@@ -101,7 +101,7 @@ feature 'Dashboard Merge Requests' do
expect(page).not_to have_content(other_merge_request.title)
end
- it 'shows all merge requests', js: true do
+ it 'shows all merge requests', :js do
filter_item_select('Any Assignee', '.js-assignee-search')
filter_item_select('Any Author', '.js-author-search')
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
index 4a004107408..8f96899fb4f 100644
--- a/spec/features/dashboard/project_member_activity_index_spec.rb
+++ b/spec/features/dashboard/project_member_activity_index_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Project member activity', js: true do
+feature 'Project member activity', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, name: 'x', namespace: user.namespace) }
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 4da95ccc169..fbf8b5c0db6 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -80,7 +80,7 @@ feature 'Dashboard Projects' do
end
end
- describe 'with a pipeline', clean_gitlab_redis_shared_state: true do
+ describe 'with a pipeline', :clean_gitlab_redis_shared_state do
let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) }
before do
diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb
index 54d477f7274..ad0f132da8c 100644
--- a/spec/features/dashboard/todos/todos_filtering_spec.rb
+++ b/spec/features/dashboard/todos/todos_filtering_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Dashboard > User filters todos', js: true do
+feature 'Dashboard > User filters todos', :js do
let(:user_1) { create(:user, username: 'user_1', name: 'user_1') }
let(:user_2) { create(:user, username: 'user_2', name: 'user_2') }
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 30bab7eeaa7..6f916078b1a 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -17,7 +17,7 @@ feature 'Dashboard Todos' do
end
end
- context 'User has a todo', js: true do
+ context 'User has a todo', :js do
before do
create(:todo, :mentioned, user: user, project: project, target: issue, author: author)
sign_in(user)
@@ -52,7 +52,7 @@ feature 'Dashboard Todos' do
end
it 'updates todo count' do
- expect(page).to have_content 'To do 0'
+ expect(page).to have_content 'Todos 0'
expect(page).to have_content 'Done 1'
end
@@ -81,7 +81,7 @@ feature 'Dashboard Todos' do
end
it 'updates todo count' do
- expect(page).to have_content 'To do 1'
+ expect(page).to have_content 'Todos 1'
expect(page).to have_content 'Done 0'
end
end
@@ -177,7 +177,7 @@ feature 'Dashboard Todos' do
end
end
- context 'User has done todos', js: true do
+ context 'User has done todos', :js do
before do
create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author)
sign_in(user)
@@ -200,7 +200,7 @@ feature 'Dashboard Todos' do
end
it 'updates todo count' do
- expect(page).to have_content 'To do 1'
+ expect(page).to have_content 'Todos 1'
expect(page).to have_content 'Done 0'
end
end
@@ -249,14 +249,14 @@ feature 'Dashboard Todos' do
expect(page).to have_selector('.gl-pagination .page', count: 2)
end
- describe 'mark all as done', js: true do
+ describe 'mark all as done', :js do
before do
visit dashboard_todos_path
- find('.js-todos-mark-all').trigger('click')
+ find('.js-todos-mark-all').click
end
it 'shows "All done" message!' do
- expect(page).to have_content 'To do 0'
+ expect(page).to have_content 'Todos 0'
expect(page).to have_content "You're all done!"
expect(page).not_to have_selector('.gl-pagination')
end
@@ -267,7 +267,7 @@ feature 'Dashboard Todos' do
end
end
- describe 'undo mark all as done', js: true do
+ describe 'undo mark all as done', :js do
before do
visit dashboard_todos_path
end
@@ -283,7 +283,7 @@ feature 'Dashboard Todos' do
it 'updates todo count' do
mark_all_and_undo
- expect(page).to have_content 'To do 2'
+ expect(page).to have_content 'Todos 2'
expect(page).to have_content 'Done 0'
end
@@ -309,9 +309,9 @@ feature 'Dashboard Todos' do
end
def mark_all_and_undo
- find('.js-todos-mark-all').trigger('click')
+ find('.js-todos-mark-all').click
wait_for_requests
- find('.js-todos-undo-all').trigger('click')
+ find('.js-todos-undo-all').click
wait_for_requests
end
end
diff --git a/spec/features/discussion_comments/commit_spec.rb b/spec/features/discussion_comments/commit_spec.rb
index 0375d0bf8ff..69d35cdbc72 100644
--- a/spec/features/discussion_comments/commit_spec.rb
+++ b/spec/features/discussion_comments/commit_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Discussion Comments Merge Request', :js do
+describe 'Discussion Comments Commit', :js do
include RepoHelpers
let(:user) { create(:user) }
diff --git a/spec/features/discussion_comments/snippets_spec.rb b/spec/features/discussion_comments/snippets_spec.rb
index 1e6389d9a13..4a236c4639b 100644
--- a/spec/features/discussion_comments/snippets_spec.rb
+++ b/spec/features/discussion_comments/snippets_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Discussion Comments Issue', :js do
+describe 'Discussion Comments Snippet', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 357d86497d9..1dd7547a7fc 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Expand and collapse diffs', js: true do
+feature 'Expand and collapse diffs', :js do
let(:branch) { 'expand-collapse-diffs' }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb
index b5325301968..801a33979ff 100644
--- a/spec/features/explore/groups_list_spec.rb
+++ b/spec/features/explore/groups_list_spec.rb
@@ -13,6 +13,7 @@ describe 'Explore Groups page', :js do
sign_in(user)
visit explore_groups_path
+ wait_for_requests
end
it 'shows groups user is member of' do
@@ -22,7 +23,7 @@ describe 'Explore Groups page', :js do
end
it 'filters groups' do
- fill_in 'filter_groups', with: group.name
+ fill_in 'filter', with: group.name
wait_for_requests
expect(page).to have_content(group.full_name)
@@ -31,10 +32,10 @@ describe 'Explore Groups page', :js do
end
it 'resets search when user cleans the input' do
- fill_in 'filter_groups', with: group.name
+ fill_in 'filter', with: group.name
wait_for_requests
- fill_in 'filter_groups', with: ""
+ fill_in 'filter', with: ""
wait_for_requests
expect(page).to have_content(group.full_name)
@@ -45,21 +46,21 @@ describe 'Explore Groups page', :js do
it 'shows non-archived projects count' do
# Initially project is not archived
- expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1")
+ expect(find('.js-groups-list-holder .content-list li:first-child .stats .number-projects')).to have_text("1")
# Archive project
empty_project.archive!
visit explore_groups_path
# Check project count
- expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("0")
+ expect(find('.js-groups-list-holder .content-list li:first-child .stats .number-projects')).to have_text("0")
# Unarchive project
empty_project.unarchive!
visit explore_groups_path
# Check project count
- expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1")
+ expect(find('.js-groups-list-holder .content-list li:first-child .stats .number-projects')).to have_text("1")
end
describe 'landing component' do
diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb
index c5ec495a418..8d5233d0c0f 100644
--- a/spec/features/explore/new_menu_spec.rb
+++ b/spec/features/explore/new_menu_spec.rb
@@ -65,9 +65,9 @@ feature 'Top Plus Menu', :js do
visit project_path(project)
page.within '.header-content' do
- find('.header-new-dropdown-toggle').trigger('click')
+ find('.header-new-dropdown-toggle').click
expect(page).to have_selector('.header-new.dropdown.open', count: 1)
- find('.header-new-project-snippet a').trigger('click')
+ find('.header-new-project-snippet a').click
end
expect(page).to have_content('New Snippet')
@@ -87,9 +87,9 @@ feature 'Top Plus Menu', :js do
visit group_path(group)
page.within '.header-content' do
- find('.header-new-dropdown-toggle').trigger('click')
+ find('.header-new-dropdown-toggle').click
expect(page).to have_selector('.header-new.dropdown.open', count: 1)
- find('.header-new-group-project a').trigger('click')
+ find('.header-new-group-project a').click
end
expect(page).to have_content('Project path')
@@ -155,7 +155,7 @@ feature 'Top Plus Menu', :js do
def click_topmenuitem(item_name)
page.within '.header-content' do
- find('.header-new-dropdown-toggle').trigger('click')
+ find('.header-new-dropdown-toggle').click
expect(page).to have_selector('.header-new.dropdown.open', count: 1)
click_link item_name
end
diff --git a/spec/features/explore/user_explores_projects_spec.rb b/spec/features/explore/user_explores_projects_spec.rb
new file mode 100644
index 00000000000..6ac9497b024
--- /dev/null
+++ b/spec/features/explore/user_explores_projects_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe 'User explores projects' do
+ set(:archived_project) { create(:project, :archived) }
+ set(:internal_project) { create(:project, :internal) }
+ set(:private_project) { create(:project, :private) }
+ set(:public_project) { create(:project, :public) }
+
+ shared_examples_for 'shows public projects' do
+ it 'shows projects' do
+ expect(page).to have_content(public_project.title)
+ expect(page).not_to have_content(internal_project.title)
+ expect(page).not_to have_content(private_project.title)
+ expect(page).not_to have_content(archived_project.title)
+ end
+ end
+
+ shared_examples_for 'shows public and internal projects' do
+ it 'shows projects' do
+ expect(page).to have_content(public_project.title)
+ expect(page).to have_content(internal_project.title)
+ expect(page).not_to have_content(private_project.title)
+ expect(page).not_to have_content(archived_project.title)
+ end
+ end
+
+ context 'when not signed in' do
+ context 'when viewing public projects' do
+ before do
+ visit(explore_projects_path)
+ end
+
+ include_examples 'shows public projects'
+ end
+ end
+
+ context 'when signed in' do
+ set(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when viewing public projects' do
+ before do
+ visit(explore_projects_path)
+ end
+
+ include_examples 'shows public and internal projects'
+ end
+
+ context 'when viewing most starred projects' do
+ before do
+ visit(starred_explore_projects_path)
+ end
+
+ include_examples 'shows public and internal projects'
+ end
+
+ context 'when viewing trending projects' do
+ before do
+ [archived_project, public_project].each { |project| create(:note_on_issue, project: project) }
+
+ TrendingProject.refresh!
+
+ visit(trending_explore_projects_path)
+ end
+
+ include_examples 'shows public projects'
+ end
+ end
+end
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index 53b3bb3b65f..3c2186b3598 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -49,7 +49,7 @@ describe "GitLab Flavored Markdown" do
end
end
- describe "for issues", js: true do
+ describe "for issues", :js do
before do
@other_issue = create(:issue,
author: user,
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index 37814ba6238..d2d0be35f1c 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Group variables', js: true do
+feature 'Group variables', :js do
let(:user) { create(:user) }
let(:group) { create(:group) }
diff --git a/spec/features/groups/labels/subscription_spec.rb b/spec/features/groups/labels/subscription_spec.rb
index 1dd09d4f203..2e06caf98f6 100644
--- a/spec/features/groups/labels/subscription_spec.rb
+++ b/spec/features/groups/labels/subscription_spec.rb
@@ -11,7 +11,7 @@ feature 'Labels subscription' do
gitlab_sign_in user
end
- scenario 'users can subscribe/unsubscribe to group labels', js: true do
+ scenario 'users can subscribe/unsubscribe to group labels', :js do
visit group_labels_path(group)
expect(page).to have_content('feature')
diff --git a/spec/features/groups/members/manage_members.rb b/spec/features/groups/members/manage_members.rb
index 9039b283393..da1e17225db 100644
--- a/spec/features/groups/members/manage_members.rb
+++ b/spec/features/groups/members/manage_members.rb
@@ -44,7 +44,11 @@ feature 'Groups > Members > Manage members' do
visit group_group_members_path(group)
- find(:css, '.project-members-page li', text: user2.name).find(:css, 'a.btn-remove').click
+ accept_confirm do
+ find(:css, '.project-members-page li', text: user2.name).find(:css, 'a.btn-remove').click
+ end
+
+ wait_for_requests
expect(page).not_to have_content(user2.name)
expect(group.users).not_to include(user2)
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 56144d17d4f..1b41b3842c8 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -18,6 +18,27 @@ feature 'Group milestones', :js do
visit new_group_milestone_path(group)
end
+ it 'renders description preview' do
+ description = find('.note-textarea')
+
+ description.native.send_keys('')
+
+ click_link('Preview')
+
+ preview = find('.js-md-preview')
+
+ expect(preview).to have_content('Nothing to preview.')
+
+ click_link('Write')
+
+ description.native.send_keys(':+1: Nice')
+
+ click_link('Preview')
+
+ expect(preview).to have_css('gl-emoji')
+ expect(find('#milestone_description', visible: false)).not_to be_visible
+ end
+
it 'creates milestone with start date' do
fill_in 'Title', with: 'testing'
find('#milestone_start_date').click
@@ -30,6 +51,13 @@ feature 'Group milestones', :js do
expect(find('.start_date')).to have_content(Date.today.at_beginning_of_month.strftime('%b %-d, %Y'))
end
+
+ it 'description input does not support autocomplete' do
+ description = find('.note-textarea')
+ description.native.send_keys('!')
+
+ expect(page).not_to have_selector('.atwho-view')
+ end
end
context 'milestones list' do
diff --git a/spec/features/groups/milestones_sorting_spec.rb b/spec/features/groups/milestones_sorting_spec.rb
new file mode 100644
index 00000000000..a0fe40cf1d3
--- /dev/null
+++ b/spec/features/groups/milestones_sorting_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+feature 'Milestones sorting', :js do
+ let(:group) { create(:group) }
+ let!(:project) { create(:project_empty_repo, group: group) }
+ let!(:other_project) { create(:project_empty_repo, group: group) }
+ let!(:project_milestone1) { create(:milestone, project: project, title: 'v1.0', due_date: 10.days.from_now) }
+ let!(:other_project_milestone1) { create(:milestone, project: other_project, title: 'v1.0', due_date: 10.days.from_now) }
+ let!(:project_milestone2) { create(:milestone, project: project, title: 'v2.0', due_date: 5.days.from_now) }
+ let!(:other_project_milestone2) { create(:milestone, project: other_project, title: 'v2.0', due_date: 5.days.from_now) }
+ let!(:group_milestone) { create(:milestone, group: group, title: 'v3.0', due_date: 7.days.from_now) }
+ let(:user) { create(:group_member, :master, user: create(:user), group: group ).user }
+
+ before do
+ sign_in(user)
+ end
+
+ scenario 'visit group milestones and sort by due_date_asc' do
+ visit group_milestones_path(group)
+
+ expect(page).to have_button('Due soon')
+
+ # assert default sorting
+ within '.milestones' do
+ expect(page.all('ul.content-list > li').first.text).to include('v2.0')
+ expect(page.all('ul.content-list > li')[1].text).to include('v3.0')
+ expect(page.all('ul.content-list > li').last.text).to include('v1.0')
+ end
+
+ click_button 'Due soon'
+
+ sort_options = find('ul.dropdown-menu-sort li').all('a').collect(&:text)
+
+ expect(sort_options[0]).to eq('Due soon')
+ expect(sort_options[1]).to eq('Due later')
+ expect(sort_options[2]).to eq('Start soon')
+ expect(sort_options[3]).to eq('Start later')
+ expect(sort_options[4]).to eq('Name, ascending')
+ expect(sort_options[5]).to eq('Name, descending')
+
+ click_link 'Due later'
+
+ expect(page).to have_button('Due later')
+
+ within '.milestones' do
+ expect(page.all('ul.content-list > li').first.text).to include('v1.0')
+ expect(page.all('ul.content-list > li')[1].text).to include('v3.0')
+ expect(page.all('ul.content-list > li').last.text).to include('v2.0')
+ end
+ end
+end
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index 303013e59d5..7fc2b383749 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -24,4 +24,35 @@ feature 'Group show page' do
it_behaves_like "an autodiscoverable RSS feed without an RSS token"
end
+
+ context 'subgroup support' do
+ let(:user) { create(:user) }
+
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ context 'when subgroups are supported', :js, :nested_groups do
+ before do
+ allow(Group).to receive(:supports_nested_groups?) { true }
+ visit path
+ end
+
+ it 'allows creating subgroups' do
+ expect(page).to have_css("li[data-text='New subgroup']", visible: false)
+ end
+ end
+
+ context 'when subgroups are not supported' do
+ before do
+ allow(Group).to receive(:supports_nested_groups?) { false }
+ visit path
+ end
+
+ it 'allows creating subgroups' do
+ expect(page).not_to have_selector("li[data-text='New subgroup']", visible: false)
+ end
+ end
+ end
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 4ec2e7e6012..c1f3d94bc20 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -65,7 +65,7 @@ feature 'Group' do
end
it 'updates the team URL on graph path update', :js do
- out_span = find('span[data-bind-out="create_chat_team"]')
+ out_span = find('span[data-bind-out="create_chat_team"]', visible: false)
expect(out_span.text).to be_empty
@@ -85,13 +85,12 @@ feature 'Group' do
end
end
- describe 'create a nested group', :nested_groups, js: true do
+ describe 'create a nested group', :nested_groups, :js do
let(:group) { create(:group, path: 'foo') }
context 'as admin' do
before do
- visit subgroups_group_path(group)
- click_link 'New Subgroup'
+ visit new_group_path(group, parent_id: group.id)
end
it 'creates a nested group' do
@@ -111,8 +110,8 @@ feature 'Group' do
sign_out(:user)
sign_in(user)
- visit subgroups_group_path(group)
- click_link 'New Subgroup'
+ visit new_group_path(group, parent_id: group.id)
+
fill_in 'Group path', with: 'bar'
click_button 'Create group'
@@ -120,16 +119,6 @@ feature 'Group' do
expect(page).to have_content("Group 'bar' was successfully created.")
end
end
-
- context 'when nested group feature is disabled' do
- it 'renders 404' do
- allow(Group).to receive(:supports_nested_groups?).and_return(false)
-
- visit subgroups_group_path(group)
-
- expect(page.status_code).to eq(404)
- end
- end
end
it 'checks permissions to avoid exposing groups by parent_id' do
@@ -142,7 +131,7 @@ feature 'Group' do
expect(page).not_to have_content('secret-group')
end
- describe 'group edit', js: true do
+ describe 'group edit', :js do
let(:group) { create(:group) }
let(:path) { edit_group_path(group) }
let(:new_name) { 'new-name' }
@@ -207,16 +196,18 @@ feature 'Group' do
end
end
- describe 'group page with nested groups', :nested_groups, js: true do
+ describe 'group page with nested groups', :nested_groups, :js do
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
+ let!(:project) { create(:project, namespace: group) }
let!(:path) { group_path(group) }
- it 'has nested groups tab with nested groups inside' do
+ it 'it renders projects and groups on the page' do
visit path
- click_link 'Subgroups'
+ wait_for_requests
expect(page).to have_content(nested_group.name)
+ expect(page).to have_content(project.name)
end
end
diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb
index 925d026ed61..caee7a67aec 100644
--- a/spec/features/issuables/default_sort_order_spec.rb
+++ b/spec/features/issuables/default_sort_order_spec.rb
@@ -26,7 +26,7 @@ describe 'Projects > Issuables > Default sort order' do
MergeRequest.all
end
- context 'in the "merge requests" tab', js: true do
+ context 'in the "merge requests" tab', :js do
let(:issuable_type) { :merge_request }
it 'is "last created"' do
@@ -37,7 +37,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "merge requests / open" tab', js: true do
+ context 'in the "merge requests / open" tab', :js do
let(:issuable_type) { :merge_request }
it 'is "created date"' do
@@ -49,7 +49,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "merge requests / merged" tab', js: true do
+ context 'in the "merge requests / merged" tab', :js do
let(:issuable_type) { :merged_merge_request }
it 'is "last updated"' do
@@ -61,7 +61,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "merge requests / closed" tab', js: true do
+ context 'in the "merge requests / closed" tab', :js do
let(:issuable_type) { :closed_merge_request }
it 'is "last updated"' do
@@ -73,7 +73,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "merge requests / all" tab', js: true do
+ context 'in the "merge requests / all" tab', :js do
let(:issuable_type) { :merge_request }
it 'is "created date"' do
@@ -102,7 +102,7 @@ describe 'Projects > Issuables > Default sort order' do
Issue.all
end
- context 'in the "issues" tab', js: true do
+ context 'in the "issues" tab', :js do
let(:issuable_type) { :issue }
it 'is "created date"' do
@@ -114,7 +114,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "issues / open" tab', js: true do
+ context 'in the "issues / open" tab', :js do
let(:issuable_type) { :issue }
it 'is "created date"' do
@@ -126,7 +126,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "issues / closed" tab', js: true do
+ context 'in the "issues / closed" tab', :js do
let(:issuable_type) { :closed_issue }
it 'is "last updated"' do
@@ -138,7 +138,7 @@ describe 'Projects > Issuables > Default sort order' do
end
end
- context 'in the "issues / all" tab', js: true do
+ context 'in the "issues / all" tab', :js do
let(:issuable_type) { :issue }
it 'is "created date"' do
diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb
new file mode 100644
index 00000000000..e25fd1a6249
--- /dev/null
+++ b/spec/features/issuables/shortcuts_issuable_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+feature 'Blob shortcuts', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:issue) { create(:issue, project: project, author: user) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:note_text) { 'I got this!' }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ describe 'pressing "r"' do
+ describe 'On an Issue' do
+ before do
+ create(:note, noteable: issue, project: project, note: note_text)
+ visit project_issue_path(project, issue)
+ wait_for_requests
+ end
+
+ it 'quotes the selected text' do
+ select_element('.note-text')
+ find('body').native.send_key('r')
+
+ expect(find('.js-main-target-form .js-vue-comment-form').value).to include(note_text)
+ end
+ end
+
+ describe 'On a Merge Request' do
+ before do
+ create(:note, noteable: merge_request, project: project, note: note_text)
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+ end
+
+ it 'quotes the selected text' do
+ select_element('.note-text')
+ find('body').native.send_key('r')
+
+ expect(find('.js-main-target-form #note_note').value).to include(note_text)
+ end
+ end
+ end
+end
diff --git a/spec/features/issuables/user_sees_sidebar_spec.rb b/spec/features/issuables/user_sees_sidebar_spec.rb
index 2bd1c8aab86..c6c2e58ecea 100644
--- a/spec/features/issuables/user_sees_sidebar_spec.rb
+++ b/spec/features/issuables/user_sees_sidebar_spec.rb
@@ -12,7 +12,7 @@ describe 'Issue Sidebar on Mobile' do
sign_in(user)
end
- context 'mobile sidebar on merge requests', js: true do
+ context 'mobile sidebar on merge requests', :js do
before do
visit project_merge_request_path(merge_request.project, merge_request)
end
@@ -20,7 +20,7 @@ describe 'Issue Sidebar on Mobile' do
it_behaves_like "issue sidebar stays collapsed on mobile"
end
- context 'mobile sidebar on issues', js: true do
+ context 'mobile sidebar on issues', :js do
before do
visit project_issue_path(project, issue)
end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index a29acb30163..850b35c4467 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -24,7 +24,7 @@ describe 'Awards Emoji' do
end
# Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529
- it 'does not shows a 500 page', js: true do
+ it 'does not shows a 500 page', :js do
expect(page).to have_text(issue.title)
end
end
@@ -37,37 +37,37 @@ describe 'Awards Emoji' do
wait_for_requests
end
- it 'increments the thumbsdown emoji', js: true do
+ it 'increments the thumbsdown emoji', :js do
find('[data-name="thumbsdown"]').click
wait_for_requests
expect(thumbsdown_emoji).to have_text("1")
end
context 'click the thumbsup emoji' do
- it 'increments the thumbsup emoji', js: true do
+ it 'increments the thumbsup emoji', :js do
find('[data-name="thumbsup"]').click
wait_for_requests
expect(thumbsup_emoji).to have_text("1")
end
- it 'decrements the thumbsdown emoji', js: true do
+ it 'decrements the thumbsdown emoji', :js do
expect(thumbsdown_emoji).to have_text("0")
end
end
context 'click the thumbsdown emoji' do
- it 'increments the thumbsdown emoji', js: true do
+ it 'increments the thumbsdown emoji', :js do
find('[data-name="thumbsdown"]').click
wait_for_requests
expect(thumbsdown_emoji).to have_text("1")
end
- it 'decrements the thumbsup emoji', js: true do
+ it 'decrements the thumbsup emoji', :js do
expect(thumbsup_emoji).to have_text("0")
end
end
- it 'toggles the smiley emoji on a note', js: true do
+ it 'toggles the smiley emoji on a note', :js do
toggle_smiley_emoji(true)
within('.note-body') do
@@ -82,7 +82,7 @@ describe 'Awards Emoji' do
end
context 'execute /award quick action' do
- it 'toggles the emoji award on noteable', js: true do
+ it 'toggles the emoji award on noteable', :js do
execute_quick_action('/award :100:')
expect(find(noteable_award_counter)).to have_text("1")
@@ -95,7 +95,7 @@ describe 'Awards Emoji' do
end
end
- context 'unauthorized user', js: true do
+ context 'unauthorized user', :js do
before do
visit project_issue_path(project, issue)
end
diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb
index e95eb19f7d1..ddb69d414da 100644
--- a/spec/features/issues/award_spec.rb
+++ b/spec/features/issues/award_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issue awards', js: true do
+feature 'Issue awards', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index a89dcdf41dc..fa4d3a55c62 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -9,7 +9,7 @@ feature 'Issues > Labels bulk assignment' do
let!(:feature) { create(:label, project: project, title: 'feature') }
let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
- context 'as an allowed user', js: true do
+ context 'as an allowed user', :js do
before do
project.team << [user, :master]
@@ -405,7 +405,7 @@ feature 'Issues > Labels bulk assignment' do
end
def update_issues
- find('.update-selected-issues').trigger('click')
+ find('.update-selected-issues').click
wait_for_requests
end
diff --git a/spec/features/issues/create_branch_merge_request_spec.rb b/spec/features/issues/create_branch_merge_request_spec.rb
deleted file mode 100644
index 546dc7e8a49..00000000000
--- a/spec/features/issues/create_branch_merge_request_spec.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-require 'rails_helper'
-
-feature 'Create Branch/Merge Request Dropdown on issue page', :feature, :js do
- let(:user) { create(:user) }
- let!(:project) { create(:project, :repository) }
- let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
-
- context 'for team members' do
- before do
- project.team << [user, :developer]
- sign_in(user)
- end
-
- it 'allows creating a merge request from the issue page' do
- visit project_issue_path(project, issue)
-
- perform_enqueued_jobs do
- select_dropdown_option('create-mr')
-
- expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
- expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
-
- wait_for_requests
- end
-
- visit project_issue_path(project, issue)
-
- expect(page).to have_content("created branch 1-cherry-coloured-funk")
- expect(page).to have_content("mentioned in merge request !1")
- end
-
- it 'allows creating a branch from the issue page' do
- visit project_issue_path(project, issue)
-
- select_dropdown_option('create-branch')
-
- wait_for_requests
-
- expect(page).to have_selector('.dropdown-toggle-text ', text: '1-cherry-coloured-funk')
- expect(current_path).to eq project_tree_path(project, '1-cherry-coloured-funk')
- end
-
- context "when there is a referenced merge request" do
- let!(:note) do
- create(:note, :on_issue, :system, project: project, noteable: issue,
- note: "mentioned in #{referenced_mr.to_reference}")
- end
-
- let(:referenced_mr) do
- create(:merge_request, :simple, source_project: project, target_project: project,
- description: "Fixes #{issue.to_reference}", author: user)
- end
-
- before do
- referenced_mr.cache_merge_request_closes_issues!(user)
-
- visit project_issue_path(project, issue)
- end
-
- it 'disables the create branch button' do
- expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hide)')
- expect(page).to have_css('.create-mr-dropdown-wrap .available.hide', visible: false)
- expect(page).to have_content /1 Related Merge Request/
- end
- end
-
- context 'when issue is confidential' do
- it 'disables the create branch button' do
- issue = create(:issue, :confidential, project: project)
-
- visit project_issue_path(project, issue)
-
- expect(page).not_to have_css('.create-mr-dropdown-wrap')
- end
- end
- end
-
- context 'for visitors' do
- before do
- visit project_issue_path(project, issue)
- end
-
- it 'shows no buttons' do
- expect(page).not_to have_selector('.create-mr-dropdown-wrap')
- end
- end
-
- def select_dropdown_option(option)
- find('.create-mr-dropdown-wrap .dropdown-toggle').click
- find("li[data-value='#{option}']").click
- find('.js-create-merge-request').click
- end
-end
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index 80cc8d22999..822ba48e005 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Resolving all open discussions in a merge request from an issue', js: true do
+feature 'Resolving all open discussions in a merge request from an issue', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
index ad5fd0fd97b..f0bed85595c 100644
--- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb
@@ -24,7 +24,7 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do
end
end
- context 'resolving the discussion', js: true do
+ context 'resolving the discussion', :js do
before do
click_button 'Resolve discussion'
end
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 1c4649d0ba9..2e4a25ee15d 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -43,15 +43,16 @@ describe 'Dropdown assignee', :js do
end
it 'should show loading indicator when opened' do
- filtered_search.set('assignee:')
+ slow_requests do
+ filtered_search.set('assignee:')
- expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true)
+ expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true)
+ end
end
it 'should hide loading indicator when loaded' do
filtered_search.set('assignee:')
- expect(find(js_dropdown_assignee)).to have_css('.filter-dropdown-loading')
expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
end
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 3cec59050ab..2fb5e7cdba4 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Dropdown author', js: true do
+describe 'Dropdown author', :js do
include FilteredSearchHelpers
let!(:project) { create(:project) }
@@ -51,9 +51,11 @@ describe 'Dropdown author', js: true do
end
it 'should show loading indicator when opened' do
- filtered_search.set('author:')
+ slow_requests do
+ filtered_search.set('author:')
- expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true)
+ expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true)
+ end
end
it 'should hide loading indicator when loaded' do
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index 44741bcc92d..8db435634fd 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Dropdown emoji', js: true do
+describe 'Dropdown emoji', :js do
include FilteredSearchHelpers
let!(:project) { create(:project, :public) }
@@ -70,9 +70,11 @@ describe 'Dropdown emoji', js: true do
end
it 'should show loading indicator when opened' do
- filtered_search.set('my-reaction:')
+ slow_requests do
+ filtered_search.set('my-reaction:')
- expect(page).to have_css('#js-dropdown-my-reaction .filter-dropdown-loading', visible: true)
+ expect(page).to have_css('#js-dropdown-my-reaction .filter-dropdown-loading', visible: true)
+ end
end
it 'should hide loading indicator when loaded' do
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index c46803112a9..18cdb199c70 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Dropdown label', js: true do
+describe 'Dropdown label', :js do
include FilteredSearchHelpers
let(:project) { create(:project) }
@@ -66,9 +66,11 @@ describe 'Dropdown label', js: true do
end
it 'shows loading indicator when opened and hides it when loaded' do
- filtered_search.set('label:')
+ slow_requests do
+ filtered_search.set('label:')
- expect(find(js_dropdown_label)).to have_css('.filter-dropdown-loading')
+ expect(page).to have_css("#{js_dropdown_label} .filter-dropdown-loading", visible: true)
+ end
expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading')
end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index f6c2e952bea..031eb06723a 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -50,15 +50,16 @@ describe 'Dropdown milestone', :js do
end
it 'should show loading indicator when opened' do
- filtered_search.set('milestone:')
+ slow_requests do
+ filtered_search.set('milestone:')
- expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true)
+ expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true)
+ end
end
it 'should hide loading indicator when loaded' do
filtered_search.set('milestone:')
- expect(find(js_dropdown_milestone)).to have_css('.filter-dropdown-loading')
expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading')
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 630d6a10c9c..b3c50964810 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Filter issues', js: true do
+describe 'Filter issues', :js do
include FilteredSearchHelpers
let(:project) { create(:project) }
@@ -139,7 +139,7 @@ describe 'Filter issues', js: true do
input_filtered_search('label:none')
expect_tokens([label_token('none', false)])
- expect_issues_list_count(8)
+ expect_issues_list_count(4)
expect_filtered_search_input_empty
end
diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb
index 447281ed19d..f355cec3ba9 100644
--- a/spec/features/issues/filtered_search/recent_searches_spec.rb
+++ b/spec/features/issues/filtered_search/recent_searches_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Recent searches', js: true do
+describe 'Recent searches', :js do
include FilteredSearchHelpers
let(:project_1) { create(:project, :public) }
@@ -27,9 +27,8 @@ describe 'Recent searches', js: true do
input_filtered_search('foo', submit: true)
input_filtered_search('bar', submit: true)
- items = all('.filtered-search-history-dropdown-item', visible: false)
+ items = all('.filtered-search-history-dropdown-item', visible: false, count: 2)
- expect(items.count).to eq(2)
expect(items[0].text).to eq('bar')
expect(items[1].text).to eq('foo')
end
@@ -38,9 +37,8 @@ describe 'Recent searches', js: true do
visit project_issues_path(project_1, label_name: 'foo', search: 'bar')
visit project_issues_path(project_1, label_name: 'qux', search: 'garply')
- items = all('.filtered-search-history-dropdown-item', visible: false)
+ items = all('.filtered-search-history-dropdown-item', visible: false, count: 2)
- expect(items.count).to eq(2)
expect(items[0].text).to eq('label:~qux garply')
expect(items[1].text).to eq('label:~foo bar')
end
@@ -50,9 +48,8 @@ describe 'Recent searches', js: true do
visit project_issues_path(project_1, search: 'foo')
- items = all('.filtered-search-history-dropdown-item', visible: false)
+ items = all('.filtered-search-history-dropdown-item', visible: false, count: 3)
- expect(items.count).to eq(3)
expect(items[0].text).to eq('foo')
expect(items[1].text).to eq('saved1')
expect(items[2].text).to eq('saved2')
@@ -69,9 +66,8 @@ describe 'Recent searches', js: true do
input_filtered_search('more', submit: true)
input_filtered_search('things', submit: true)
- items = all('.filtered-search-history-dropdown-item', visible: false)
+ items = all('.filtered-search-history-dropdown-item', visible: false, count: 2)
- expect(items.count).to eq(2)
expect(items[0].text).to eq('things')
expect(items[1].text).to eq('more')
end
@@ -80,7 +76,8 @@ describe 'Recent searches', js: true do
set_recent_searches(project_1_local_storage_key, '["foo", "bar"]')
visit project_issues_path(project_1)
- all('.filtered-search-history-dropdown-item', visible: false)[0].trigger('click')
+ find('.filtered-search-history-dropdown-toggle-button').click
+ all('.filtered-search-history-dropdown-item', count: 2)[0].click
wait_for_filtered_search('foo')
expect(find('.filtered-search').value.strip).to eq('foo')
@@ -90,12 +87,11 @@ describe 'Recent searches', js: true do
set_recent_searches(project_1_local_storage_key, '["foo"]')
visit project_issues_path(project_1)
- items_before = all('.filtered-search-history-dropdown-item', visible: false)
+ find('.filtered-search-history-dropdown-toggle-button').click
+ all('.filtered-search-history-dropdown-item', count: 1)
- expect(items_before.count).to eq(1)
-
- find('.filtered-search-history-clear-button', visible: false).trigger('click')
- items_after = all('.filtered-search-history-dropdown-item', visible: false)
+ find('.filtered-search-history-clear-button').click
+ items_after = all('.filtered-search-history-dropdown-item', count: 0)
expect(items_after.count).to eq(0)
end
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index d4dd570fb37..88688422dc7 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'Search bar', js: true do
+describe 'Search bar', :js do
include FilteredSearchHelpers
let!(:project) { create(:project) }
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 2b624f4842d..0ae70c855db 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -1,8 +1,7 @@
require 'rails_helper'
-describe 'Visual tokens', js: true do
+describe 'Visual tokens', :js do
include FilteredSearchHelpers
- include WaitForRequests
let!(:project) { create(:project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
@@ -28,7 +27,7 @@ describe 'Visual tokens', js: true do
sign_in(user)
create(:issue, project: project)
- page.driver.set_cookie('sidebar_collapsed', 'true')
+ set_cookie('sidebar_collapsed', 'true')
visit project_issues_path(project)
end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 8ce470fc288..2db6f9a2982 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -218,15 +218,54 @@ describe 'New/edit issue', :js do
context 'edit issue' do
before do
- visit project_issue_path(project, issue)
- page.within('.content .issuable-actions') do
- click_on 'Edit'
+ visit edit_project_issue_path(project, issue)
+ end
+
+ it 'allows user to update issue' do
+ expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
+ expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
+
+ page.within '.js-user-search' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+ expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+ click_button 'Save changes'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
end
end
it 'description has autocomplete' do
- find_field('issue-description').native.send_keys('')
- fill_in 'issue-description', with: '@'
+ find('#issue_description').native.send_keys('')
+ fill_in 'issue_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index c6cf6265645..95d637265e0 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'GFM autocomplete', js: true do
+feature 'GFM autocomplete', :js do
let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let(:project) { create(:project) }
let(:label) { create(:label, project: project, title: 'special+') }
@@ -17,9 +17,9 @@ feature 'GFM autocomplete', js: true do
it 'updates issue descripton with GFM reference' do
find('.issuable-edit').click
- find('#issue-description').native.send_keys("@#{user.name[0...3]}")
+ simulate_input('#issue-description', "@#{user.name[0...3]}")
- find('.atwho-view .cur').trigger('click')
+ find('.atwho-view .cur').click
click_button 'Save changes'
@@ -28,7 +28,6 @@ feature 'GFM autocomplete', js: true do
it 'opens autocomplete menu when field starts with text' do
page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('')
find('#note-body').native.send_keys('@')
end
@@ -46,7 +45,6 @@ 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-body').native.send_keys('')
find('#note-body').native.send_keys(':')
end
@@ -86,7 +84,6 @@ feature 'GFM autocomplete', js: true do
it 'selects the first item for assignee dropdowns' do
page.within '.timeline-content-form' do
- find('#note-body').native.send_keys('')
find('#note-body').native.send_keys('@')
end
@@ -100,7 +97,7 @@ 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-body').native.send_keys('')
- find('#note-body').native.send_keys("@#{user.name[0...8]}")
+ simulate_input('#note-body', "@#{user.name[0...8]}")
end
expect(page).to have_selector('.atwho-container')
@@ -112,7 +109,6 @@ 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-body').native.send_keys('')
find('#note-body').native.send_keys(':1')
end
@@ -127,9 +123,8 @@ feature 'GFM autocomplete', js: true do
it 'wraps the result in double quotes' do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
- note.native.send_keys("~#{label.title[0]}")
- note.click
+ find('#note-body').native.send_keys('')
+ simulate_input('#note-body', "~#{label.title[0]}")
end
label_item = find('.atwho-view li', text: label.title)
@@ -152,16 +147,13 @@ feature 'GFM autocomplete', js: true do
it "does not show dropdown when preceded with a special character" do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
note.native.send_keys("@")
- note.click
end
expect(page).to have_selector('.atwho-container')
page.within '.timeline-content-form' do
note.native.send_keys("@")
- note.click
end
expect(page).to have_selector('.atwho-container', visible: false)
@@ -170,9 +162,7 @@ feature 'GFM autocomplete', js: true do
it "does not throw an error if no labels exist" do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
note.native.send_keys('~')
- note.click
end
expect(page).to have_selector('.atwho-container', visible: false)
@@ -181,9 +171,7 @@ feature 'GFM autocomplete', js: true do
it 'doesn\'t wrap for assignee values' do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
note.native.send_keys("@#{user.username[0]}")
- note.click
end
user_item = find('.atwho-view li', text: user.username)
@@ -194,9 +182,7 @@ feature 'GFM autocomplete', js: true do
it 'doesn\'t wrap for emoji values' do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
- note.native.send_keys(":cartwheel")
- note.click
+ note.native.send_keys(":cartwheel_")
end
emoji_item = find('.atwho-view li', text: 'cartwheel_tone1')
@@ -223,28 +209,27 @@ feature 'GFM autocomplete', js: true do
it 'triggers autocomplete after selecting a quick action' do
note = find('#note-body')
page.within '.timeline-content-form' do
- note.native.send_keys('')
note.native.send_keys('/as')
- note.click
end
- find('.atwho-view li', text: '/assign').native.send_keys(:tab)
+ find('.atwho-view li', text: '/assign')
+ note.native.send_keys(:tab)
user_item = find('.atwho-view li', text: user.username)
expect(user_item).to have_content(user.username)
end
+ end
- def expect_to_wrap(should_wrap, item, note, value)
- expect(item).to have_content(value)
- expect(item).not_to have_content("\"#{value}\"")
+ def expect_to_wrap(should_wrap, item, note, value)
+ expect(item).to have_content(value)
+ expect(item).not_to have_content("\"#{value}\"")
- item.click
+ item.click
- if should_wrap
- expect(note.value).to include("\"#{value}\"")
- else
- expect(note.value).not_to include("\"#{value}\"")
- end
+ if should_wrap
+ expect(note.value).to include("\"#{value}\"")
+ else
+ expect(note.value).not_to include("\"#{value}\"")
end
end
end
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index c0c396af93f..4224a8fe5d4 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -1,9 +1,9 @@
require 'rails_helper'
feature 'Issue Detail', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:issue) { create(:issue, project: project, author: user) }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project, author: user) }
context 'when user displays the issue' do
before do
@@ -25,8 +25,9 @@ feature 'Issue Detail', :js do
wait_for_requests
click_link 'Edit'
- fill_in 'issue-title', with: 'issue title'
+ fill_in 'issuable-title', with: 'issue title'
click_button 'Save'
+ wait_for_requests
Users::DestroyService.new(user).execute(user)
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index af11b474842..a9de52bd8d5 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -13,7 +13,7 @@ feature 'Issue Sidebar' do
sign_in(user)
end
- context 'assignee', js: true do
+ context 'assignee', :js do
let(:user2) { create(:user) }
let(:issue2) { create(:issue, project: project, author: user2) }
@@ -82,7 +82,7 @@ feature 'Issue Sidebar' do
visit_issue(project, issue)
end
- context 'sidebar', js: true do
+ context 'sidebar', :js do
it 'changes size when the screen size is smaller' do
sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
# Resize the window
@@ -101,7 +101,7 @@ feature 'Issue Sidebar' do
end
end
- context 'editing issue labels', js: true do
+ context 'editing issue labels', :js do
before do
page.within('.block.labels') do
find('.edit-link').click
@@ -114,7 +114,7 @@ feature 'Issue Sidebar' do
end
end
- context 'creating a new label', js: true do
+ context 'creating a new label', :js do
before do
page.within('.block.labels') do
click_link 'Create new'
@@ -130,8 +130,8 @@ feature 'Issue Sidebar' do
it 'adds new label' do
page.within('.block.labels') do
fill_in 'new_label_name', with: 'wontfix'
- page.find('.suggest-colors a', match: :first).trigger('click')
- page.find('button', text: 'Create').trigger('click')
+ page.find('.suggest-colors a', match: :first).click
+ page.find('button', text: 'Create').click
page.within('.dropdown-page-one') do
expect(page).to have_content 'wontfix'
@@ -142,8 +142,8 @@ feature 'Issue Sidebar' do
it 'shows error message if label title is taken' do
page.within('.block.labels') do
fill_in 'new_label_name', with: label.title
- page.find('.suggest-colors a', match: :first).trigger('click')
- page.find('button', text: 'Create').trigger('click')
+ page.find('.suggest-colors a', match: :first).click
+ page.find('button', text: 'Create').click
page.within('.dropdown-page-two') do
expect(page).to have_content 'Title has already been taken'
@@ -170,7 +170,7 @@ feature 'Issue Sidebar' do
end
def open_issue_sidebar
- find('aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle').trigger('click')
+ find('aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle').click
find('aside.right-sidebar.right-sidebar-expanded')
end
end
diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb
index 634ea111dc1..fee8fd9b365 100644
--- a/spec/features/issues/markdown_toolbar_spec.rb
+++ b/spec/features/issues/markdown_toolbar_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issue markdown toolbar', js: true do
+feature 'Issue markdown toolbar', :js do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
@@ -16,6 +16,7 @@ feature 'Issue markdown toolbar', js: true do
find('#note-body').native.send_key(:enter)
find('#note-body').native.send_keys('bold')
+ find('.js-main-target-form #note-body')
page.evaluate_script('document.querySelectorAll(".js-main-target-form #note-body")[0].setSelectionRange(4, 9)')
first('.toolbar-btn').click
@@ -28,6 +29,7 @@ feature 'Issue markdown toolbar', js: true do
find('#note-body').native.send_key(:enter)
find('#note-body').native.send_keys('underline')
+ find('.js-main-target-form #note-body')
page.evaluate_script('document.querySelectorAll(".js-main-target-form #note-body")[0].setSelectionRange(4, 50)')
find('.toolbar-btn:nth-child(2)').click
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index b2724945da4..17035b5501c 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -37,8 +37,8 @@ feature 'issue move to another project' do
visit issue_path(issue)
end
- scenario 'moving issue to another project', js: true do
- find('.js-move-issue').trigger('click')
+ scenario 'moving issue to another project', :js do
+ find('.js-move-issue').click
wait_for_requests
all('.js-move-issue-dropdown-item')[0].click
find('.js-move-issue-confirmation-button').click
@@ -49,10 +49,10 @@ feature 'issue move to another project' do
expect(page.current_path).to include project_path(new_project)
end
- scenario 'searching project dropdown', js: true do
+ scenario 'searching project dropdown', :js do
new_project_search.team << [user, :reporter]
- find('.js-move-issue').trigger('click')
+ find('.js-move-issue').click
wait_for_requests
page.within '.js-sidebar-move-issue-block' do
@@ -63,13 +63,13 @@ feature 'issue move to another project' do
end
end
- context 'user does not have permission to move the issue to a project', js: true do
+ context 'user does not have permission to move the issue to a project', :js do
let!(:private_project) { create(:project, :private) }
let(:another_project) { create(:project) }
background { another_project.team << [user, :guest] }
scenario 'browsing projects in projects select' do
- find('.js-move-issue').trigger('click')
+ find('.js-move-issue').click
wait_for_requests
page.within '.js-sidebar-move-issue-block' do
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
index 332ce78b138..d25231d624c 100644
--- a/spec/features/issues/spam_issues_spec.rb
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'New issue', js: true do
+describe 'New issue', :js do
include StubENV
let(:project) { create(:project, :public) }
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 8405f1cd48d..29a2d38ae18 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Manually create a todo item from issue', js: true do
+feature 'Manually create a todo item from issue', :js do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index 1f57c110c11..bcc6e9bab0f 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -118,7 +118,7 @@ feature 'Multiple issue updating from issues#index', :js do
end
def click_update_issues_button
- find('.update-selected-issues').trigger('click')
+ find('.update-selected-issues').click
wait_for_requests
end
end
diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
new file mode 100644
index 00000000000..539d7e9ff01
--- /dev/null
+++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
@@ -0,0 +1,248 @@
+require 'rails_helper'
+
+describe 'User creates branch and merge request on issue page', :js do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :repository) }
+ let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
+
+ context 'when signed out' do
+ before do
+ visit project_issue_path(project, issue)
+ end
+
+ it "doesn't show 'Create merge request' button" do
+ expect(page).not_to have_selector('.create-mr-dropdown-wrap')
+ end
+ end
+
+ context 'when signed in' do
+ before do
+ project.add_developer(user)
+
+ sign_in(user)
+ end
+
+ context 'when interacting with the dropdown' do
+ before do
+ visit project_issue_path(project, issue)
+ end
+
+ # In order to improve tests performance, all UI checks are placed in this test.
+ it 'shows elements' do
+ button_create_merge_request = find('.js-create-merge-request')
+ button_toggle_dropdown = find('.create-mr-dropdown-wrap .dropdown-toggle')
+
+ button_toggle_dropdown.click
+
+ dropdown = find('.create-merge-request-dropdown-menu')
+
+ page.within(dropdown) do
+ button_create_target = find('.js-create-target')
+ input_branch_name = find('.js-branch-name')
+ input_source = find('.js-ref')
+ li_create_branch = find("li[data-value='create-branch']")
+ li_create_merge_request = find("li[data-value='create-mr']")
+
+ # Test that all elements are presented.
+ expect(page).to have_content('Create merge request and branch')
+ expect(page).to have_content('Create branch')
+ expect(page).to have_content('Branch name')
+ expect(page).to have_content('Source (branch or tag)')
+ expect(page).to have_button('Create merge request')
+ expect(page).to have_selector('.js-branch-name:focus')
+
+ test_selection_mark(li_create_branch, li_create_merge_request, button_create_target, button_create_merge_request)
+ test_branch_name_checking(input_branch_name)
+ test_source_checking(input_source)
+
+ # The button inside dropdown should be disabled if any errors occured.
+ expect(page).to have_button('Create branch', disabled: true)
+ end
+
+ # The top level button should be disabled if any errors occured.
+ expect(page).to have_button('Create branch', disabled: true)
+ end
+
+ context 'when branch name is auto-generated' do
+ it 'creates a merge request' do
+ perform_enqueued_jobs do
+ select_dropdown_option('create-mr')
+
+ expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
+ expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
+
+ wait_for_requests
+ end
+
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_content('created branch 1-cherry-coloured-funk')
+ expect(page).to have_content('mentioned in merge request !1')
+ end
+
+ it 'creates a branch' do
+ select_dropdown_option('create-branch')
+
+ wait_for_requests
+
+ expect(page).to have_selector('.dropdown-toggle-text ', text: '1-cherry-coloured-funk')
+ expect(current_path).to eq project_tree_path(project, '1-cherry-coloured-funk')
+ end
+ end
+
+ context 'when branch name is custom' do
+ let(:branch_name) { 'custom-branch-name' }
+
+ it 'creates a merge request' do
+ perform_enqueued_jobs do
+ select_dropdown_option('create-mr', branch_name)
+
+ expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
+ expect(page).to have_content('Request to merge custom-branch-name into')
+ expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
+
+ wait_for_requests
+ end
+
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_content('created branch custom-branch-name')
+ expect(page).to have_content('mentioned in merge request !1')
+ end
+
+ it 'creates a branch' do
+ select_dropdown_option('create-branch', branch_name)
+
+ wait_for_requests
+
+ expect(page).to have_selector('.dropdown-toggle-text ', text: branch_name)
+ expect(current_path).to eq project_tree_path(project, branch_name)
+ end
+ end
+ end
+
+ context "when there is a referenced merge request" do
+ let!(:note) do
+ create(:note, :on_issue, :system, project: project, noteable: issue,
+ note: "mentioned in #{referenced_mr.to_reference}")
+ end
+
+ let(:referenced_mr) do
+ create(:merge_request, :simple, source_project: project, target_project: project,
+ description: "Fixes #{issue.to_reference}", author: user)
+ end
+
+ before do
+ referenced_mr.cache_merge_request_closes_issues!(user)
+
+ visit project_issue_path(project, issue)
+ end
+
+ it 'disables the create branch button' do
+ expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hide)')
+ expect(page).to have_css('.create-mr-dropdown-wrap .available.hide', visible: false)
+ expect(page).to have_content /1 Related Merge Request/
+ end
+ end
+
+ context 'when merge requests are disabled' do
+ before do
+ project.project_feature.update(merge_requests_access_level: 0)
+
+ visit project_issue_path(project, issue)
+ end
+
+ it 'shows only create branch button' do
+ expect(page).not_to have_button('Create merge request')
+ expect(page).to have_button('Create branch')
+ end
+ end
+
+ context 'when issue is confidential' do
+ let(:issue) { create(:issue, :confidential, project: project) }
+
+ it 'disables the create branch button' do
+ visit project_issue_path(project, issue)
+
+ expect(page).not_to have_css('.create-mr-dropdown-wrap')
+ end
+ end
+ end
+
+ private
+
+ def select_dropdown_option(option, branch_name = nil)
+ find('.create-mr-dropdown-wrap .dropdown-toggle').click
+ find("li[data-value='#{option}']").click
+
+ if branch_name
+ find('.js-branch-name').set(branch_name)
+
+ # Javascript debounces AJAX calls.
+ # So we have to wait until AJAX requests are started.
+ # Details are in app/assets/javascripts/create_merge_request_dropdown.js
+ # this.refDebounce = _.debounce(...)
+ sleep 0.5
+
+ wait_for_requests
+ end
+
+ find('.js-create-merge-request').click
+ end
+
+ def test_branch_name_checking(input_branch_name)
+ expect(input_branch_name.value).to eq(issue.to_branch_name)
+
+ input_branch_name.set('new-branch-name')
+ branch_name_message = find('.js-branch-message')
+
+ expect(branch_name_message).to have_text('Checking branch name availability…')
+
+ wait_for_requests
+
+ expect(branch_name_message).to have_text('Branch name is available')
+
+ input_branch_name.set(project.default_branch)
+
+ expect(branch_name_message).to have_text('Checking branch name availability…')
+
+ wait_for_requests
+
+ expect(branch_name_message).to have_text('Branch is already taken')
+ end
+
+ def test_selection_mark(li_create_branch, li_create_merge_request, button_create_target, button_create_merge_request)
+ page.within(li_create_merge_request) do
+ expect(page).to have_css('i.fa.fa-check')
+ expect(button_create_target).to have_text('Create merge request')
+ expect(button_create_merge_request).to have_text('Create merge request')
+ end
+
+ li_create_branch.click
+
+ page.within(li_create_branch) do
+ expect(page).to have_css('i.fa.fa-check')
+ expect(button_create_target).to have_text('Create branch')
+ expect(button_create_merge_request).to have_text('Create branch')
+ end
+ end
+
+ def test_source_checking(input_source)
+ expect(input_source.value).to eq(project.default_branch)
+
+ input_source.set('mas') # Intentionally entered first 3 letters of `master` to check autocomplete feature later.
+ source_message = find('.js-ref-message')
+
+ expect(source_message).to have_text('Checking source availability…')
+
+ wait_for_requests
+
+ expect(source_message).to have_text('Source is not available')
+
+ # JavaScript gets refs started with `mas` (entered above) and places the first match.
+ # User sees `mas` in black color (the part he entered) and the `ter` in gray color (a hint).
+ # Since hinting is implemented via text selection and rspec/capybara doesn't have matchers for it,
+ # we just checking the whole source name.
+ expect(input_source.value).to eq(project.default_branch)
+ end
+end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 7437c469a72..c4c06ed514b 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issues > User uses quick actions', js: true do
+feature 'Issues > User uses quick actions', :js do
include QuickActionsHelpers
it_behaves_like 'issuable record that supports quick actions in its description and notes', :issue do
@@ -226,7 +226,7 @@ feature 'Issues > User uses quick actions', js: true do
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}")
+ write_note("/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"\n\n/move #{target_project.full_path}")
expect(page).to have_content 'Commands applied'
expect(issue.reload).to be_closed
@@ -245,7 +245,7 @@ feature 'Issues > User uses quick actions', js: true do
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}\"")
+ write_note("/move #{target_project.full_path}\n\n/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"")
expect(page).to have_content 'Commands applied'
expect(issue.reload).to be_closed
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index e2db1442d90..b9af77f918a 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Issues', :js do
+describe 'Issues' do
include DropzoneHelper
include IssueHelpers
include SortingHelper
@@ -24,15 +24,109 @@ describe 'Issues', :js do
end
before do
- visit project_issue_path(project, issue)
- page.within('.content .issuable-actions') do
- find('.issuable-edit').click
- end
- find('.issue-details .content-block .js-zen-enter').click
+ visit edit_project_issue_path(project, issue)
+ find('.js-zen-enter').click
end
it 'opens new issue popup' do
- expect(page).to have_content(issue.description)
+ expect(page).to have_content("Issue ##{issue.iid}")
+ end
+ end
+
+ describe 'Editing issue assignee' do
+ let!(:issue) do
+ create(:issue,
+ author: user,
+ assignees: [user],
+ project: project)
+ end
+
+ it 'allows user to select unassigned', :js do
+ visit edit_project_issue_path(project, issue)
+
+ expect(page).to have_content "Assignee #{user.name}"
+
+ first('.js-user-search').click
+ click_link 'Unassigned'
+
+ click_button 'Save changes'
+
+ page.within('.assignee') do
+ expect(page).to have_content 'No assignee - assign yourself'
+ end
+
+ expect(issue.reload.assignees).to be_empty
+ end
+ end
+
+ describe 'due date', :js do
+ context 'on new form' do
+ before do
+ visit new_project_issue_path(project)
+ end
+
+ it 'saves with due date' do
+ date = Date.today.at_beginning_of_month
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+ find('#issuable-due-date').click
+
+ page.within '.pika-single' do
+ click_button date.day
+ end
+
+ expect(find('#issuable-due-date').value).to eq date.to_s
+
+ click_button 'Submit issue'
+
+ page.within '.issuable-sidebar' do
+ expect(page).to have_content date.to_s(:medium)
+ end
+ end
+ end
+
+ context 'on edit form' do
+ let(:issue) { create(:issue, author: user, project: project, due_date: Date.today.at_beginning_of_month.to_s) }
+
+ before do
+ visit edit_project_issue_path(project, issue)
+ end
+
+ it 'saves with due date' do
+ date = Date.today.at_beginning_of_month
+
+ expect(find('#issuable-due-date').value).to eq date.to_s
+
+ date = date.tomorrow
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+ find('#issuable-due-date').click
+
+ page.within '.pika-single' do
+ click_button date.day
+ end
+
+ expect(find('#issuable-due-date').value).to eq date.to_s
+
+ click_button 'Save changes'
+
+ page.within '.issuable-sidebar' do
+ expect(page).to have_content date.to_s(:medium)
+ end
+ end
+
+ it 'warns about version conflict' do
+ issue.update(title: "New title")
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Someone edited the issue the same time you did'
+ end
end
end
@@ -270,10 +364,10 @@ describe 'Issues', :js do
visit namespace_project_issues_path(user.namespace, project1)
end
- it 'changes incoming email address token', js: true do
+ it 'changes incoming email address token', :js do
find('.issue-email-modal-btn').click
previous_token = find('input#issue_email').value
- find('.incoming-email-token-reset').trigger('click')
+ find('.incoming-email-token-reset').click
wait_for_requests
@@ -286,7 +380,7 @@ describe 'Issues', :js do
end
end
- describe 'update labels from issue#show', js: true do
+ describe 'update labels from issue#show', :js do
let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
let!(:label) { create(:label, project: project) }
@@ -309,7 +403,7 @@ describe 'Issues', :js do
let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
context 'by authorized user' do
- it 'allows user to select unassigned', js: true do
+ it 'allows user to select unassigned', :js do
visit project_issue_path(project, issue)
page.within('.assignee') do
@@ -327,7 +421,7 @@ describe 'Issues', :js do
expect(issue.reload.assignees).to be_empty
end
- it 'allows user to select an assignee', js: true do
+ it 'allows user to select an assignee', :js do
issue2 = create(:issue, project: project, author: user)
visit project_issue_path(project, issue2)
@@ -348,7 +442,7 @@ describe 'Issues', :js do
end
end
- it 'allows user to unselect themselves', js: true do
+ it 'allows user to unselect themselves', :js do
issue2 = create(:issue, project: project, author: user)
visit project_issue_path(project, issue2)
@@ -377,7 +471,7 @@ describe 'Issues', :js do
project.team << [[guest], :guest]
end
- it 'shows assignee text', js: true do
+ it 'shows assignee text', :js do
sign_out(:user)
sign_in(guest)
@@ -392,7 +486,7 @@ describe 'Issues', :js do
let!(:milestone) { create(:milestone, project: project) }
context 'by authorized user' do
- it 'allows user to select unassigned', js: true do
+ it 'allows user to select unassigned', :js do
visit project_issue_path(project, issue)
page.within('.milestone') do
@@ -410,7 +504,7 @@ describe 'Issues', :js do
expect(issue.reload.milestone).to be_nil
end
- it 'allows user to de-select milestone', js: true do
+ it 'allows user to de-select milestone', :js do
visit project_issue_path(project, issue)
page.within('.milestone') do
@@ -440,7 +534,7 @@ describe 'Issues', :js do
issue.save
end
- it 'shows milestone text', js: true do
+ it 'shows milestone text', :js do
sign_out(:user)
sign_in(guest)
@@ -473,7 +567,7 @@ describe 'Issues', :js do
end
end
- context 'dropzone upload file', js: true do
+ context 'dropzone upload file', :js do
before do
visit new_project_issue_path(project)
end
@@ -489,6 +583,18 @@ describe 'Issues', :js do
expect(page.find_field("issue_description").value).not_to match /\n\n$/
end
+
+ it "cancels a file upload correctly" do
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+
+ click_button 'Cancel'
+ end
+
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_button('Cancel')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
end
context 'form filled by URL parameters' do
@@ -544,7 +650,7 @@ describe 'Issues', :js do
end
describe 'due date' do
- context 'update due on issue#show', js: true do
+ context 'update due on issue#show', :js do
let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
before do
@@ -588,8 +694,8 @@ describe 'Issues', :js do
end
end
- describe 'title issue#show', js: true do
- it 'updates the title', js: true do
+ describe 'title issue#show', :js do
+ it 'updates the title', :js do
issue = create(:issue, author: user, assignees: [user], project: project, title: 'new title')
visit project_issue_path(project, issue)
@@ -603,7 +709,7 @@ describe 'Issues', :js do
end
end
- describe 'confidential issue#show', js: true do
+ describe 'confidential issue#show', :js do
it 'shows confidential sibebar information as confidential and can be turned off' do
issue = create(:issue, :confidential, project: project)
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index c9983f0941f..6dfabcc7225 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -197,7 +197,7 @@ feature 'Login' do
expect(page).to have_content('The global settings require you to enable Two-Factor Authentication for your account. You need to do this before ')
end
- it 'allows skipping two-factor configuration', js: true do
+ it 'allows skipping two-factor configuration', :js do
expect(current_path).to eq profile_two_factor_auth_path
click_link 'Configure it later'
@@ -215,7 +215,7 @@ feature 'Login' do
)
end
- it 'disallows skipping two-factor configuration', js: true do
+ it 'disallows skipping two-factor configuration', :js do
expect(current_path).to eq profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
@@ -260,7 +260,7 @@ feature 'Login' do
'before ')
end
- it 'allows skipping two-factor configuration', js: true do
+ it 'allows skipping two-factor configuration', :js do
expect(current_path).to eq profile_two_factor_auth_path
click_link 'Configure it later'
@@ -279,7 +279,7 @@ feature 'Login' do
)
end
- it 'disallows skipping two-factor configuration', js: true do
+ it 'disallows skipping two-factor configuration', :js do
expect(current_path).to eq profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
diff --git a/spec/features/logout_spec.rb b/spec/features/logout_spec.rb
new file mode 100644
index 00000000000..635729efa53
--- /dev/null
+++ b/spec/features/logout_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe 'Logout/Sign out', :js do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ visit root_path
+ end
+
+ it 'sign out redirects to sign in page' do
+ gitlab_sign_out
+
+ expect(current_path).to eq new_user_session_path
+ end
+
+ it 'sign out does not show signed out flash notice' do
+ gitlab_sign_out
+
+ expect(page).not_to have_selector('.flash-notice')
+ end
+end
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index b70d3060f05..cc1b187ff54 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -69,6 +69,12 @@ describe 'GitLab Markdown' do
end
end
+ it 'parses mermaid code block' do
+ aggregate_failures do
+ expect(doc).to have_selector('pre.code.js-render-mermaid')
+ end
+ end
+
it 'parses strikethroughs' do
expect(doc).to have_selector(%{del:contains("and this text doesn't")})
end
diff --git a/spec/features/merge_requests/assign_issues_spec.rb b/spec/features/merge_requests/assign_issues_spec.rb
index 63fa72650ac..d49d145f254 100644
--- a/spec/features/merge_requests/assign_issues_spec.rb
+++ b/spec/features/merge_requests/assign_issues_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Merge request issue assignment', js: true do
+feature 'Merge request issue assignment', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue1) { create(:issue, project: project) }
diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb
index e886309133d..a24464f2556 100644
--- a/spec/features/merge_requests/award_spec.rb
+++ b/spec/features/merge_requests/award_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Merge request awards', js: true do
+feature 'Merge request awards', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
index 1f5e7b55fb0..fbbfe7942be 100644
--- a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
+++ b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Check if mergeable with unresolved discussions', js: true do
+feature 'Check if mergeable with unresolved discussions', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let!(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
diff --git a/spec/features/merge_requests/cherry_pick_spec.rb b/spec/features/merge_requests/cherry_pick_spec.rb
index 4b1e1b9a8d4..48f370c3ad4 100644
--- a/spec/features/merge_requests/cherry_pick_spec.rb
+++ b/spec/features/merge_requests/cherry_pick_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Cherry-pick Merge Requests', js: true do
+describe 'Cherry-pick Merge Requests', :js do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, namespace: group) }
diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb
index 299b4f5708a..4dd4e40f52c 100644
--- a/spec/features/merge_requests/closes_issues_spec.rb
+++ b/spec/features/merge_requests/closes_issues_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Merge Request closing issues message', js: true do
+feature 'Merge Request closing issues message', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue_1) { create(:issue, project: project)}
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 2d2c674f8fb..4e2963c116d 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Merge request conflict resolution', js: true do
+feature 'Merge request conflict resolution', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -23,11 +23,11 @@ feature 'Merge request conflict resolution', js: true do
within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
all('button', text: 'Use ours').each do |button|
- button.trigger('click')
+ button.send_keys(:return)
end
end
- click_button 'Commit conflict resolution'
+ find_button('Commit conflict resolution').send_keys(:return)
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
@@ -60,16 +60,18 @@ feature 'Merge request conflict resolution', js: true do
within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do
click_button 'Edit inline'
wait_for_requests
+ find('.files-wrapper .diff-file pre')
execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("One morning");')
end
within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do
click_button 'Edit inline'
wait_for_requests
+ find('.files-wrapper .diff-file pre')
execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");')
end
- click_button 'Commit conflict resolution'
+ find_button('Commit conflict resolution').send_keys(:return)
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
@@ -139,6 +141,7 @@ feature 'Merge request conflict resolution', js: true do
it 'conflicts are resolved in Edit inline mode' do
within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do
wait_for_requests
+ find('.files-wrapper .diff-file pre')
execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("Gregor Samsa woke from troubled dreams");')
end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index 96e8027a54d..db5ce2d11a8 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Create New Merge Request', js: true do
+feature 'Create New Merge Request', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
@@ -67,6 +67,28 @@ feature 'Create New Merge Request', js: true do
expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
end
+ it 'allows filtering multiple dropdowns' do
+ visit project_new_merge_request_path(project)
+
+ first('.js-source-branch').click
+
+ input = find('.dropdown-source-branch .dropdown-input-field')
+ input.click
+ input.send_keys('orphaned-branch')
+
+ find('.dropdown-source-branch .dropdown-content li', match: :first)
+ source_items = all('.dropdown-source-branch .dropdown-content li')
+
+ expect(source_items.count).to eq(1)
+
+ first('.js-target-branch').click
+
+ find('.dropdown-target-branch .dropdown-content li', match: :first)
+ target_items = all('.dropdown-target-branch .dropdown-content li')
+
+ expect(target_items.count).to be > 1
+ end
+
context 'when target project cannot be viewed by the current user' do
it 'does not leak the private project name & namespace' do
private_project = create(:project, :private, :repository)
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index f9bc3ee6c58..ca2225318cd 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -34,7 +34,7 @@ feature 'Merge request created from fork' do
commit_id: merge_request.commit_shas.first)
end
- scenario 'user can reply to the comment', js: true do
+ scenario 'user can reply to the comment', :js do
visit_merge_request(merge_request)
expect(page).to have_content(comment)
@@ -57,7 +57,7 @@ feature 'Merge request created from fork' do
forked_project.destroy!
end
- scenario 'user can access merge request', js: true do
+ scenario 'user can access merge request', :js do
visit_merge_request(merge_request)
expect(page).to have_content 'Test merge request'
@@ -78,12 +78,11 @@ feature 'Merge request created from fork' do
create(:ci_build, pipeline: pipeline, name: 'spinach')
end
- scenario 'user visits a pipelines page', js: true do
+ scenario 'user visits a pipelines page', :js do
visit_merge_request(merge_request)
page.within('.merge-request-tabs') { click_link 'Pipelines' }
page.within('.ci-table') do
- expect(page).to have_content pipeline.status
expect(page).to have_content pipeline.id
end
end
diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb
index 874c6e2ff69..7f69e82af4c 100644
--- a/spec/features/merge_requests/deleted_source_branch_spec.rb
+++ b/spec/features/merge_requests/deleted_source_branch_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
# This test serves as a regression test for a bug that caused an error
# message to be shown by JavaScript when the source branch was deleted.
# Please do not remove "js: true".
-describe 'Deleted source branch', js: true do
+describe 'Deleted source branch', :js do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
index 4766cdf716f..9e816cf041b 100644
--- a/spec/features/merge_requests/diff_notes_avatars_spec.rb
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Diff note avatars', js: true do
+feature 'Diff note avatars', :js do
include NoteInteractionHelpers
let(:user) { create(:user) }
@@ -22,7 +22,7 @@ feature 'Diff note avatars', js: true do
project.team << [user, :master]
sign_in user
- page.driver.set_cookie('sidebar_collapsed', 'true')
+ set_cookie('sidebar_collapsed', 'true')
end
context 'discussion tab' do
@@ -56,7 +56,7 @@ feature 'Diff note avatars', js: true do
end
it 'does not render avatar after commenting' do
- first('.diff-line-num').trigger('mouseover')
+ first('.diff-line-num').click
find('.js-add-diff-note-button').click
page.within('.js-discussion-note-form') do
@@ -85,7 +85,7 @@ feature 'Diff note avatars', js: true do
it 'shows note avatar' do
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').send_keys(:return)
expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
end
@@ -93,7 +93,7 @@ feature 'Diff note avatars', js: true do
it 'shows comment on note avatar' do
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').send_keys(:return)
expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
end
@@ -101,7 +101,7 @@ feature 'Diff note avatars', js: true do
it 'toggles comments when clicking avatar' do
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').send_keys(:return)
end
expect(page).to have_selector('.notes_holder', visible: false)
@@ -117,7 +117,7 @@ feature 'Diff note avatars', js: true do
open_more_actions_dropdown(note)
page.within find(".note-row-#{note.id}") do
- find('.js-note-delete').click
+ accept_confirm { find('.js-note-delete').click }
end
wait_for_requests
@@ -139,7 +139,7 @@ feature 'Diff note avatars', js: true do
end
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').trigger('click')
+ find('.diff-notes-collapse').send_keys(:return)
expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
end
@@ -152,14 +152,14 @@ feature 'Diff note avatars', js: true do
page.within '.js-discussion-note-form' do
find('.js-note-text').native.send_keys('Test')
- find('.js-comment-button').trigger('click')
+ find('.js-comment-button').click
wait_for_requests
end
end
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').trigger('click')
+ find('.diff-notes-collapse').send_keys(:return)
expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
expect(find('.diff-comments-more-count')).to have_content '+1'
@@ -177,7 +177,7 @@ feature 'Diff note avatars', js: true do
it 'shows extra comment count' do
page.within find_line(position.line_code(project.repository)) do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').send_keys(:return)
expect(find('.diff-comments-more-count')).to have_content '+1'
end
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index fd110e68e84..15d380b1bf4 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Diff notes resolve', js: true do
+feature 'Diff notes resolve', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
@@ -88,14 +88,43 @@ feature 'Diff notes resolve', js: true do
end
end
- it 'hides resolved discussion' do
- page.within '.diff-content' do
- click_button 'Resolve discussion'
+ describe 'resolved discussion' do
+ before do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ end
+
+ visit_merge_request
end
- visit_merge_request
+ describe 'timeline view' do
+ it 'hides when resolve discussion is clicked' do
+ expect(page).to have_selector('.discussion-body', visible: false)
+ end
+
+ it 'shows resolved discussion when toggled' do
+ find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click
+
+ expect(page.find(".timeline-content #note_#{note.noteable_id}")).to be_visible
+ end
+ end
+
+ describe 'side-by-side view' do
+ before do
+ page.within('.merge-request-tabs') { click_link 'Changes' }
+ page.find('#parallel-diff-btn').click
+ end
+
+ it 'hides when resolve discussion is clicked' do
+ expect(page).to have_selector('.diffs .diff-file .notes_holder', visible: false)
+ end
- expect(page).to have_selector('.discussion-body', visible: false)
+ it 'shows resolved discussion when toggled' do
+ find('.diff-comment-avatar-holders').click
+
+ expect(find('.diffs .diff-file .notes_holder')).to be_visible
+ end
+ end
end
it 'allows user to resolve from reply form without a comment' do
@@ -163,7 +192,7 @@ feature 'Diff notes resolve', js: true do
page.find('.discussion-next-btn').click
end
- expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
+ expect(page.evaluate_script("window.pageYOffset")).to be > 0
end
it 'hides jump to next button when all resolved' do
@@ -212,10 +241,8 @@ feature 'Diff notes resolve', js: true do
end
it 'resolves discussion' do
- page.all('.note').each do |note|
- note.all('.line-resolve-btn').each do |button|
- button.click
- end
+ page.all('.note .line-resolve-btn').each do |button|
+ button.click
end
expect(page).to have_content('Resolved by')
@@ -276,10 +303,10 @@ feature 'Diff notes resolve', js: true do
end
page.within '.line-resolve-all-container' do
- page.find('.discussion-next-btn').trigger('click')
+ page.find('.discussion-next-btn').click
end
- expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
+ expect(page.evaluate_script("window.pageYOffset")).to be > 0
end
it 'updates updated text after resolving note' do
diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
index ee9bb50a881..1bf77296ae6 100644
--- a/spec/features/merge_requests/diffs_spec.rb
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -1,20 +1,18 @@
require 'spec_helper'
-feature 'Diffs URL', js: true do
+feature 'Diffs URL', :js do
include ProjectForksHelper
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
context 'when visit with */* as accept header' do
- before do
- page.driver.add_header('Accept', '*/*')
- end
-
it 'renders the notes' do
create :note_on_merge_request, project: project, noteable: merge_request, note: 'Rebasing with master'
- visit diffs_project_merge_request_path(project, merge_request)
+ inspect_requests(inject_headers: { 'Accept' => '*/*' }) do
+ visit diffs_project_merge_request_path(project, merge_request)
+ end
# Load notes and diff through AJAX
expect(page).to have_css('.note-text', visible: false, text: 'Rebasing with master')
@@ -44,12 +42,8 @@ feature 'Diffs URL', js: true do
visit "#{diffs_project_merge_request_path(project, merge_request)}#{fragment}"
end
- it 'shows collapsed note' do
- wait_for_requests
-
- expect(page).to have_selector('.discussion-notes.collapsed') do |note_container|
- expect(note_container).to have_selector(fragment, visible: false)
- end
+ it 'shows expanded note' do
+ expect(page).to have_selector(fragment, visible: true)
end
end
end
@@ -94,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").trigger('click')
+ find("[id=\"#{changelog_id}\"] .js-edit-blob").click
expect(page).to have_selector('.js-fork-suggestion-button', count: 1)
expect(page).to have_selector('.js-cancel-fork-suggestion-button', count: 1)
diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb
index 7386e78fb13..4362f8b3fcc 100644
--- a/spec/features/merge_requests/edit_mr_spec.rb
+++ b/spec/features/merge_requests/edit_mr_spec.rb
@@ -29,7 +29,7 @@ feature 'Edit Merge Request' do
expect(page).to have_content 'Someone edited the merge request the same time you did'
end
- it 'allows to unselect "Remove source branch"', js: true do
+ it 'allows to unselect "Remove source branch"', :js do
merge_request.update(merge_params: { 'force_remove_source_branch' => '1' })
expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy
@@ -42,7 +42,7 @@ feature 'Edit Merge Request' do
expect(page).to have_content 'Remove source branch'
end
- it 'should preserve description textarea height', js: true do
+ it 'should preserve description textarea height', :js do
long_description = %q(
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ac ornare ligula, ut tempus arcu. Etiam ultricies accumsan dolor vitae faucibus. Donec at elit lacus. Mauris orci ante, aliquam quis lorem eget, convallis faucibus arcu. Aenean at pulvinar lacus. Ut viverra quam massa, molestie ornare tortor dignissim a. Suspendisse tristique pellentesque tellus, id lacinia metus elementum id. Nam tristique, arcu rhoncus faucibus viverra, lacus ipsum sagittis ligula, vitae convallis odio lacus a nibh. Ut tincidunt est purus, ac vestibulum augue maximus in. Suspendisse vel erat et mi ultricies semper. Pellentesque volutpat pellentesque consequat.
@@ -66,6 +66,7 @@ feature 'Edit Merge Request' do
end
def get_textarea_height
+ find('#merge_request_description')
page.evaluate_script('document.getElementById("merge_request_description").offsetHeight')
end
end
diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb
index 9912e8165e6..7adae08e499 100644
--- a/spec/features/merge_requests/filter_by_labels_spec.rb
+++ b/spec/features/merge_requests/filter_by_labels_spec.rb
@@ -79,22 +79,6 @@ feature 'Merge Request filtering by Labels', :js do
end
end
- context 'clear button' do
- before do
- input_filtered_search('label:~bug')
- end
-
- it 'allows user to remove filtered labels' do
- first('.clear-search').click
- filtered_search.send_keys(:enter)
-
- expect(page).to have_issuable_counts(open: 3, closed: 0, all: 3)
- expect(page).to have_content "Bugfix2"
- expect(page).to have_content "Feature1"
- expect(page).to have_content "Bugfix1"
- end
- end
-
context 'filter dropdown' do
it 'filters by label name' do
init_label_search
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index 166c02a7a7f..8b9ff9be943 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -18,7 +18,7 @@ feature 'Merge Request filtering by Milestone' do
sign_in(user)
end
- scenario 'filters by no Milestone', js: true do
+ scenario 'filters by no Milestone', :js do
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
@@ -32,7 +32,7 @@ feature 'Merge Request filtering by Milestone' do
expect(page).to have_css('.merge-request', count: 1)
end
- context 'filters by upcoming milestone', js: true do
+ context 'filters by upcoming milestone', :js do
it 'does not show merge requests with no expiry' do
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
@@ -67,7 +67,7 @@ feature 'Merge Request filtering by Milestone' do
end
end
- scenario 'filters by a specific Milestone', js: true do
+ scenario 'filters by a specific Milestone', :js do
create(:merge_request, :with_diffs, source_project: project, milestone: milestone)
create(:merge_request, :simple, source_project: project)
@@ -83,7 +83,7 @@ feature 'Merge Request filtering by Milestone' do
milestone.update(name: "rock 'n' roll")
end
- scenario 'filters by a specific Milestone', js: true do
+ scenario 'filters by a specific Milestone', :js do
create(:merge_request, :with_diffs, source_project: project, milestone: milestone)
create(:merge_request, :simple, source_project: project)
diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb
index 16703bc1c01..aac295ab940 100644
--- a/spec/features/merge_requests/filter_merge_requests_spec.rb
+++ b/spec/features/merge_requests/filter_merge_requests_spec.rb
@@ -36,7 +36,7 @@ describe 'Filter merge requests' do
expect_mr_list_count(0)
end
- context 'assignee', js: true do
+ context 'assignee', :js do
it 'updates to current user' do
expect_assignee_visual_tokens()
end
@@ -69,7 +69,7 @@ describe 'Filter merge requests' do
expect_mr_list_count(0)
end
- context 'milestone', js: true do
+ context 'milestone', :js do
it 'updates to current milestone' do
expect_milestone_visual_tokens()
end
@@ -88,7 +88,7 @@ describe 'Filter merge requests' do
end
end
- describe 'for label from mr#index', js: true do
+ describe 'for label from mr#index', :js do
it 'filters by no label' do
input_filtered_search('label:none')
@@ -137,7 +137,7 @@ describe 'Filter merge requests' do
expect_mr_list_count(0)
end
- context 'assignee and label', js: true do
+ context 'assignee and label', :js do
def expect_assignee_label_visual_tokens
wait_for_requests
@@ -183,7 +183,7 @@ describe 'Filter merge requests' do
visit project_merge_requests_path(project)
end
- context 'only text', js: true do
+ context 'only text', :js do
it 'filters merge requests by searched text' do
input_filtered_search('bug')
@@ -199,7 +199,7 @@ describe 'Filter merge requests' do
end
end
- context 'filters and searches', js: true do
+ context 'filters and searches', :js do
it 'filters by text and label' do
input_filtered_search('Bug')
@@ -289,7 +289,7 @@ describe 'Filter merge requests' do
end
end
- describe 'filter by assignee id', js: true do
+ describe 'filter by assignee id', :js do
it 'filter by current user' do
visit project_merge_requests_path(project, assignee_id: user.id)
@@ -312,7 +312,7 @@ describe 'Filter merge requests' do
end
end
- describe 'filter by author id', js: true do
+ describe 'filter by author id', :js do
it 'filter by current user' do
visit project_merge_requests_path(project, author_id: user.id)
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
index 758fc9b139d..1dcc1e139a0 100644
--- a/spec/features/merge_requests/form_spec.rb
+++ b/spec/features/merge_requests/form_spec.rb
@@ -43,7 +43,7 @@ describe 'New/edit merge request', :js do
expect(page).to have_content user2.name
end
- find('a', text: 'Assign to me').trigger('click')
+ find('a', text: 'Assign to me').click
expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
diff --git a/spec/features/merge_requests/image_diff_notes.rb b/spec/features/merge_requests/image_diff_notes.rb
index e8ca8a98d70..3c53b51e330 100644
--- a/spec/features/merge_requests/image_diff_notes.rb
+++ b/spec/features/merge_requests/image_diff_notes.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'image diff notes', js: true do
+feature 'image diff notes', :js do
include NoteInteractionHelpers
let(:user) { create(:user) }
diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
index 08a3bb84aac..82b2b56ef80 100644
--- a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
+++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Clicking toggle commit message link', js: true do
+feature 'Clicking toggle commit message link', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:issue_1) { create(:issue, project: project)}
diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
index dcc70338d7f..bac56270362 100644
--- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
@@ -52,10 +52,12 @@ feature 'Mini Pipeline Graph', :js do
end
it 'should expand when hovered' do
+ find('.mini-pipeline-graph-dropdown-toggle')
before_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();")
toggle.hover
+ find('.mini-pipeline-graph-dropdown-toggle')
after_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();")
expect(before_width).to be < after_width
@@ -90,7 +92,7 @@ feature 'Mini Pipeline Graph', :js do
end
it 'should close when toggle is clicked again' do
- toggle.trigger('click')
+ toggle.click
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
end
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
index 59e67420333..91f207bd339 100644
--- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Only allow merge requests to be merged if the pipeline succeeds', js: true do
+feature 'Only allow merge requests to be merged if the pipeline succeeds', :js do
let(:merge_request) { create(:merge_request_with_diffs) }
let(:project) { merge_request.target_project }
@@ -10,7 +10,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t
project.team << [merge_request.author, :master]
end
- context 'project does not have CI enabled', js: true do
+ context 'project does not have CI enabled', :js do
it 'allows MR to be merged' do
visit_merge_request(merge_request)
@@ -20,7 +20,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t
end
end
- context 'when project has CI enabled', js: true do
+ context 'when project has CI enabled', :js do
given!(:pipeline) do
create(:ci_empty_pipeline,
project: project,
diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb
index 347ce788b36..a3fcc27cab0 100644
--- a/spec/features/merge_requests/pipelines_spec.rb
+++ b/spec/features/merge_requests/pipelines_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Pipelines for Merge Requests', js: true do
+feature 'Pipelines for Merge Requests', :js do
describe 'pipeline tab' do
given(:user) { create(:user) }
given(:merge_request) { create(:merge_request) }
diff --git a/spec/features/merge_requests/resolve_outdated_diff_discussions.rb b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb
index 55a82bdf2b9..25abbb469ab 100644
--- a/spec/features/merge_requests/resolve_outdated_diff_discussions.rb
+++ b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Resolve outdated diff discussions', js: true do
+feature 'Resolve outdated diff discussions', :js do
let(:project) { create(:project, :repository, :public) }
let(:merge_request) do
diff --git a/spec/features/merge_requests/target_branch_spec.rb b/spec/features/merge_requests/target_branch_spec.rb
index 9bbf2610bcb..bce36e05e57 100644
--- a/spec/features/merge_requests/target_branch_spec.rb
+++ b/spec/features/merge_requests/target_branch_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Target branch', js: true do
+describe 'Target branch', :js do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
diff --git a/spec/features/merge_requests/toggle_whitespace_changes_spec.rb b/spec/features/merge_requests/toggle_whitespace_changes_spec.rb
index dd989fd49b2..fa3d988b27a 100644
--- a/spec/features/merge_requests/toggle_whitespace_changes_spec.rb
+++ b/spec/features/merge_requests/toggle_whitespace_changes_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Toggle Whitespace Changes', js: true do
+feature 'Toggle Whitespace Changes', :js do
before do
sign_in(create(:admin))
merge_request = create(:merge_request)
diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb
index 4e5ec9fbd2d..cd92ad22267 100644
--- a/spec/features/merge_requests/toggler_behavior_spec.rb
+++ b/spec/features/merge_requests/toggler_behavior_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'toggler_behavior', js: true do
+feature 'toggler_behavior', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, author: user) }
diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb
index 9cb8a357309..c5498563b39 100644
--- a/spec/features/merge_requests/update_merge_requests_spec.rb
+++ b/spec/features/merge_requests/update_merge_requests_spec.rb
@@ -10,7 +10,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
sign_in(user)
end
- context 'status', js: true do
+ context 'status', :js do
describe 'close merge request' do
before do
visit project_merge_requests_path(project)
@@ -37,7 +37,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
end
end
- context 'assignee', js: true do
+ context 'assignee', :js do
describe 'set assignee' do
before do
visit project_merge_requests_path(project)
@@ -67,7 +67,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
end
end
- context 'milestone', js: true do
+ context 'milestone', :js do
let(:milestone) { create(:milestone, project: project) }
describe 'set milestone' do
@@ -127,7 +127,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
end
def click_update_merge_requests_button
- find('.update-selected-issues').trigger('click')
+ find('.update-selected-issues').click
wait_for_requests
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 7a773fb2baa..d44eb23d7f4 100644
--- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
@@ -8,7 +8,7 @@ feature 'Merge requests > User posts diff notes', :js do
let(:project) { merge_request.source_project }
before do
- page.driver.set_cookie('sidebar_collapsed', 'true')
+ set_cookie('sidebar_collapsed', 'true')
project.add_developer(user)
sign_in(user)
@@ -103,7 +103,10 @@ feature 'Merge requests > User posts diff notes', :js do
it 'allows commenting' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
- first('.js-note-delete', visible: false).trigger('click')
+ accept_confirm do
+ first('button.more-actions-toggle').click
+ first('.js-note-delete').click
+ end
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
end
@@ -236,7 +239,7 @@ feature 'Merge requests > User posts diff notes', :js do
def should_allow_dismissing_a_comment(line_holder, diff_side = nil)
write_comment_on_line(line_holder, diff_side)
- find('.js-close-discussion-note-form').trigger('click')
+ find('.js-close-discussion-note-form').click
assert_comment_dismissal(line_holder)
end
diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb
index d7cda73ab40..f4c75a2f265 100644
--- a/spec/features/merge_requests/user_posts_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_notes_spec.rb
@@ -141,7 +141,7 @@ describe 'Merge requests > User posts notes', :js do
end
it 'removes the attachment div and resets the edit form' do
- find('.js-note-attachment-delete').click
+ accept_confirm { find('.js-note-attachment-delete').click }
is_expected.not_to have_css('.note-attachment')
is_expected.not_to have_css('.current-note-edit-form')
wait_for_requests
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index 95c50df1896..ee0766f1192 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Merge Requests > User uses quick actions', js: true do
+feature 'Merge Requests > User uses quick actions', :js do
include QuickActionsHelpers
it_behaves_like 'issuable record that supports quick actions in its description and notes', :merge_request do
diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb
index 8e231fbc281..29f95039af8 100644
--- a/spec/features/merge_requests/versions_spec.rb
+++ b/spec/features/merge_requests/versions_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Merge Request versions', js: true do
+feature 'Merge Request versions', :js do
let(:merge_request) { create(:merge_request, importing: true) }
let(:project) { merge_request.source_project }
let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
@@ -67,8 +67,8 @@ feature 'Merge Request versions', js: true do
line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_2_2'
page.within(diff_file_selector) do
- find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").trigger 'mouseover'
- find(".line_holder[id='#{line_code}'] button").trigger 'click'
+ find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover
+ find(".line_holder[id='#{line_code}'] button").click
page.within("form[data-line-code='#{line_code}']") do
fill_in "note[note]", with: "Typo, please fix"
@@ -137,8 +137,8 @@ feature 'Merge Request versions', js: true do
line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_4'
page.within(diff_file_selector) do
- find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").trigger 'mouseover'
- find(".line_holder[id='#{line_code}'] button").trigger 'click'
+ find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover
+ find(".line_holder[id='#{line_code}'] button").click
page.within("form[data-line-code='#{line_code}']") do
fill_in "note[note]", with: "Typo, please fix"
diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb
index c0221525c9f..72a52c979b3 100644
--- a/spec/features/merge_requests/widget_deployments_spec.rb
+++ b/spec/features/merge_requests/widget_deployments_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Widget Deployments Header', js: true do
+feature 'Widget Deployments Header', :js do
describe 'when deployed to an environment' do
given(:user) { create(:user) }
given(:project) { merge_request.target_project }
@@ -42,7 +42,7 @@ feature 'Widget Deployments Header', js: true do
end
scenario 'does start build when stop button clicked' do
- click_button('Stop environment')
+ accept_confirm { click_button('Stop environment') }
expect(page).to have_content('close_app')
end
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index ab1353e3369..2bad3b02250 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -256,7 +256,7 @@ describe 'Merge request', :js do
end
end
- context 'user can merge into source project but cannot push to fork', js: true do
+ context 'user can merge into source project but cannot push to fork', :js do
let(:fork_project) { create(:project, :public, :repository) }
let(:user2) { create(:user) }
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index 6c9dc67ad74..27efc32c95b 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -65,4 +65,33 @@ feature 'Milestone' do
expect(find('.alert-danger')).to have_content('already being used for another group or project milestone.')
end
end
+
+ feature 'Open a milestone' do
+ scenario 'shows total issue time spent correctly when no time has been logged' do
+ milestone = create(:milestone, project: project, title: 8.7)
+
+ visit project_milestone_path(project, milestone)
+
+ page.within('.block.time_spent') do
+ expect(page).to have_content 'No time spent'
+ expect(page).to have_content 'None'
+ end
+ end
+
+ scenario 'shows total issue time spent' do
+ milestone = create(:milestone, project: project, title: 8.7)
+ issue1 = create(:issue, project: project, milestone: milestone)
+ issue2 = create(:issue, project: project, milestone: milestone)
+ issue1.spend_time(duration: 3600, user: user)
+ issue1.save!
+ issue2.spend_time(duration: 7200, user: user)
+ issue2.save!
+
+ visit project_milestone_path(project, milestone)
+
+ page.within('.block.time_spent') do
+ expect(page).to have_content '3h'
+ end
+ end
+ end
end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 1cddd35fd8a..c60883911f7 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Profile account page' do
+describe 'Profile account page', :js do
let(:user) { create(:user) }
before do
@@ -56,47 +56,38 @@ describe 'Profile account page' do
end
end
- describe 'when I reset private token' do
- before do
- visit profile_account_path
- end
-
- it 'resets private token' do
- previous_token = find("#private-token").value
-
- click_link('Reset private token')
-
- expect(find('#private-token').value).not_to eq(previous_token)
- end
- end
-
describe 'when I reset RSS token' do
before do
- visit profile_account_path
+ visit profile_personal_access_tokens_path
end
it 'resets RSS token' do
- previous_token = find("#rss-token").value
+ within('.rss-token-reset') do
+ previous_token = find("#rss_token").value
- click_link('Reset RSS token')
+ accept_confirm { click_link('reset it') }
+
+ expect(find('#rss_token').value).not_to eq(previous_token)
+ end
expect(page).to have_content 'RSS token was successfully reset'
- expect(find('#rss-token').value).not_to eq(previous_token)
end
end
describe 'when I reset incoming email token' do
before do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
- visit profile_account_path
+ visit profile_personal_access_tokens_path
end
it 'resets incoming email token' do
- previous_token = find('#incoming-email-token').value
+ within('.incoming-email-token-reset') do
+ previous_token = find('#incoming_email_token').value
- click_link('Reset incoming email token')
+ accept_confirm { click_link('reset it') }
- expect(find('#incoming-email-token').value).not_to eq(previous_token)
+ expect(find('#incoming_email_token').value).not_to eq(previous_token)
+ end
end
end
diff --git a/spec/features/profiles/chat_names_spec.rb b/spec/features/profiles/chat_names_spec.rb
index 35793539e0e..5c959acbbc9 100644
--- a/spec/features/profiles/chat_names_spec.rb
+++ b/spec/features/profiles/chat_names_spec.rb
@@ -33,7 +33,7 @@ feature 'Profile > Chat' do
scenario 'second use of link is denied' do
visit authorize_path
- expect(page).to have_http_status(:not_found)
+ expect(page).to have_gitlab_http_status(:not_found)
end
end
@@ -51,7 +51,7 @@ feature 'Profile > Chat' do
scenario 'second use of link is denied' do
visit authorize_path
- expect(page).to have_http_status(:not_found)
+ expect(page).to have_gitlab_http_status(:not_found)
end
end
end
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index aa71c4dbba4..7d5ba3a7328 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -12,7 +12,7 @@ feature 'Profile > SSH Keys' do
visit profile_keys_path
end
- scenario 'auto-populates the title', js: true do
+ scenario 'auto-populates the title', :js do
fill_in('Key', with: attributes_for(:key).fetch(:key))
expect(page).to have_field("Title", with: "dummy@gitlab.com")
diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb
index 45f78444362..d1edeef8da4 100644
--- a/spec/features/profiles/oauth_applications_spec.rb
+++ b/spec/features/profiles/oauth_applications_spec.rb
@@ -7,14 +7,14 @@ describe 'Profile > Applications' do
sign_in(user)
end
- describe 'User manages applications', js: true do
+ describe 'User manages applications', :js do
it 'deletes an application' do
create(:oauth_application, owner: user)
visit oauth_applications_path
page.within('.oauth-applications') do
expect(page).to have_content('Your applications (1)')
- click_button 'Destroy'
+ accept_confirm { click_button 'Destroy' }
end
expect(page).to have_content('The application was deleted successfully')
@@ -28,7 +28,7 @@ describe 'Profile > Applications' do
page.within('.oauth-authorized-applications') do
expect(page).to have_content('Authorized applications (1)')
- click_button 'Revoke'
+ accept_confirm { click_button 'Revoke' }
end
expect(page).to have_content('The application was revoked access.')
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
index 225d4c16841..4665626f114 100644
--- a/spec/features/profiles/password_spec.rb
+++ b/spec/features/profiles/password_spec.rb
@@ -53,12 +53,13 @@ describe 'Profile > Password' do
context 'Regular user' do
let(:user) { create(:user) }
- it 'renders 200 when sign-in is disabled' do
- stub_application_setting(password_authentication_enabled: false)
+ it 'renders 404 when password authentication is disabled for the web interface and Git' do
+ stub_application_setting(password_authentication_enabled_for_web: false)
+ stub_application_setting(password_authentication_enabled_for_git: false)
visit edit_profile_password_path
- expect(page).to have_http_status(200)
+ expect(page).to have_gitlab_http_status(404)
end
end
@@ -68,7 +69,7 @@ describe 'Profile > Password' do
it 'renders 404' do
visit edit_profile_password_path
- expect(page).to have_http_status(404)
+ expect(page).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index f3124bbf29e..8461cd0027c 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Profile > Personal Access Tokens', js: true do
+describe 'Profile > Personal Access Tokens', :js do
let(:user) { create(:user) }
def active_personal_access_tokens
@@ -34,7 +34,7 @@ describe 'Profile > Personal Access Tokens', js: true do
fill_in "Name", with: name
# Set date to 1st of next month
- find_field("Expires at").trigger('focus')
+ find_field("Expires at").click
find(".pika-next").click
click_on "1"
@@ -78,7 +78,7 @@ describe 'Profile > Personal Access Tokens', js: true do
it "allows revocation of an active token" do
visit profile_personal_access_tokens_path
- click_on "Revoke"
+ accept_confirm { click_on "Revoke" }
expect(page).to have_selector(".settings-message")
expect(no_personal_access_tokens_message).to have_text("This user has no active Personal Access Tokens.")
@@ -100,7 +100,7 @@ describe 'Profile > Personal Access Tokens', js: true do
errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") }
allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors)
- click_on "Revoke"
+ accept_confirm { click_on "Revoke" }
expect(active_personal_access_tokens).to have_text(personal_access_token.name)
expect(page).to have_content("Could not revoke")
end
diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
index 6a4173d43e1..d5fe5bdffc5 100644
--- a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
+++ b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Profile > Notifications > User changes notified_of_own_activity setting', js: true do
+feature 'Profile > Notifications > User changes notified_of_own_activity setting', :js do
let(:user) { create(:user) }
before do
diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb
index 48c1787c8b7..df89918f17a 100644
--- a/spec/features/profiles/user_visits_notifications_tab_spec.rb
+++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User visits the notifications tab', js: true do
+feature 'User visits the notifications tab', :js do
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -13,7 +13,7 @@ feature 'User visits the notifications tab', js: true do
it 'changes the project notifications setting' do
expect(page).to have_content('Notifications')
- first('#notifications-button').trigger('click')
+ first('#notifications-button').click
click_link('On mention')
expect(page).to have_content('On mention')
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index 924ee0e4174..90d6841af0e 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -53,7 +53,7 @@ describe 'User visits the profile preferences page' do
expect(page).to have_content("You don't have starred projects yet")
expect(page.current_path).to eq starred_dashboard_projects_path
- find('.shortcuts-activity').trigger('click')
+ find('.shortcuts-activity').click
expect(page).not_to have_content("You don't have starred projects yet")
expect(page.current_path).to eq dashboard_projects_path
diff --git a/spec/features/projects/artifacts/download_spec.rb b/spec/features/projects/artifacts/download_spec.rb
index f1bdb2812c6..6f76c14910b 100644
--- a/spec/features/projects/artifacts/download_spec.rb
+++ b/spec/features/projects/artifacts/download_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Download artifact', :js do
+feature 'Download artifact' do
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) }
let(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) }
diff --git a/spec/features/projects/artifacts/file_spec.rb b/spec/features/projects/artifacts/file_spec.rb
index b2be10a7e0c..df1d17bdcb7 100644
--- a/spec/features/projects/artifacts/file_spec.rb
+++ b/spec/features/projects/artifacts/file_spec.rb
@@ -39,7 +39,6 @@ feature 'Artifact file', :js do
context 'JPG file' do
before do
- page.driver.browser.url_blacklist = []
visit_file('rails_sample.jpg')
wait_for_requests
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index 368a046f741..c68e10a2563 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -50,7 +50,7 @@ feature 'test coverage badge' do
scenario 'user requests test coverage badge image' do
show_test_coverage_badge
- expect(page).to have_http_status(404)
+ expect(page).to have_gitlab_http_status(404)
end
end
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 89ae891037e..68c4a647958 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -39,7 +39,7 @@ feature 'list of badges' do
end
end
- scenario 'user changes current ref of build status badge', js: true do
+ scenario 'user changes current ref of build status badge', :js do
page.within('.pipeline-status') do
first('.js-project-refs-dropdown').click
diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
index 1160f674974..c12e56d2c3f 100644
--- a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
+++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', js: true do
+feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js do
include TreeHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 3d465e709b9..88813d9b5ff 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'File blob', :js do
+ include MobileHelpers
+
let(:project) { create(:project, :public, :repository) }
def visit_blob(path, anchor: nil, ref: 'master')
@@ -30,6 +32,16 @@ feature 'File blob', :js do
expect(page).to have_link('Open raw')
end
end
+
+ it 'displays file actions on all screen sizes' do
+ file_actions_selector = '.file-actions'
+
+ resize_screen_sm
+ expect(page).to have_selector(file_actions_selector, visible: true)
+
+ resize_screen_xs
+ expect(page).to have_selector(file_actions_selector, visible: true)
+ end
end
context 'Markdown file' do
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 62ac9fd0e95..965028a6f90 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Editing file blob', js: true do
+feature 'Editing file blob', :js do
include TreeHelper
let(:project) { create(:project, :public, :repository) }
@@ -20,6 +20,7 @@ feature 'Editing file blob', js: true do
def edit_and_commit
wait_for_requests
find('.js-edit-blob').click
+ find('#editor')
execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")')
click_button 'Commit changes'
end
diff --git a/spec/features/projects/blobs/shortcuts_blob_spec.rb b/spec/features/projects/blobs/shortcuts_blob_spec.rb
index 1e3080fa319..9f1fef80ab5 100644
--- a/spec/features/projects/blobs/shortcuts_blob_spec.rb
+++ b/spec/features/projects/blobs/shortcuts_blob_spec.rb
@@ -6,7 +6,7 @@ feature 'Blob shortcuts' do
let(:path) { project.repository.ls_files(project.repository.root_ref)[0] }
let(:sha) { project.repository.commit.sha }
- describe 'On a file(blob)', js: true do
+ describe 'On a file(blob)', :js do
def get_absolute_url(path = "")
"http://#{page.server.host}:#{page.server.port}#{path}"
end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index d1f5623554d..7a77df83034 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -46,7 +46,7 @@ describe 'Branches' do
end
describe 'Find branches' do
- it 'shows filtered branches', js: true do
+ it 'shows filtered branches', :js do
visit project_branches_path(project)
fill_in 'branch-search', with: 'fix'
@@ -58,7 +58,7 @@ describe 'Branches' do
end
describe 'Delete unprotected branch' do
- it 'removes branch after confirmation', js: true do
+ it 'removes branch after confirmation', :js do
visit project_branches_path(project)
fill_in 'branch-search', with: 'fix'
@@ -67,7 +67,7 @@ describe 'Branches' do
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
- find('.js-branch-fix .btn-remove').trigger(:click)
+ accept_confirm { find('.js-branch-fix .btn-remove').click }
expect(page).not_to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 0)
diff --git a/spec/features/projects/clusters/interchangeability_spec.rb b/spec/features/projects/clusters/interchangeability_spec.rb
new file mode 100644
index 00000000000..01f9526608f
--- /dev/null
+++ b/spec/features/projects/clusters/interchangeability_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+feature 'Interchangeability between KubernetesService and Platform::Kubernetes' do
+ EXCEPT_METHODS = %i[test title description help fields initialize_properties namespace namespace= api_url api_url=].freeze
+ EXCEPT_METHODS_GREP_V = %w[_touched? _changed? _was].freeze
+
+ it 'Clusters::Platform::Kubernetes covers core interfaces in KubernetesService' do
+ expected_interfaces = KubernetesService.instance_methods(false)
+ expected_interfaces = expected_interfaces - EXCEPT_METHODS
+ EXCEPT_METHODS_GREP_V.each do |g|
+ expected_interfaces = expected_interfaces.grep_v(/#{Regexp.escape(g)}\z/)
+ end
+
+ expect(expected_interfaces - Clusters::Platforms::Kubernetes.instance_methods).to be_empty
+ end
+end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 810f2c39b43..197e6df4997 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Clusters', :js do
+ include GoogleApi::CloudPlatformHelpers
+
let!(:project) { create(:project, :repository) }
let!(:user) { create(:user) }
@@ -11,13 +13,17 @@ feature 'Clusters', :js do
context 'when user has signed in Google' do
before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:validate_token).and_return(true)
+ allow_any_instance_of(Projects::ClustersController)
+ .to receive(:token_in_session).and_return('token')
+ allow_any_instance_of(Projects::ClustersController)
+ .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
end
context 'when user does not have a cluster and visits cluster index page' do
before do
visit project_clusters_path(project)
+
+ click_link 'Create on GKE'
end
it 'user sees a new page' do
@@ -36,18 +42,32 @@ feature 'Clusters', :js do
allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
- fill_in 'cluster_gcp_project_id', with: 'gcp-project-123'
- fill_in 'cluster_gcp_cluster_name', with: 'dev-cluster'
+ fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
+ fill_in 'cluster_name', with: 'dev-cluster'
click_button 'Create cluster'
end
it 'user sees a cluster details page and creation status' do
expect(page).to have_content('Cluster is being created on Google Container Engine...')
- Gcp::Cluster.last.make_created!
+ # Application Installation buttons
+ page.within('.js-cluster-application-row-helm') do
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button').text).to eq('Install')
+ end
+
+ Clusters::Cluster.last.provider.make_created!
expect(page).to have_content('Cluster was successfully created on Google Container Engine')
end
+
+ it 'user sees a error if something worng during creation' do
+ expect(page).to have_content('Cluster is being created on Google Container Engine...')
+
+ Clusters::Cluster.last.provider.make_errored!('Something wrong!')
+
+ expect(page).to have_content('Something wrong!')
+ end
end
context 'when user filled form with invalid parameters' do
@@ -62,7 +82,8 @@ feature 'Clusters', :js do
end
context 'when user has a cluster and visits cluster index page' do
- let!(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service, project: project) }
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
before do
visit project_clusters_path(project)
@@ -70,7 +91,79 @@ feature 'Clusters', :js do
it 'user sees an cluster details page' do
expect(page).to have_button('Save')
- expect(page.find(:css, '.cluster-name').value).to eq(cluster.gcp_cluster_name)
+ expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
+
+ # Application Installation buttons
+ page.within('.js-cluster-application-row-helm') do
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+ end
+ end
+
+ context 'when user installs application: Helm Tiller' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
+
+ page.within('.js-cluster-application-row-helm') do
+ page.find(:css, '.js-cluster-application-install-button').click
+ end
+ end
+
+ it 'user sees status transition' do
+ page.within('.js-cluster-application-row-helm') do
+ # FE sends request and gets the response, then the buttons is "Install"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+
+ Clusters::Cluster.last.application_helm.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
+
+ Clusters::Cluster.last.application_helm.make_installed!
+
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
+ end
+
+ expect(page).to have_content('Helm Tiller was successfully installed on your cluster')
+ end
+ end
+
+ context 'when user installs application: Ingress' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
+ # Helm Tiller needs to be installed before you can install Ingress
+ create(:cluster_applications_helm, :installed, cluster: cluster)
+
+ visit project_clusters_path(project)
+
+ page.within('.js-cluster-application-row-ingress') do
+ page.find(:css, '.js-cluster-application-install-button').click
+ end
+ end
+
+ it 'user sees status transition' do
+ page.within('.js-cluster-application-row-ingress') do
+ # FE sends request and gets the response, then the buttons is "Install"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+
+ Clusters::Cluster.last.application_ingress.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
+
+ Clusters::Cluster.last.application_ingress.make_installed!
+
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
+ end
+
+ expect(page).to have_content('Ingress was successfully installed on your cluster')
+ end
end
context 'when user disables the cluster' do
@@ -93,7 +186,7 @@ feature 'Clusters', :js do
it 'user sees creation form with the succeccful message' do
expect(page).to have_content('Cluster integration was successfully removed.')
- expect(page).to have_button('Create cluster')
+ expect(page).to have_link('Create on GKE')
end
end
end
@@ -102,6 +195,8 @@ feature 'Clusters', :js do
context 'when user has not signed in Google' do
before do
visit project_clusters_path(project)
+
+ click_link 'Create on GKE'
end
it 'user sees a login page' do
diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb
index 740331fe42a..79e84a4f0a6 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project commit pipelines', js: true do
+feature 'project commit pipelines', :js do
given(:project) { create(:project, :repository) }
background do
@@ -20,7 +20,6 @@ feature 'project commit pipelines', js: true do
visit pipelines_project_commit_path(project, project.commit.sha)
page.within('.table-holder') do
- expect(page).to have_content project.pipelines[0].status # pipeline status
expect(page).to have_content project.pipelines[0].id # pipeline ids
end
end
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 7086f56bb1b..c11a95732b2 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -64,7 +64,7 @@ describe 'Cherry-pick Commits' do
end
end
- context "I cherry-pick a commit from a different branch", js: true do
+ context "I cherry-pick a commit from a different branch", :js do
it do
find('.header-action-buttons a.dropdown-toggle').click
find(:css, "a[href='#modal-cherry-pick-commit']").click
diff --git a/spec/features/projects/commit/diff_notes_spec.rb b/spec/features/projects/commit/diff_notes_spec.rb
index f0fe4e00acc..4dbfc6f6edf 100644
--- a/spec/features/projects/commit/diff_notes_spec.rb
+++ b/spec/features/projects/commit/diff_notes_spec.rb
@@ -20,8 +20,8 @@ feature 'Commit diff', :js do
it "adds comment to diff" do
diff_line_num = first('.diff-line-num.new')
- diff_line_num.trigger('mouseover')
- diff_line_num.find('.js-add-diff-note-button').trigger('click')
+ diff_line_num.hover
+ diff_line_num.find('.js-add-diff-note-button').click
page.within(first('.diff-viewer')) do
find('.js-note-text').set 'test comment'
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index 807a2189cc4..91282063a8d 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -12,6 +12,13 @@ feature 'Mini Pipeline Graph in Commit View', :js do
end
let(:build) { create(:ci_build, pipeline: pipeline) }
+ it 'display icon with status' do
+ build.run
+ visit project_commit_path(project, project.commit.id)
+
+ expect(page).to have_selector('.ci-status-icon-running')
+ end
+
it 'displays a mini pipeline graph' do
build.run
visit project_commit_path(project, project.commit.id)
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 82d73fe8531..87ffc2a0b90 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe "Compare", js: true do
+describe "Compare", :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb
index 2d1a9b931b5..e445758cb5e 100644
--- a/spec/features/projects/deploy_keys_spec.rb
+++ b/spec/features/projects/deploy_keys_spec.rb
@@ -20,7 +20,7 @@ describe 'Project deploy keys', :js do
page.within(find('.deploy-keys')) do
expect(page).to have_selector('.deploy-keys li', count: 1)
- click_on 'Remove'
+ accept_confirm { find(:button, text: 'Remove').send_keys(:return) }
expect(page).not_to have_selector('.fa-spinner', count: 0)
expect(page).to have_selector('.deploy-keys li', count: 0)
diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
index fe8567ce348..36809240f76 100644
--- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb
+++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
@@ -17,7 +17,7 @@ feature 'Developer views empty project instructions' do
expect_instructions_for('http')
end
- scenario 'switches to SSH', js: true do
+ scenario 'switches to SSH', :js do
visit_project
select_protocol('SSH')
@@ -37,7 +37,7 @@ feature 'Developer views empty project instructions' do
expect_instructions_for('ssh')
end
- scenario 'switches to HTTP', js: true do
+ scenario 'switches to HTTP', :js do
visit_project
select_protocol('HTTP')
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
index 17f914c9c17..7a372757523 100644
--- a/spec/features/projects/edit_spec.rb
+++ b/spec/features/projects/edit_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Project edit', js: true do
+feature 'Project edit', :js do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 56addd64056..dfcf97ad495 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -101,35 +101,48 @@ feature 'Environment' do
end
context 'with terminal' do
- let(:project) { create(:kubernetes_project, :test_repo) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ context 'for project master' do
+ let(:role) { :master }
- context 'for project master' do
- let(:role) { :master }
+ scenario 'it shows the terminal button' do
+ expect(page).to have_terminal_button
+ end
- scenario 'it shows the terminal button' do
- expect(page).to have_terminal_button
+ context 'web terminal', :js do
+ before do
+ # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly
+ allow_any_instance_of(Environment).to receive(:terminals) { nil }
+ visit terminal_project_environment_path(project, environment)
+ end
+
+ it 'displays a web terminal' do
+ expect(page).to have_selector('#terminal')
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
end
- context 'web terminal', :js do
- before do
- # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly
- allow_any_instance_of(Environment).to receive(:terminals) { nil }
- visit terminal_project_environment_path(project, environment)
- end
+ context 'for developer' do
+ let(:role) { :developer }
- it 'displays a web terminal' do
- expect(page).to have_selector('#terminal')
- expect(page).to have_link(nil, href: environment.external_url)
+ scenario 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
end
end
end
- context 'for developer' do
- let(:role) { :developer }
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project, :test_repo) }
- scenario 'does not show terminal button' do
- expect(page).not_to have_terminal_button
- end
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
@@ -193,12 +206,14 @@ feature 'Environment' do
create(:environment, project: project,
name: 'staging-1.0/review',
state: :available)
-
- visit folder_project_environments_path(project, id: 'staging-1.0')
end
it 'renders a correct environment folder' do
- expect(page).to have_http_status(:ok)
+ reqs = inspect_requests do
+ visit folder_project_environments_path(project, id: 'staging-1.0')
+ end
+
+ expect(reqs.first.status_code).to eq(200)
expect(page).to have_content('Environments / staging-1.0')
end
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index af7ad365546..4a05313c14a 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -14,8 +14,10 @@ feature 'Environments page', :js do
it 'shows "Available" and "Stopped" tab with links' do
visit_environments(project)
- expect(page).to have_link('Available')
- expect(page).to have_link('Stopped')
+ expect(page).to have_selector('.js-environments-tab-available')
+ expect(page).to have_content('Available')
+ expect(page).to have_selector('.js-environments-tab-stopped')
+ expect(page).to have_content('Stopped')
end
describe 'with one available environment' do
@@ -75,8 +77,8 @@ feature 'Environments page', :js do
it 'does not show environments and counters are set to zero' do
expect(page).to have_content('You don\'t have any environments right now.')
- expect(page.find('.js-available-environments-count').text).to eq('0')
- expect(page.find('.js-stopped-environments-count').text).to eq('0')
+ expect(page.find('.js-environments-tab-available .badge').text).to eq('0')
+ expect(page.find('.js-environments-tab-stopped .badge').text).to eq('0')
end
end
@@ -93,8 +95,8 @@ feature 'Environments page', :js do
it 'shows environments names and counters' do
expect(page).to have_link(environment.name)
- expect(page.find('.js-available-environments-count').text).to eq('1')
- expect(page.find('.js-stopped-environments-count').text).to eq('0')
+ expect(page.find('.js-environments-tab-available .badge').text).to eq('1')
+ expect(page.find('.js-environments-tab-stopped .badge').text).to eq('0')
end
it 'does not show deployments' do
@@ -145,13 +147,13 @@ feature 'Environments page', :js do
expect(page).to have_content(action.name.humanize)
end
- it 'allows to play a manual action', js: true do
+ it 'allows to play a manual action', :js do
expect(action).to be_manual
find('.js-dropdown-play-icon-container').click
expect(page).to have_content(action.name.humanize)
- expect { find('.js-manual-action-link').trigger('click') }
+ expect { find('.js-manual-action-link').click }
.not_to change { Ci::Pipeline.count }
end
@@ -206,22 +208,35 @@ feature 'Environments page', :js do
end
context 'when kubernetes terminal is available' do
- let(:project) { create(:kubernetes_project, :test_repo) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ context 'for project master' do
+ let(:role) { :master }
- context 'for project master' do
- let(:role) { :master }
+ it 'shows the terminal button' do
+ expect(page).to have_terminal_button
+ end
+ end
+
+ context 'when user is a developer' do
+ let(:role) { :developer }
- it 'shows the terminal button' do
- expect(page).to have_terminal_button
+ it 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
+ end
end
end
- context 'when user is a developer' do
- let(:role) { :developer }
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project, :test_repo) }
- it 'does not show terminal button' do
- expect(page).not_to have_terminal_button
- end
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [create(:project, :repository)]) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
end
@@ -294,11 +309,32 @@ feature 'Environments page', :js do
end
end
+ describe 'environments folders view' do
+ before do
+ create(:environment, project: project,
+ name: 'staging.review/review-1',
+ state: :available)
+ create(:environment, project: project,
+ name: 'staging.review/review-2',
+ state: :available)
+ end
+
+ scenario 'user opens folder view' do
+ visit folder_project_environments_path(project, 'staging.review')
+ wait_for_requests
+
+ expect(page).to have_content('Environments / staging.review')
+ expect(page).to have_content('review-1')
+ expect(page).to have_content('review-2')
+ end
+ end
+
def have_terminal_button
have_link(nil, href: terminal_project_environment_path(project, environment))
end
def visit_environments(project, **opts)
visit project_environments_path(project, **opts)
+ wait_for_requests
end
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 57722276d79..951456763dc 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -6,7 +6,7 @@ describe 'Edit Project Settings' do
let!(:issue) { create(:issue, project: project) }
let(:non_member) { create(:user) }
- describe 'project features visibility selectors', js: true do
+ describe 'project features visibility selectors', :js do
before do
project.team << [member, :master]
sign_in(member)
@@ -22,7 +22,7 @@ describe 'Edit Project Settings' do
# disable by clicking toggle
toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
- click_button 'Save changes'
+ find('input[value="Save changes"]').click
end
wait_for_requests
expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
@@ -30,7 +30,7 @@ describe 'Edit Project Settings' do
# re-enable by clicking toggle again
toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
- click_button 'Save changes'
+ find('input[value="Save changes"]').click
end
wait_for_requests
expect(page).to have_selector(".shortcuts-#{shortcut_name}")
@@ -163,7 +163,7 @@ describe 'Edit Project Settings' do
end
end
- describe 'repository visibility', js: true do
+ describe 'repository visibility', :js do
before do
project.team << [member, :master]
sign_in(member)
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index f62a9edd37e..84197e45dcb 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'user browses project', js: true do
+feature 'user browses project', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
index cebb238dda1..3c3a5326538 100644
--- a/spec/features/projects/files/dockerfile_dropdown_spec.rb
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -16,7 +16,7 @@ feature 'User wants to add a Dockerfile file' do
expect(page).to have_css('.dockerfile-selector')
end
- scenario 'user can pick a Dockerfile file from the dropdown', js: true do
+ scenario 'user can pick a Dockerfile file from the dropdown', :js do
find('.js-dockerfile-selector').click
wait_for_requests
diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
index c7e3f657639..3ab43b3c656 100644
--- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb
+++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
@@ -1,24 +1,24 @@
require 'spec_helper'
-feature 'User uses soft wrap whilst editing file', js: true do
+feature 'User uses soft wrap whilst editing file', :js do
before do
user = create(:user)
project = create(:project, :repository)
project.team << [user, :master]
sign_in user
visit project_new_blob_path(project, 'master', file_name: 'test_file-name')
- editor = find('.file-editor.code')
- editor.click
- editor.send_keys 'Touch water with paw then recoil in horror chase dog then
- run away chase the pig around the house eat owner\'s food, and knock
- dish off table head butt cant eat out of my own dish. Cat is love, cat
- is life rub face on everything poop on grasses so meow. Playing with
- balls of wool flee in terror at cucumber discovered on floor run in
- circles tuxedo cats always looking dapper, but attack dog, run away
- and pretend to be victim so all of a sudden cat goes crazy, yet chase
- laser. Make muffins sit in window and stare ooo, a bird! yum lick yarn
- hanging out of own butt jump off balcony, onto stranger\'s head yet
- chase laser. Purr for no reason stare at ceiling hola te quiero.'.squish
+ page.within('.file-editor.code') do
+ find('.ace_text-input', visible: false).send_keys 'Touch water with paw then recoil in horror chase dog then
+ run away chase the pig around the house eat owner\'s food, and knock
+ dish off table head butt cant eat out of my own dish. Cat is love, cat
+ is life rub face on everything poop on grasses so meow. Playing with
+ balls of wool flee in terror at cucumber discovered on floor run in
+ circles tuxedo cats always looking dapper, but attack dog, run away
+ and pretend to be victim so all of a sudden cat goes crazy, yet chase
+ laser. Make muffins sit in window and stare ooo, a bird! yum lick yarn
+ hanging out of own butt jump off balcony, onto stranger\'s head yet
+ chase laser. Purr for no reason stare at ceiling hola te quiero.'.squish
+ end
end
let(:toggle_button) { find('.soft-wrap-toggle') }
@@ -36,6 +36,6 @@ feature 'User uses soft wrap whilst editing file', js: true do
end
def get_content_width
- find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/)
+ find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/).to_i
end
end
diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb
index 7f97fdb8cc9..618725ee781 100644
--- a/spec/features/projects/files/find_file_keyboard_spec.rb
+++ b/spec/features/projects/files/find_file_keyboard_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Find file keyboard shortcuts', js: true do
+feature 'Find file keyboard shortcuts', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
index e2044c9d5aa..81d68c3d67c 100644
--- a/spec/features/projects/files/gitignore_dropdown_spec.rb
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -13,7 +13,7 @@ feature 'User wants to add a .gitignore file' do
expect(page).to have_css('.gitignore-selector')
end
- scenario 'user can pick a .gitignore file from the dropdown', js: true do
+ scenario 'user can pick a .gitignore file from the dropdown', :js do
find('.js-gitignore-selector').click
wait_for_requests
within '.gitignore-selector' do
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
index ab242b0b0b5..8e58fa7bd56 100644
--- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -13,7 +13,7 @@ feature 'User wants to add a .gitlab-ci.yml file' do
expect(page).to have_css('.gitlab-ci-yml-selector')
end
- scenario 'user can pick a template from the dropdown', js: true do
+ scenario 'user can pick a template from the dropdown', :js do
find('.js-gitlab-ci-yml-selector').click
wait_for_requests
within '.gitlab-ci-yml-selector' do
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 95af263bcac..6c5b1086ec1 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project owner creates a license file', js: true do
+feature 'project owner creates a license file', :js do
let(:project_master) { create(:user) }
let(:project) { create(:project, :repository) }
background do
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 7bcab01c739..6c616bf0456 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project owner sees a link to create a license file in empty project', js: true do
+feature 'project owner sees a link to create a license file in empty project', :js do
let(:project_master) { create(:user) }
let(:project) { create(:project) }
background do
diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb
index 48003eeaa87..f95a60e5194 100644
--- a/spec/features/projects/files/template_type_dropdown_spec.rb
+++ b/spec/features/projects/files/template_type_dropdown_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Template type dropdown selector', js: true do
+feature 'Template type dropdown selector', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb
index 9bcd5beabb8..64fe350f3dc 100644
--- a/spec/features/projects/files/undo_template_spec.rb
+++ b/spec/features/projects/files/undo_template_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Template Undo Button', js: true do
+feature 'Template Undo Button', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
index e10d29e5eea..842840cc04c 100644
--- a/spec/features/projects/fork_spec.rb
+++ b/spec/features/projects/fork_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Project fork' do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
@@ -24,8 +26,9 @@ describe 'Project fork' do
end
context 'master in group' do
+ let(:group) { create(:group) }
+
before do
- group = create(:group)
group.add_master(user)
end
@@ -53,5 +56,17 @@ describe 'Project fork' do
expect(page).to have_css('.fork-thumbnail', count: 2)
expect(page).to have_css('.fork-thumbnail.disabled')
end
+
+ it 'links to the fork if the project was already forked within that namespace' do
+ forked_project = fork_project(project, user, namespace: group, repository: true)
+
+ visit new_project_fork_path(project)
+
+ expect(page).to have_css('div.forked', text: group.full_name)
+
+ click_link group.full_name
+
+ expect(current_path).to eq(project_path(forked_project))
+ end
end
end
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
index cff3b1f5743..1c988726ae6 100644
--- a/spec/features/projects/gfm_autocomplete_load_spec.rb
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'GFM autocomplete loading', js: true do
+describe 'GFM autocomplete loading', :js do
let(:project) { create(:project) }
before do
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 62d244ff259..461aa39d0ad 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
# It looks up for any sensitive word inside the JSON, so if a sensitive word is found
# we''l have to either include it adding the model that includes it to the +safe_list+
# or make sure the attribute is blacklisted in the +import_export.yml+ configuration
-feature 'Import/Export - project export integration test', js: true do
+feature 'Import/Export - project export integration test', :js do
include Select2Helper
include ExportFileHelper
@@ -41,7 +41,7 @@ feature 'Import/Export - project export integration test', js: true do
expect(page).to have_content('Export project')
- click_link 'Export project'
+ find(:link, 'Export project').send_keys(:return)
visit edit_project_path(project)
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index e5c7781a096..af125e1b9d3 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Import/Export - project import integration test', js: true do
+feature 'Import/Export - project import integration test', :js do
include Select2Helper
let(:user) { create(:user) }
@@ -27,6 +27,7 @@ feature 'Import/Export - project import integration test', js: true do
select2(namespace.id, from: '#project_namespace_id')
fill_in :project_path, with: project_path, visible: true
+ click_import_project_tab
click_link 'GitLab export'
expect(page).to have_content('Import an exported GitLab project')
@@ -51,6 +52,7 @@ feature 'Import/Export - project import integration test', js: true do
context 'path is not prefilled' do
scenario 'user imports an exported project successfully' do
visit new_project_path
+ click_import_project_tab
click_link 'GitLab export'
fill_in :path, with: 'test-project-path', visible: true
@@ -72,6 +74,7 @@ feature 'Import/Export - project import integration test', js: true do
select2(user.namespace.id, from: '#project_namespace_id')
fill_in :project_path, with: project.name, visible: true
+ click_import_project_tab
click_link 'GitLab export'
attach_file('file', file)
click_on 'Import project'
@@ -81,19 +84,6 @@ feature 'Import/Export - project import integration test', js: true do
end
end
- context 'when limited to the default user namespace' do
- scenario 'passes correct namespace ID in the URL' do
- visit new_project_path
-
- fill_in :project_path, with: 'test-project-path', visible: true
-
- click_link 'GitLab export'
-
- expect(page).to have_content('GitLab project export')
- expect(URI.parse(current_url).query).to eq("namespace_id=#{user.namespace.id}&path=test-project-path")
- end
- end
-
def wiki_exists?(project)
wiki = ProjectWiki.new(project)
File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty?
@@ -102,4 +92,8 @@ feature 'Import/Export - project import integration test', js: true do
def project_hook_exists?(project)
Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository).exists?
end
+
+ def click_import_project_tab
+ find('#import-project-tab').click
+ end
end
diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb
index 691b0e1e4ca..e76bc6f1220 100644
--- a/spec/features/projects/import_export/namespace_export_file_spec.rb
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Import/Export - Namespace export file cleanup', js: true do
+feature 'Import/Export - Namespace export file cleanup', :js do
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
@@ -52,7 +52,7 @@ feature 'Import/Export - Namespace export file cleanup', js: true do
expect(page).to have_content('Export project')
- click_link 'Export project'
+ find(:link, 'Export project').send_keys(:return)
visit edit_project_path(project)
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 9614c72cdc3..0c354298433 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index c9b8ef4e37b..a012db8fd27 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'issuable templates', js: true do
+feature 'issuable templates', :js do
include ProjectForksHelper
let(:user) { create(:user) }
@@ -35,7 +35,7 @@ feature 'issuable templates', js: true do
page.within('.content .issuable-actions') do
click_on 'Edit'
end
- fill_in :'issue-title', with: 'test issue title'
+ fill_in :'issuable-title', with: 'test issue title'
end
scenario 'user selects "bug" template' do
@@ -80,7 +80,7 @@ feature 'issuable templates', js: true do
page.within('.content .issuable-actions') do
click_on 'Edit'
end
- fill_in :'issue-title', with: 'test issue title'
+ fill_in :'issuable-title', with: 'test issue title'
fill_in :'issue-description', with: prior_description
end
@@ -118,7 +118,7 @@ feature 'issuable templates', js: true do
context 'user creates a merge request from a forked project using templates' do
let(:template_content) { 'this is a test "feature-proposal" template' }
let(:fork_user) { create(:user) }
- let(:forked_project) { fork_project(project, fork_user) }
+ let(:forked_project) { fork_project(project, fork_user, repository: true) }
let(:merge_request) { create(:merge_request, :with_diffs, source_project: forked_project, target_project: project) }
background do
diff --git a/spec/features/projects/issues/list_spec.rb b/spec/features/projects/issues/list_spec.rb
deleted file mode 100644
index 9fc03f49f5b..00000000000
--- a/spec/features/projects/issues/list_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'spec_helper'
-
-feature 'Issues List' do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
-
- background do
- project.team << [user, :developer]
-
- sign_in(user)
- end
-
- scenario 'user does not see create new list button' do
- create(:issue, project: project)
-
- visit project_issues_path(project)
-
- expect(page).not_to have_selector('.js-new-board-list')
- end
-end
diff --git a/spec/features/projects/issues/user_views_issues_spec.rb b/spec/features/projects/issues/user_views_issues_spec.rb
new file mode 100644
index 00000000000..d35009b8974
--- /dev/null
+++ b/spec/features/projects/issues/user_views_issues_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe 'User views issues' do
+ set(:user) { create(:user) }
+
+ shared_examples_for 'shows issues' do
+ it 'shows issues' do
+ expect(page).to have_content(project.name)
+ .and have_content(issue1.title)
+ .and have_content(issue2.title)
+ .and have_no_selector('.js-new-board-list')
+ end
+ end
+
+ context 'when project is public' do
+ set(:project) { create(:project_empty_repo, :public) }
+ set(:issue1) { create(:issue, project: project) }
+ set(:issue2) { create(:issue, project: project) }
+
+ context 'when signed in' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(project_issues_path(project))
+ end
+
+ include_examples 'shows issues'
+ end
+
+ context 'when not signed in' do
+ before do
+ visit(project_issues_path(project))
+ end
+
+ include_examples 'shows issues'
+ end
+ end
+
+ context 'when project is internal' do
+ set(:project) { create(:project_empty_repo, :internal) }
+ set(:issue1) { create(:issue, project: project) }
+ set(:issue2) { create(:issue, project: project) }
+
+ context 'when signed in' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(project_issues_path(project))
+ end
+
+ include_examples 'shows issues'
+ end
+ end
+end
diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
index 21c9acc7ac0..5d9208ebadd 100644
--- a/spec/features/projects/jobs/user_browses_job_spec.rb
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -21,12 +21,12 @@ describe 'User browses a job', :js do
expect(page).to have_content("Job ##{build.id}")
expect(page).to have_css('#build-trace')
- click_link('Erase')
+ accept_confirm { click_link('Erase') }
+ expect(page).to have_no_css('.artifacts')
expect(build).not_to have_trace
expect(build.artifacts_file.exists?).to be_falsy
expect(build.artifacts_metadata.exists?).to be_falsy
- expect(page).to have_no_css('.artifacts')
page.within('.erased') do
expect(page).to have_content('Job has been erased')
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index a4ed589f3de..c2a0d2395a9 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -299,14 +299,14 @@ feature 'Jobs' do
end
shared_examples 'expected variables behavior' do
- it 'shows variable key and value after click', js: true do
- expect(page).to have_css('.reveal-variables')
+ it 'shows variable key and value after click', :js do
+ expect(page).to have_css('.js-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).not_to have_css('.js-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
@@ -380,7 +380,6 @@ feature 'Jobs' do
end
it 'loads the page and shows all needed controls' do
- expect(page.status_code).to eq(200)
expect(page).to have_content 'Retry'
end
end
@@ -392,11 +391,10 @@ feature 'Jobs' do
job.run!
visit project_job_path(project, job)
find('.js-cancel-job').click()
- find('.js-retry-button').trigger('click')
+ find('.js-retry-button').click
end
it 'shows the right status and buttons', :js do
- expect(page).to have_http_status(200)
page.within('aside.right-sidebar') do
expect(page).to have_content 'Cancel'
end
@@ -443,28 +441,30 @@ feature 'Jobs' do
context 'access source' do
context 'job from project' do
before do
- Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
job.run!
- visit project_job_path(project, job)
- find('.js-raw-link-controller').click()
end
it 'sends the right headers' do
- expect(page.status_code).to eq(200)
- expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(page.response_headers['X-Sendfile']).to eq(job.trace.send(:current_path))
+ requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do
+ visit raw_project_job_path(project, job)
+ end
+
+ expect(requests.first.status_code).to eq(200)
+ expect(requests.first.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(requests.first.response_headers['X-Sendfile']).to eq(job.trace.send(:current_path))
end
end
context 'job from other project' do
before do
- Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
job2.run!
- visit raw_project_job_path(project, job2)
end
it 'sends the right headers' do
- expect(page.status_code).to eq(404)
+ requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do
+ visit raw_project_job_path(project, job2)
+ end
+ expect(requests.first.status_code).to eq(404)
end
end
end
@@ -473,8 +473,6 @@ feature 'Jobs' do
let(:existing_file) { Tempfile.new('existing-trace-file').path }
before do
- Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
-
job.run!
end
@@ -483,16 +481,14 @@ feature 'Jobs' do
allow_any_instance_of(Gitlab::Ci::Trace)
.to receive(:paths)
.and_return([existing_file])
-
- visit project_job_path(project, job)
-
- find('.js-raw-link-controller').click
end
it 'sends the right headers' do
- expect(page.status_code).to eq(200)
- expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(page.response_headers['X-Sendfile']).to eq(existing_file)
+ requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do
+ visit raw_project_job_path(project, job)
+ end
+ expect(requests.first.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(requests.first.response_headers['X-Sendfile']).to eq(existing_file)
end
end
diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb
index 5716d151250..e8c70dec854 100644
--- a/spec/features/projects/labels/subscription_spec.rb
+++ b/spec/features/projects/labels/subscription_spec.rb
@@ -13,7 +13,7 @@ feature 'Labels subscription' do
sign_in user
end
- scenario 'users can subscribe/unsubscribe to labels', js: true do
+ scenario 'users can subscribe/unsubscribe to labels', :js do
visit project_labels_path(project)
expect(page).to have_content('bug')
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 8f85e972027..d063f5c27b5 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -17,7 +17,7 @@ feature 'Prioritize labels' do
sign_in user
end
- scenario 'user can prioritize a group label', js: true do
+ scenario 'user can prioritize a group label', :js do
visit project_labels_path(project)
expect(page).to have_content('Star labels to start sorting by priority')
@@ -34,7 +34,7 @@ feature 'Prioritize labels' do
end
end
- scenario 'user can unprioritize a group label', js: true do
+ scenario 'user can unprioritize a group label', :js do
create(:label_priority, project: project, label: feature, priority: 1)
visit project_labels_path(project)
@@ -52,7 +52,7 @@ feature 'Prioritize labels' do
end
end
- scenario 'user can prioritize a project label', js: true do
+ scenario 'user can prioritize a project label', :js do
visit project_labels_path(project)
expect(page).to have_content('Star labels to start sorting by priority')
@@ -69,7 +69,7 @@ feature 'Prioritize labels' do
end
end
- scenario 'user can unprioritize a project label', js: true do
+ scenario 'user can unprioritize a project label', :js do
create(:label_priority, project: project, label: bug, priority: 1)
visit project_labels_path(project)
@@ -88,7 +88,7 @@ feature 'Prioritize labels' do
end
end
- scenario 'user can sort prioritized labels and persist across reloads', js: true do
+ scenario 'user can sort prioritized labels and persist across reloads', :js do
create(:label_priority, project: project, label: bug, priority: 1)
create(:label_priority, project: project, label: feature, priority: 2)
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
index c8988aa63a7..6d729f2f85f 100644
--- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Members > Group requester cannot request access to project', js: true do
+feature 'Projects > Members > Group requester cannot request access to project', :js do
let(:user) { create(:user) }
let(:owner) { create(:user) }
let(:group) { create(:group, :public, :access_requestable) }
diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb
index 9950272af08..7f067aadec6 100644
--- a/spec/features/projects/members/groups_with_access_list_spec.rb
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Members > Groups with access list', js: true do
+feature 'Projects > Members > Groups with access list', :js do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public) }
@@ -31,6 +31,7 @@ feature 'Projects > Members > Groups with access list', js: true do
tomorrow = Date.today + 3
fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F")
+ find('body').click
wait_for_requests
page.within(find('li.group_member')) do
@@ -40,7 +41,7 @@ feature 'Projects > Members > Groups with access list', js: true do
scenario 'deletes group link' do
page.within(first('.group_member')) do
- find('.btn-remove').click
+ accept_confirm { find('.btn-remove').click }
end
wait_for_requests
diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb
index 237c059e595..65b11a1d9e7 100644
--- a/spec/features/projects/members/list_spec.rb
+++ b/spec/features/projects/members/list_spec.rb
@@ -55,6 +55,22 @@ feature 'Project members list' do
end
end
+ scenario 'remove user from project', :js do
+ other_user = create(:user)
+ project.add_developer(other_user)
+
+ visit_members_page
+
+ accept_confirm do
+ find(:css, 'li.project_member', text: other_user.name).find(:css, 'a.btn-remove').click
+ end
+
+ wait_for_requests
+
+ expect(page).not_to have_content(other_user.name)
+ expect(project.users).not_to include(other_user)
+ end
+
scenario 'invite user to project', :js do
visit_members_page
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index cd621b6b3ce..0f88f4cb1e8 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Members > Master adds member with expiration date', js: true do
+feature 'Projects > Members > Master adds member with expiration date', :js do
include Select2Helper
include ActiveSupport::Testing::TimeHelpers
@@ -20,7 +20,7 @@ feature 'Projects > Members > Master adds member with expiration date', js: true
page.within '.users-project-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
- fill_in 'expires_at', with: date.to_s(:medium)
+ fill_in 'expires_at', with: date.to_s(:medium) + "\n"
click_on 'Add to project'
end
@@ -37,7 +37,7 @@ feature 'Projects > Members > Master adds member with expiration date', js: true
visit project_project_members_path(project)
page.within "#project_member_#{new_member.project_members.first.id}" do
- find('.js-access-expiration-date').set date.to_s(:medium)
+ find('.js-access-expiration-date').set date.to_s(:medium) + "\n"
wait_for_requests
expect(page).to have_content('Expires in 3 days')
end
diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb
index 3b368f8e25d..3198798306c 100644
--- a/spec/features/projects/members/share_with_group_spec.rb
+++ b/spec/features/projects/members/share_with_group_spec.rb
@@ -41,7 +41,7 @@ feature 'Project > Members > Share with Group', :js do
select2 group_to_share_with.id, from: '#link_group_id'
page.find('body').click
- find('.btn-create').trigger('click')
+ find('.btn-create').click
page.within('.project-members-groups') do
expect(page).to have_content(group_to_share_with.name)
@@ -123,7 +123,7 @@ feature 'Project > Members > Share with Group', :js do
fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d')
page.find('body').click
- find('.btn-create').trigger('click')
+ find('.btn-create').click
end
scenario 'the group link shows the expiration time with a warning class' do
@@ -149,7 +149,7 @@ feature 'Project > Members > Share with Group', :js do
create(:group).add_owner(master)
visit project_settings_members_path(project)
- execute_script 'GroupsSelect.PER_PAGE = 1;'
+ execute_script 'GROUP_SELECT_PER_PAGE = 1;'
open_select2 '#link_group_id'
end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 0fbe1ddb2a5..4eb36156812 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -60,7 +60,7 @@ feature 'Projects > Members > User requests access', :js do
expect(project.requesters.exists?(user_id: user)).to be_truthy
- click_link 'Withdraw Access Request'
+ accept_confirm { click_link 'Withdraw Access Request' }
expect(project.requesters.exists?(user_id: user)).to be_falsey
expect(page).to have_content 'Your access request to the project has been withdrawn.'
diff --git a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
index f34302f25f8..e3f90a78cb5 100644
--- a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
+++ b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
@@ -31,7 +31,7 @@ describe 'User comments on a diff', :js do
page.within('.files > div:nth-child(3)') do
expect(page).to have_content('Line is wrong')
- find('.js-toggle-diff-comments').trigger('click')
+ find('.js-toggle-diff-comments').click
expect(page).not_to have_content('Line is wrong')
end
@@ -64,7 +64,7 @@ describe 'User comments on a diff', :js do
# Hide the comment.
page.within('.files > div:nth-child(3)') do
- find('.js-toggle-diff-comments').trigger('click')
+ find('.js-toggle-diff-comments').click
expect(page).not_to have_content('Line is wrong')
end
@@ -77,7 +77,7 @@ describe 'User comments on a diff', :js do
# Show the comment.
page.within('.files > div:nth-child(3)') do
- find('.js-toggle-diff-comments').trigger('click')
+ find('.js-toggle-diff-comments').click
end
# Now both the comments should be shown.
@@ -90,6 +90,7 @@ describe 'User comments on a diff', :js do
end
# Check the same comments in the side-by-side view.
+ execute_script("window.scrollTo(0,0);")
click_link('Side-by-side')
wait_for_requests
@@ -153,11 +154,11 @@ describe 'User comments on a diff', :js do
find('.more-actions').click
find('.more-actions .dropdown-menu li', match: :first)
- find('.js-note-delete').click
+ accept_confirm { find('.js-note-delete').click }
end
page.within('.merge-request-tabs') do
- find('.notes-tab').trigger('click')
+ find('.notes-tab').click
end
wait_for_requests
diff --git a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
index f6e3997383f..3d19a2923b9 100644
--- a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'User edits a merge request', :js do
+ include Select2Helper
+
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
@@ -15,8 +17,7 @@ describe 'User edits a merge request', :js do
it 'changes the target branch' do
expect(page).to have_content('Target branch')
- first('.target_branch').click
- select('merge-test', from: 'merge_request_target_branch', visible: false)
+ select2('merge-test', from: '#merge_request_target_branch')
click_button('Save changes')
expect(page).to have_content("Request to merge #{merge_request.source_branch} into merge-test")
diff --git a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
index 30a80f8e652..4ca435491cb 100644
--- a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
+++ b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
@@ -13,7 +13,7 @@ describe 'User manages subscription', :js do
end
it 'toggles subscription' do
- subscribe_button = find('.issuable-subscribe-button span')
+ subscribe_button = find('.js-issuable-subscribe-button')
expect(subscribe_button).to have_content('Subscribe')
diff --git a/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb
index 8970cf54457..3aac93eaf7c 100644
--- a/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb
@@ -6,7 +6,7 @@ describe 'User views an open merge request' do
end
context 'when a merge request does not have repository' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
before do
visit(merge_request_path(merge_request))
diff --git a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
index 07b8c1ef479..bf95dbb7d09 100644
--- a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
+++ b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
@@ -1,72 +1,115 @@
require 'spec_helper'
describe 'User views open merge requests' do
- let(:project) { create(:project, :public, :repository) }
+ set(:user) { create(:user) }
- context "when the target branch is the project's default branch" do
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
-
- before do
- visit(project_merge_requests_path(project))
+ shared_examples_for 'shows merge requests' do
+ it 'shows merge requests' do
+ expect(page).to have_content(project.name).and have_content(merge_request.source_project.name)
end
+ end
- it 'shows open merge requests' do
- expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title)
- end
+ context 'when project is public' do
+ set(:project) { create(:project, :public, :repository) }
- it 'does not show target branch name' do
- expect(page).to have_content(merge_request.title)
- expect(find('.issuable-info')).not_to have_content(project.default_branch)
- end
- end
+ context 'when not signed in' do
+ context "when the target branch is the project's default branch" do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
- context "when the target branch is different from the project's default branch" do
- let!(:merge_request) do
- create(:merge_request,
- source_project: project,
- target_project: project,
- source_branch: 'fix',
- target_branch: 'feature_conflict')
- end
+ before do
+ visit(project_merge_requests_path(project))
+ end
- before do
- visit(project_merge_requests_path(project))
- end
+ include_examples 'shows merge requests'
- it 'shows target branch name' do
- expect(page).to have_content(merge_request.target_branch)
- end
- end
+ it 'shows open merge requests' do
+ expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title)
+ end
- context 'when a merge request has pipelines' do
- let!(:build) { create :ci_build, pipeline: pipeline }
+ it 'does not show target branch name' do
+ expect(page).to have_content(merge_request.title)
+ expect(find('.issuable-info')).not_to have_content(project.default_branch)
+ end
+ end
- let(:merge_request) do
- create(:merge_request_with_diffs,
- source_project: project,
- target_project: project,
- source_branch: 'merge-test')
- end
+ context "when the target branch is different from the project's default branch" do
+ let!(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'feature_conflict')
+ end
+
+ before do
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'shows target branch name' do
+ expect(page).to have_content(merge_request.target_branch)
+ end
+ end
- let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- head_pipeline_of: merge_request)
+ context 'when a merge request has pipelines' do
+ let!(:build) { create :ci_build, pipeline: pipeline }
+
+ let(:merge_request) do
+ create(:merge_request_with_diffs,
+ source_project: project,
+ target_project: project,
+ source_branch: 'merge-test')
+ end
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ head_pipeline_of: merge_request)
+ end
+
+ before do
+ project.enable_ci
+
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'shows pipeline status' do
+ page.within('.mr-list') do
+ expect(page).to have_link('Pipeline: pending')
+ end
+ end
+ end
end
- before do
- project.enable_ci
+ context 'when signed in' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
- visit(project_merge_requests_path(project))
+ visit(project_merge_requests_path(project))
+ end
+
+ include_examples 'shows merge requests'
end
+ end
- it 'shows pipeline status' do
- page.within('.mr-list') do
- expect(page).to have_link('Pipeline: pending')
+ context 'when project is internal' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ set(:project) { create(:project, :internal, :repository) }
+
+ context 'when signed in' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(project_merge_requests_path(project))
end
+
+ include_examples 'shows merge requests'
end
end
end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index cd3dc72d3c6..6f097ad16c7 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -9,12 +9,14 @@ feature 'New project' do
sign_in(user)
end
- it 'shows "New project" page' do
+ it 'shows "New project" page', :js do
visit new_project_path
expect(page).to have_content('Project path')
expect(page).to have_content('Project name')
+ find('#import-project-tab').click
+
expect(page).to have_link('GitHub')
expect(page).to have_link('Bitbucket')
expect(page).to have_link('GitLab.com')
@@ -23,14 +25,15 @@ feature 'New project' do
expect(page).to have_link('GitLab export')
end
- context 'Visibility level selector' do
+ context 'Visibility level selector', :js do
Gitlab::VisibilityLevel.options.each do |key, level|
it "sets selector to #{key}" do
stub_application_setting(default_project_visibility: level)
visit new_project_path
-
- expect(find_field("project_visibility_level_#{level}")).to be_checked
+ page.within('#blank-project-pane') do
+ expect(find_field("project_visibility_level_#{level}")).to be_checked
+ end
end
it "saves visibility level #{level} on validation error" do
@@ -38,8 +41,9 @@ feature 'New project' do
choose(s_(key))
click_button('Create project')
-
- expect(find_field("project_visibility_level_#{level}")).to be_checked
+ page.within('#blank-project-pane') do
+ expect(find_field("project_visibility_level_#{level}")).to be_checked
+ end
end
end
end
@@ -51,9 +55,11 @@ feature 'New project' do
end
it 'selects the user namespace' do
- namespace = find('#project_namespace_id')
+ page.within('#blank-project-pane') do
+ namespace = find('#project_namespace_id')
- expect(namespace.text).to eq user.username
+ expect(namespace.text).to eq user.username
+ end
end
end
@@ -66,9 +72,11 @@ feature 'New project' do
end
it 'selects the group namespace' do
- namespace = find('#project_namespace_id option[selected]')
+ page.within('#blank-project-pane') do
+ namespace = find('#project_namespace_id option[selected]')
- expect(namespace.text).to eq group.name
+ expect(namespace.text).to eq group.name
+ end
end
end
@@ -82,9 +90,11 @@ feature 'New project' do
end
it 'selects the group namespace' do
- namespace = find('#project_namespace_id option[selected]')
+ page.within('#blank-project-pane') do
+ namespace = find('#project_namespace_id option[selected]')
- expect(namespace.text).to eq subgroup.full_path
+ expect(namespace.text).to eq subgroup.full_path
+ end
end
end
@@ -124,9 +134,10 @@ feature 'New project' do
end
end
- context 'Import project options' do
+ context 'Import project options', :js do
before do
visit new_project_path
+ find('#import-project-tab').click
end
context 'from git repository url' do
diff --git a/spec/features/projects/no_password_spec.rb b/spec/features/projects/no_password_spec.rb
index 6aff079dd39..b3b3212556c 100644
--- a/spec/features/projects/no_password_spec.rb
+++ b/spec/features/projects/no_password_spec.rb
@@ -30,7 +30,7 @@ feature 'No Password Alert' do
let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml') }
before do
- stub_application_setting(password_authentication_enabled?: false)
+ stub_application_setting(password_authentication_enabled_for_git?: false)
stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 24b335a7068..fa2f7a1fd78 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -54,7 +54,7 @@ feature 'Pipeline Schedules', :js do
end
it 'deletes the pipeline' do
- click_link 'Delete'
+ accept_confirm { click_link 'Delete' }
expect(page).not_to have_css(".pipeline-schedule-table-row")
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index acbc5b046e6..b8fa1a54c24 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -67,13 +67,13 @@ describe 'Pipeline', :js do
it 'shows a running icon and a cancel action for the running build' do
page.within('#ci-badge-deploy') do
expect(page).to have_selector('.js-ci-status-icon-running')
- expect(page).to have_selector('.js-icon-action-cancel')
+ expect(page).to have_selector('.js-icon-cancel')
expect(page).to have_content('deploy')
end
end
it 'should be possible to cancel the running build' do
- find('#ci-badge-deploy .ci-action-icon-container').trigger('click')
+ find('#ci-badge-deploy .ci-action-icon-container').click
expect(page).not_to have_content('Cancel running')
end
@@ -86,13 +86,13 @@ describe 'Pipeline', :js do
expect(page).to have_content('build')
end
- page.within('#ci-badge-build .ci-action-icon-container') do
- expect(page).to have_selector('.js-icon-action-retry')
+ page.within('#ci-badge-build .ci-action-icon-container.js-icon-retry') do
+ expect(page).to have_selector('svg')
end
end
it 'should be possible to retry the success job' do
- find('#ci-badge-build .ci-action-icon-container').trigger('click')
+ find('#ci-badge-build .ci-action-icon-container').click
expect(page).not_to have_content('Retry job')
end
@@ -105,13 +105,13 @@ describe 'Pipeline', :js do
expect(page).to have_content('test')
end
- page.within('#ci-badge-test .ci-action-icon-container') do
- expect(page).to have_selector('.js-icon-action-retry')
+ page.within('#ci-badge-test .ci-action-icon-container.js-icon-retry') do
+ expect(page).to have_selector('svg')
end
end
it 'should be possible to retry the failed build' do
- find('#ci-badge-test .ci-action-icon-container').trigger('click')
+ find('#ci-badge-test .ci-action-icon-container').click
expect(page).not_to have_content('Retry job')
end
@@ -124,13 +124,13 @@ describe 'Pipeline', :js do
expect(page).to have_content('manual')
end
- page.within('#ci-badge-manual-build .ci-action-icon-container') do
- expect(page).to have_selector('.js-icon-action-play')
+ page.within('#ci-badge-manual-build .ci-action-icon-container.js-icon-play') do
+ expect(page).to have_selector('svg')
end
end
it 'should be possible to play the manual job' do
- find('#ci-badge-manual-build .ci-action-icon-container').trigger('click')
+ find('#ci-badge-manual-build .ci-action-icon-container').click
expect(page).not_to have_content('Play job')
end
@@ -165,7 +165,7 @@ describe 'Pipeline', :js do
context 'when retrying' do
before do
- find('.js-retry-button').trigger('click')
+ find('.js-retry-button').click
end
it { expect(page).not_to have_content('Retry') }
@@ -231,7 +231,7 @@ describe 'Pipeline', :js do
context 'when retrying' do
before do
- find('.js-retry-button').trigger('click')
+ find('.js-retry-button').click
end
it { expect(page).not_to have_content('Retry') }
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 92486d2bc57..a1b1d94ae06 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -56,31 +56,37 @@ describe 'Pipelines', :js do
end
it 'shows a tab for All pipelines and count' do
- expect(page.find('.js-pipelines-tab-all a').text).to include('All')
+ expect(page.find('.js-pipelines-tab-all').text).to include('All')
expect(page.find('.js-pipelines-tab-all .badge').text).to include('1')
end
it 'shows a tab for Pending pipelines and count' do
- expect(page.find('.js-pipelines-tab-pending a').text).to include('Pending')
+ expect(page.find('.js-pipelines-tab-pending').text).to include('Pending')
expect(page.find('.js-pipelines-tab-pending .badge').text).to include('0')
end
it 'shows a tab for Running pipelines and count' do
- expect(page.find('.js-pipelines-tab-running a').text).to include('Running')
+ expect(page.find('.js-pipelines-tab-running').text).to include('Running')
expect(page.find('.js-pipelines-tab-running .badge').text).to include('1')
end
it 'shows a tab for Finished pipelines and count' do
- expect(page.find('.js-pipelines-tab-finished a').text).to include('Finished')
+ expect(page.find('.js-pipelines-tab-finished').text).to include('Finished')
expect(page.find('.js-pipelines-tab-finished .badge').text).to include('0')
end
it 'shows a tab for Branches' do
- expect(page.find('.js-pipelines-tab-branches a').text).to include('Branches')
+ expect(page.find('.js-pipelines-tab-branches').text).to include('Branches')
end
it 'shows a tab for Tags' do
- expect(page.find('.js-pipelines-tab-tags a').text).to include('Tags')
+ expect(page.find('.js-pipelines-tab-tags').text).to include('Tags')
+ end
+
+ it 'updates content when tab is clicked' do
+ page.find('.js-pipelines-tab-pending').click
+ wait_for_requests
+ expect(page).to have_content('No pipelines to show.')
end
end
@@ -103,7 +109,7 @@ describe 'Pipelines', :js do
context 'when canceling' do
before do
- find('.js-pipelines-cancel-button').click
+ accept_confirm { find('.js-pipelines-cancel-button').click }
wait_for_requests
end
@@ -232,7 +238,7 @@ describe 'Pipelines', :js do
context 'when canceling' do
before do
- find('.js-pipelines-cancel-button').trigger('click')
+ accept_alert { find('.js-pipelines-cancel-button').click }
end
it 'indicates that pipeline was canceled' do
@@ -345,14 +351,14 @@ describe 'Pipelines', :js do
context 'when clicking a stage badge' do
it 'should open a dropdown' do
- find('.js-builds-dropdown-button').trigger('click')
+ find('.js-builds-dropdown-button').click
expect(page).to have_link build.name
end
it 'should be possible to cancel pending build' do
- find('.js-builds-dropdown-button').trigger('click')
- find('a.js-ci-action-icon').trigger('click')
+ find('.js-builds-dropdown-button').click
+ find('a.js-ci-action-icon').click
expect(page).to have_content('canceled')
expect(build.reload).to be_canceled
@@ -361,11 +367,16 @@ describe 'Pipelines', :js do
context 'dropdown jobs list' do
it 'should keep the dropdown open when the user ctr/cmd + clicks in the job name' do
- find('.js-builds-dropdown-button').trigger('click')
-
- execute_script('var e = $.Event("keydown", { keyCode: 64 }); $("body").trigger(e);')
-
- find('.mini-pipeline-graph-dropdown-item').trigger('click')
+ find('.js-builds-dropdown-button').click
+ dropdown_item = find('.mini-pipeline-graph-dropdown-item').native
+
+ %i(alt control).each do |meta_key|
+ page.driver.browser.action
+ .key_down(meta_key)
+ .click(dropdown_item)
+ .key_up(meta_key)
+ .perform
+ end
expect(page).to have_selector('.js-ci-action-icon')
end
@@ -391,6 +402,14 @@ describe 'Pipelines', :js do
expect(page).to have_selector('.gl-pagination .page', count: 2)
end
+
+ it 'should show updated content' do
+ visit project_pipelines_path(project)
+ wait_for_requests
+ page.find('.js-next-button a').click
+
+ expect(page).to have_selector('.gl-pagination .page', count: 2)
+ end
end
end
@@ -453,7 +472,7 @@ describe 'Pipelines', :js do
visit new_project_pipeline_path(project)
end
- context 'for valid commit', js: true do
+ context 'for valid commit', :js do
before do
click_button project.default_branch
@@ -481,6 +500,18 @@ describe 'Pipelines', :js do
end
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
+ it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again' do
+ click_button project.default_branch
+
+ stub_ci_pipeline_to_return_yaml_file
+
+ page.within '.dropdown-menu' do
+ click_link 'master'
+ end
+
+ expect { click_on 'Create pipeline' }
+ .to change { Ci::Pipeline.count }.by(1)
+ end
end
end
end
@@ -501,7 +532,7 @@ describe 'Pipelines', :js do
end
describe 'find pipelines' do
- it 'shows filtered pipelines', js: true do
+ it 'shows filtered pipelines', :js do
click_button project.default_branch
page.within '.dropdown-menu' do
@@ -525,7 +556,6 @@ describe 'Pipelines', :js do
let(:project) { create(:project, :public, :repository) }
it { expect(page).to have_content 'Build with confidence' }
- it { expect(page).to have_http_status(:success) }
end
context 'when project is private' do
diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb
index 06568817757..a3ea778d401 100644
--- a/spec/features/projects/project_settings_spec.rb
+++ b/spec/features/projects/project_settings_spec.rb
@@ -10,7 +10,7 @@ describe 'Edit Project Settings' do
sign_in(user)
end
- describe 'Project settings section', js: true do
+ describe 'Project settings section', :js do
it 'shows errors for invalid project name' do
visit edit_project_path(project)
fill_in 'project_name_edit', with: 'foo&bar'
@@ -125,7 +125,7 @@ describe 'Edit Project Settings' do
end
end
- describe 'Transfer project section', js: true do
+ describe 'Transfer project section', :js do
let!(:project) { create(:project, :repository, namespace: user.namespace, name: 'gitlabhq') }
let!(:group) { create(:group) }
@@ -144,7 +144,10 @@ describe 'Edit Project Settings' do
specify 'the project is accessible via the new path' do
transfer_project(project, group)
new_path = namespace_project_path(group, project)
+
visit new_path
+ wait_for_requests
+
expect(current_path).to eq(new_path)
expect(find('.breadcrumbs')).to have_content(project.name)
end
@@ -153,7 +156,10 @@ describe 'Edit Project Settings' do
old_path = project_path(project)
transfer_project(project, group)
new_path = namespace_project_path(group, project)
+
visit old_path
+ wait_for_requests
+
expect(current_path).to eq(new_path)
expect(find('.breadcrumbs')).to have_content(project.name)
end
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
index f0a23729220..33ccbc1a29f 100644
--- a/spec/features/projects/ref_switcher_spec.rb
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -1,11 +1,12 @@
require 'rails_helper'
-feature 'Ref switcher', js: true do
+feature 'Ref switcher', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
before do
project.team << [user, :master]
+ set_cookie('new_repo', 'true')
sign_in(user)
visit project_tree_path(project, 'master')
end
@@ -40,4 +41,38 @@ feature 'Ref switcher', js: true do
expect(page).to have_title "'test'"
end
+
+ context "create branch" do
+ let(:input) { find('.js-new-branch-name') }
+
+ before do
+ click_button 'master'
+ wait_for_requests
+
+ page.within '.project-refs-form' do
+ find(".dropdown-footer-list a").click
+ end
+ end
+
+ it "shows error message for the invalid branch name" do
+ input.set 'foo bar'
+ click_button('Create')
+ wait_for_requests
+ expect(page).to have_content 'Branch name is invalid'
+ end
+
+ it "should create new branch properly" do
+ input.set 'new-branch-name'
+ click_button('Create')
+ wait_for_requests
+ expect(find('.js-project-refs-dropdown')).to have_content 'new-branch-name'
+ end
+
+ it "should create new branch by Enter key" do
+ input.set 'new-branch-name-2'
+ input.native.send_keys :enter
+ wait_for_requests
+ expect(find('.js-project-refs-dropdown')).to have_content 'new-branch-name-2'
+ end
+ end
end
diff --git a/spec/features/projects/services/user_activates_jira_spec.rb b/spec/features/projects/services/user_activates_jira_spec.rb
index 0a86292ae6c..ac78b1dfb1c 100644
--- a/spec/features/projects/services/user_activates_jira_spec.rb
+++ b/spec/features/projects/services/user_activates_jira_spec.rb
@@ -65,7 +65,7 @@ describe 'User activates Jira', :js do
expect(find('.flash-container-page')).to have_content 'Test failed. message'
expect(find('.flash-container-page')).to have_content 'Save anyway'
- find('.flash-alert .flash-action').trigger('click')
+ find('.flash-alert .flash-action').click
wait_for_requests
expect(page).to have_content('JIRA activated.')
diff --git a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
index 95d5e8b14b9..6f057137867 100644
--- a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
@@ -76,7 +76,7 @@ feature 'Setup Mattermost slash commands', :js do
select_element = find('#mattermost_team_id')
selected_option = select_element.find('option[selected]')
- expect(select_element['disabled']).to be(true)
+ expect(select_element['disabled']).to eq("true")
expect(selected_option).to have_content(team_name.to_s)
end
@@ -104,7 +104,7 @@ feature 'Setup Mattermost slash commands', :js do
select_element = find('#mattermost_team_id')
- expect(select_element['disabled']).to be(false)
+ expect(select_element['disabled']).to be_falsey
expect(select_element.all('option').count).to eq(3)
end
@@ -122,7 +122,7 @@ feature 'Setup Mattermost slash commands', :js do
click_link 'Add to Mattermost'
- expect(find('input[type="submit"]')['disabled']).not_to be(true)
+ expect(find('input[type="submit"]')['disabled']).not_to eq("true")
end
it 'disables the submit button if the required fields are not provided', :js do
@@ -132,7 +132,7 @@ feature 'Setup Mattermost slash commands', :js do
fill_in('mattermost_trigger', with: '')
- expect(find('input[type="submit"]')['disabled']).to be(true)
+ expect(find('input[type="submit"]')['disabled']).to eq("true")
end
def stub_teams(count: 0)
diff --git a/spec/features/projects/services/user_activates_packagist_spec.rb b/spec/features/projects/services/user_activates_packagist_spec.rb
new file mode 100644
index 00000000000..b0cc818f093
--- /dev/null
+++ b/spec/features/projects/services/user_activates_packagist_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe 'User activates Packagist' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('Packagist')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('Username', with: 'theUser')
+ fill_in('Token', with: 'verySecret')
+ click_button('Save')
+
+ expect(page).to have_content('Packagist activated.')
+ end
+end
diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/services/user_views_services_spec.rb
index f86591c2633..5c5e8b66642 100644
--- a/spec/features/projects/services/user_views_services_spec.rb
+++ b/spec/features/projects/services/user_views_services_spec.rb
@@ -21,5 +21,6 @@ describe 'User views services' do
expect(page).to have_content('JetBrains TeamCity')
expect(page).to have_content('Asana')
expect(page).to have_content('Irker (IRC gateway)')
+ expect(page).to have_content('Packagist')
end
end
diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb
new file mode 100644
index 00000000000..28954a4fb40
--- /dev/null
+++ b/spec/features/projects/settings/forked_project_settings_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+feature 'Settings for a forked project', :js do
+ include ProjectForksHelper
+ let(:user) { create(:user) }
+ let(:original_project) { create(:project) }
+ let(:forked_project) { fork_project(original_project, user) }
+
+ before do
+ original_project.add_master(user)
+ forked_project.add_master(user)
+ sign_in(user)
+ end
+
+ shared_examples 'project settings for a forked projects' do
+ it 'allows deleting the link to the forked project' do
+ visit edit_project_path(forked_project)
+
+ click_button 'Remove fork relationship'
+
+ wait_for_requests
+
+ fill_in('confirm_name_input', with: forked_project.name)
+ click_button('Confirm')
+
+ expect(page).to have_content('The fork relationship has been removed.')
+ expect(forked_project.reload.forked?).to be_falsy
+ end
+ end
+
+ it_behaves_like 'project settings for a forked projects'
+
+ context 'when the original project is deleted' do
+ before do
+ original_project.destroy!
+ end
+
+ it_behaves_like 'project settings for a forked projects'
+ end
+end
diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb
index d932c4e4d9a..cbdb7973ac8 100644
--- a/spec/features/projects/settings/integration_settings_spec.rb
+++ b/spec/features/projects/settings/integration_settings_spec.rb
@@ -76,7 +76,7 @@ feature 'Integration settings' do
expect(page).to have_content(url)
end
- scenario 'test existing webhook', js: true do
+ scenario 'test existing webhook', :js do
WebMock.stub_request(:post, hook.url)
visit integrations_path
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index b1ec556bf16..ac76c30cc7c 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -21,7 +21,7 @@ feature 'Project settings > Merge Requests', :js do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
- click_on('Save changes')
+ find('input[value="Save changes"]').send_keys(:return)
end
expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
@@ -41,7 +41,7 @@ feature 'Project settings > Merge Requests', :js do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click
- click_on('Save changes')
+ find('input[value="Save changes"]').send_keys(:return)
end
expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
@@ -62,7 +62,7 @@ feature 'Project settings > Merge Requests', :js do
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
- click_on('Save changes')
+ find('input[value="Save changes"]').send_keys(:return)
end
expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index 975d204e75e..eb8e7265dd3 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -8,13 +8,14 @@ feature "Pipelines settings" do
background do
sign_in(user)
project.team << [user, role]
- visit project_pipelines_settings_path(project)
end
context 'for developer' do
given(:role) { :developer }
scenario 'to be disallowed to view' do
+ visit project_settings_ci_cd_path(project)
+
expect(page.status_code).to eq(404)
end
end
@@ -22,7 +23,9 @@ feature "Pipelines settings" do
context 'for master' do
given(:role) { :master }
- scenario 'be allowed to change', js: true do
+ scenario 'be allowed to change' do
+ visit project_settings_ci_cd_path(project)
+
fill_in('Test coverage parsing', with: 'coverage_regex')
click_on 'Save changes'
@@ -32,6 +35,8 @@ feature "Pipelines settings" do
end
scenario 'updates auto_cancel_pending_pipelines' do
+ visit project_settings_ci_cd_path(project)
+
page.check('Auto-cancel redundant, pending pipelines')
click_on 'Save changes'
@@ -42,14 +47,119 @@ feature "Pipelines settings" do
expect(checkbox).to be_checked
end
- scenario 'update auto devops settings' do
- fill_in('project_auto_devops_attributes_domain', with: 'test.com')
- page.choose('project_auto_devops_attributes_enabled_false')
- click_on 'Save changes'
+ describe 'Auto DevOps' do
+ it 'update auto devops settings' do
+ visit project_settings_ci_cd_path(project)
- expect(page.status_code).to eq(200)
- expect(project.auto_devops).to be_present
- expect(project.auto_devops).not_to be_enabled
+ fill_in('project_auto_devops_attributes_domain', with: 'test.com')
+ page.choose('project_auto_devops_attributes_enabled_false')
+ click_on 'Save changes'
+
+ expect(page.status_code).to eq(200)
+ expect(project.auto_devops).to be_present
+ expect(project.auto_devops).not_to be_enabled
+ end
+
+ describe 'Immediately run pipeline checkbox option', :js do
+ context 'when auto devops is set to instance default (enabled)' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ project.create_auto_devops!(enabled: nil)
+ visit project_settings_ci_cd_path(project)
+ end
+
+ it 'does not show checkboxes on page-load' do
+ expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
+ end
+
+ it 'selecting explicit disabled hides all checkboxes' do
+ page.choose('project_auto_devops_attributes_enabled_false')
+
+ expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
+ end
+
+ it 'selecting explicit enabled hides all checkboxes because we are already enabled' do
+ page.choose('project_auto_devops_attributes_enabled_true')
+
+ expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
+ end
+ end
+
+ context 'when auto devops is set to instance default (disabled)' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ project.create_auto_devops!(enabled: nil)
+ visit project_settings_ci_cd_path(project)
+ end
+
+ it 'does not show checkboxes on page-load' do
+ expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
+ end
+
+ it 'selecting explicit disabled hides all checkboxes' do
+ page.choose('project_auto_devops_attributes_enabled_false')
+
+ expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
+ end
+
+ it 'selecting explicit enabled shows a checkbox' do
+ page.choose('project_auto_devops_attributes_enabled_true')
+
+ expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1)
+ end
+ end
+
+ context 'when auto devops is set to explicit disabled' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ project.create_auto_devops!(enabled: false)
+ visit project_settings_ci_cd_path(project)
+ end
+
+ it 'does not show checkboxes on page-load' do
+ expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 2, visible: false)
+ end
+
+ it 'selecting explicit enabled shows a checkbox' do
+ page.choose('project_auto_devops_attributes_enabled_true')
+
+ expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1)
+ end
+
+ it 'selecting instance default (enabled) shows a checkbox' do
+ page.choose('project_auto_devops_attributes_enabled_')
+
+ expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1)
+ end
+ end
+
+ context 'when auto devops is set to explicit enabled' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ project.create_auto_devops!(enabled: true)
+ visit project_settings_ci_cd_path(project)
+ end
+
+ it 'does not have any checkboxes' do
+ expect(page).not_to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper', visible: false)
+ end
+ end
+
+ context 'when master contains a .gitlab-ci.yml file' do
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.repository.create_file(user, '.gitlab-ci.yml', "script: ['test']", message: 'test', branch_name: project.default_branch)
+ stub_application_setting(auto_devops_enabled: true)
+ project.create_auto_devops!(enabled: false)
+ visit project_settings_ci_cd_path(project)
+ end
+
+ it 'does not have any checkboxes' do
+ expect(page).not_to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper', visible: false)
+ end
+ end
+ end
end
end
end
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 15180d4b498..e2a5619c22b 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -23,7 +23,7 @@ feature 'Repository settings' do
context 'for master' do
given(:role) { :master }
- context 'Deploy Keys', js: true do
+ context 'Deploy Keys', :js do
let(:private_deploy_key) { create(:deploy_key, title: 'private_deploy_key', public: false) }
let(:public_deploy_key) { create(:another_deploy_key, title: 'public_deploy_key', public: true) }
let(:new_ssh_key) { attributes_for(:key)[:key] }
@@ -34,7 +34,6 @@ feature 'Repository settings' do
visit project_settings_repository_path(project)
- expect(page.status_code).to eq(200)
expect(page).to have_content('private_deploy_key')
expect(page).to have_content('public_deploy_key')
end
@@ -86,7 +85,7 @@ feature 'Repository settings' do
project.deploy_keys << private_deploy_key
visit project_settings_repository_path(project)
- find('li', text: private_deploy_key.title).click_button('Remove')
+ accept_confirm { find('li', text: private_deploy_key.title).click_button('Remove') }
expect(page).not_to have_content(private_deploy_key.title)
end
diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb
index 37ee6255bd1..1c3b84d0114 100644
--- a/spec/features/projects/settings/visibility_settings_spec.rb
+++ b/spec/features/projects/settings/visibility_settings_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Visibility settings', js: true do
+feature 'Visibility settings', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace, visibility_level: 20) }
diff --git a/spec/features/projects/show_project_spec.rb b/spec/features/projects/show_project_spec.rb
index 1bc6fae9e7f..0b94c9eae5d 100644
--- a/spec/features/projects/show_project_spec.rb
+++ b/spec/features/projects/show_project_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Project show page', feature: true do
+describe 'Project show page', :feature do
context 'when project pending delete' do
let(:project) { create(:project, :empty_repo, pending_delete: true) }
diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb
index 3e79dba3f19..e4215291f99 100644
--- a/spec/features/projects/snippets/create_snippet_spec.rb
+++ b/spec/features/projects/snippets/create_snippet_spec.rb
@@ -10,7 +10,7 @@ feature 'Create Snippet', :js do
fill_in 'project_snippet_title', with: 'My Snippet Title'
fill_in 'project_snippet_description', with: 'My Snippet **Description**'
page.within('.file-editor') do
- find('.ace_editor').native.send_keys('Hello World!')
+ find('.ace_text-input', visible: false).send_keys('Hello World!')
end
end
@@ -59,7 +59,7 @@ feature 'Create Snippet', :js do
fill_form
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
- click_button('Create snippet')
+ find("input[value='Create snippet']").send_keys(:return)
wait_for_requests
expect(page).to have_content('My Snippet Title')
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
new file mode 100644
index 00000000000..156293289dd
--- /dev/null
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+feature 'Multi-file editor new directory', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ set_cookie('new_repo', 'true')
+
+ visit project_tree_path(project, :master)
+
+ wait_for_requests
+ end
+
+ it 'creates directory in current directory' do
+ find('.add-to-tree').click
+
+ click_link('New directory')
+
+ page.within('.popup-dialog') do
+ find('.form-control').set('foldername')
+
+ click_button('Create directory')
+ end
+
+ find('.multi-file-commit-panel-collapse-btn').click
+
+ fill_in('commit-message', with: 'commit message')
+
+ click_button('Commit')
+
+ expect(page).to have_selector('td', text: 'commit message')
+ end
+end
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
new file mode 100644
index 00000000000..8fb8476e631
--- /dev/null
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+feature 'Multi-file editor new file', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ set_cookie('new_repo', 'true')
+
+ visit project_tree_path(project, :master)
+
+ wait_for_requests
+ end
+
+ it 'creates file in current directory' do
+ find('.add-to-tree').click
+
+ click_link('New file')
+
+ page.within('.popup-dialog') do
+ find('.form-control').set('filename')
+
+ click_button('Create file')
+ end
+
+ find('.multi-file-commit-panel-collapse-btn').click
+
+ fill_in('commit-message', with: 'commit message')
+
+ click_button('Commit')
+
+ expect(page).to have_selector('td', text: 'commit message')
+ end
+end
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
new file mode 100644
index 00000000000..d4e57d1ecfa
--- /dev/null
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+feature 'Multi-file editor upload file', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') }
+ let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ set_cookie('new_repo', 'true')
+
+ visit project_tree_path(project, :master)
+
+ wait_for_requests
+ end
+
+ it 'uploads text file' do
+ find('.add-to-tree').click
+
+ # make the field visible so capybara can use it
+ execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
+ attach_file('file-upload', txt_file)
+
+ find('.add-to-tree').click
+
+ expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt')
+ expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
+ end
+
+ it 'uploads image file' do
+ find('.add-to-tree').click
+
+ # make the field visible so capybara can use it
+ execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
+ attach_file('file-upload', img_file)
+
+ find('.add-to-tree').click
+
+ expect(page).to have_selector('.multi-file-tab', text: 'dk.png')
+ expect(page).not_to have_selector('.monaco-editor')
+ expect(page).to have_content('The source could not be displayed for this temporary file.')
+ end
+end
diff --git a/spec/features/projects/user_browses_files_spec.rb b/spec/features/projects/user_browses_files_spec.rb
index b7a0b72db50..f5e4d7f5130 100644
--- a/spec/features/projects/user_browses_files_spec.rb
+++ b/spec/features/projects/user_browses_files_spec.rb
@@ -76,7 +76,7 @@ describe 'User browses files' do
expect(page).to have_content('LICENSE')
end
- it 'shows files from a repository with apostroph in its name', js: true do
+ it 'shows files from a repository with apostroph in its name', :js do
first('.js-project-refs-dropdown').click
page.within('.project-refs-form') do
@@ -91,7 +91,7 @@ describe 'User browses files' do
expect(page).not_to have_content('Loading commit data...')
end
- it 'shows the code with a leading dot in the directory', js: true do
+ it 'shows the code with a leading dot in the directory', :js do
first('.js-project-refs-dropdown').click
page.within('.project-refs-form') do
@@ -117,7 +117,7 @@ describe 'User browses files' do
click_link('.gitignore')
end
- it 'shows a file content', js: true do
+ it 'shows a file content', :js do
wait_for_requests
expect(page).to have_content('*.rbc')
end
@@ -168,17 +168,18 @@ describe 'User browses files' do
visit(tree_path_root_ref)
end
- it 'shows a preview of a file content', js: true do
+ it 'shows a preview of a file content', :js do
find('.add-to-tree').click
click_link('Upload file')
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg'))
page.within('#modal-upload-blob') do
fill_in(:commit_message, with: 'New commit message')
+ fill_in(:branch_name, with: 'new_branch_name', visible: true)
+ click_button('Upload file')
end
- fill_in(:branch_name, with: 'new_branch_name', visible: true)
- click_button('Upload file')
+ wait_for_all_requests
visit(project_blob_path(project, 'new_branch_name/logo_sample.svg'))
diff --git a/spec/features/projects/user_creates_directory_spec.rb b/spec/features/projects/user_creates_directory_spec.rb
index e8f2e4813c5..052cb3188c5 100644
--- a/spec/features/projects/user_creates_directory_spec.rb
+++ b/spec/features/projects/user_creates_directory_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'User creates a directory', js: true do
+feature 'User creates a directory', :js do
let(:fork_message) do
"You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
diff --git a/spec/features/projects/user_creates_files_spec.rb b/spec/features/projects/user_creates_files_spec.rb
index 51d918bc85d..d84b91ddc32 100644
--- a/spec/features/projects/user_creates_files_spec.rb
+++ b/spec/features/projects/user_creates_files_spec.rb
@@ -59,7 +59,8 @@ describe 'User creates files' do
expect(page).to have_selector('.file-editor')
end
- it 'creates and commit a new file', js: true do
+ it 'creates and commit a new file', :js do
+ find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
@@ -74,7 +75,8 @@ describe 'User creates files' do
expect(page).to have_content('*.rbca')
end
- it 'creates and commit a new file with new lines at the end of file', js: true do
+ it 'creates and commit a new file with new lines at the end of file', :js do
+ find('#editor')
execute_script('ace.edit("editor").setValue("Sample\n\n\n")')
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
@@ -86,14 +88,16 @@ describe 'User creates files' do
find('.js-edit-blob').click
+ find('#editor')
expect(evaluate_script('ace.edit("editor").getValue()')).to eq("Sample\n\n\n")
end
- it 'creates and commit a new file with a directory name', js: true do
+ it 'creates and commit a new file with a directory name', :js do
fill_in(:file_name, with: 'foo/bar/baz.txt')
expect(page).to have_selector('.file-editor')
+ find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
@@ -105,9 +109,10 @@ describe 'User creates files' do
expect(page).to have_content('*.rbca')
end
- it 'creates and commit a new file specifying a new branch', js: true do
+ it 'creates and commit a new file specifying a new branch', :js do
expect(page).to have_selector('.file-editor')
+ find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true)
@@ -130,12 +135,13 @@ describe 'User creates files' do
visit(project2_tree_path_root_ref)
end
- it 'creates and commit new file in forked project', js: true do
+ it 'creates and commit new file in forked project', :js do
find('.add-to-tree').click
click_link('New file')
expect(page).to have_selector('.file-editor')
+ find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md')
diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb
index 1c3791f63ac..f95469ad070 100644
--- a/spec/features/projects/user_creates_project_spec.rb
+++ b/spec/features/projects/user_creates_project_spec.rb
@@ -1,15 +1,16 @@
require 'spec_helper'
-feature 'User creates a project', js: true do
+feature 'User creates a project', :js do
let(:user) { create(:user) }
before do
sign_in(user)
create(:personal_key, user: user)
- visit(new_project_path)
end
it 'creates a new project' do
+ visit(new_project_path)
+
fill_in(:project_path, with: 'Empty')
page.within('#content-body') do
@@ -24,4 +25,32 @@ feature 'User creates a project', js: true do
expect(page).to have_content('git remote')
expect(page).to have_content(project.url_to_repo)
end
+
+ context 'in a subgroup they do not own', :nested_groups do
+ let(:parent) { create(:group) }
+ let!(:subgroup) { create(:group, parent: parent) }
+
+ before do
+ parent.add_owner(user)
+ end
+
+ it 'creates a new project' do
+ visit(new_project_path)
+
+ fill_in :project_path, with: 'a-subgroup-project'
+
+ page.find('.js-select-namespace').click
+ page.find("div[role='option']", text: subgroup.full_path).click
+
+ page.within('#content-body') do
+ click_button('Create project')
+ end
+
+ expect(page).to have_content("Project 'a-subgroup-project' was successfully created")
+
+ project = Project.last
+
+ expect(project.namespace).to eq(subgroup)
+ end
+ end
end
diff --git a/spec/features/projects/user_deletes_files_spec.rb b/spec/features/projects/user_deletes_files_spec.rb
index 7f48a69d9b7..9e4e92ec076 100644
--- a/spec/features/projects/user_deletes_files_spec.rb
+++ b/spec/features/projects/user_deletes_files_spec.rb
@@ -21,7 +21,7 @@ describe 'User deletes files' do
visit(project_tree_path_root_ref)
end
- it 'deletes the file', js: true do
+ it 'deletes the file', :js do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
@@ -41,7 +41,7 @@ describe 'User deletes files' do
visit(project2_tree_path_root_ref)
end
- it 'deletes the file in a forked project', js: true do
+ it 'deletes the file in a forked project', :js do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb
index 2798041fa0c..d26ee653415 100644
--- a/spec/features/projects/user_edits_files_spec.rb
+++ b/spec/features/projects/user_edits_files_spec.rb
@@ -18,11 +18,12 @@ describe 'User edits files' do
visit(project_tree_path_root_ref)
end
- it 'inserts a content of a file', js: true do
+ it 'inserts a content of a file', :js do
click_link('.gitignore')
find('.js-edit-blob').click
find('.file-editor', match: :first)
+ find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
@@ -35,11 +36,12 @@ describe 'User edits files' do
expect(page).not_to have_link('edit')
end
- it 'commits an edited file', js: true do
+ it 'commits an edited file', :js do
click_link('.gitignore')
find('.js-edit-blob').click
find('.file-editor', match: :first)
+ find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
@@ -51,12 +53,13 @@ describe 'User edits files' do
expect(page).to have_content('*.rbca')
end
- it 'commits an edited file to a new branch', js: true do
+ it 'commits an edited file to a new branch', :js do
click_link('.gitignore')
find('.js-edit-blob').click
find('.file-editor', match: :first)
+ find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
fill_in(:branch_name, with: 'new_branch_name', visible: true)
@@ -69,11 +72,12 @@ describe 'User edits files' do
expect(page).to have_content('*.rbca')
end
- it 'shows the diff of an edited file', js: true do
+ it 'shows the diff of an edited file', :js do
click_link('.gitignore')
find('.js-edit-blob').click
find('.file-editor', match: :first)
+ find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
click_link('Preview changes')
@@ -87,7 +91,7 @@ describe 'User edits files' do
visit(project2_tree_path_root_ref)
end
- it 'inserts a content of a file in a forked project', js: true do
+ it 'inserts a content of a file in a forked project', :js do
click_link('.gitignore')
find('.js-edit-blob').click
@@ -103,12 +107,13 @@ describe 'User edits files' do
find('.file-editor', match: :first)
+ find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
end
- it 'commits an edited file in a forked project', js: true do
+ it 'commits an edited file in a forked project', :js do
click_link('.gitignore')
find('.js-edit-blob').click
@@ -119,6 +124,7 @@ describe 'User edits files' do
find('.file-editor', match: :first)
+ find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
@@ -145,6 +151,7 @@ describe 'User edits files' do
expect(page).not_to have_link('Fork')
expect(page).not_to have_button('Cancel')
+ find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'Another commit', visible: true)
click_button('Commit changes')
diff --git a/spec/features/projects/user_interacts_with_stars_spec.rb b/spec/features/projects/user_interacts_with_stars_spec.rb
index 0ac3f8181fa..d9d2e0ab171 100644
--- a/spec/features/projects/user_interacts_with_stars_spec.rb
+++ b/spec/features/projects/user_interacts_with_stars_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'User interacts with project stars' do
let(:project) { create(:project, :public, :repository) }
- context 'when user is signed in', js: true do
+ context 'when user is signed in', :js do
let(:user) { create(:user) }
before do
diff --git a/spec/features/projects/user_replaces_files_spec.rb b/spec/features/projects/user_replaces_files_spec.rb
index a9628198d5b..245b6aa285b 100644
--- a/spec/features/projects/user_replaces_files_spec.rb
+++ b/spec/features/projects/user_replaces_files_spec.rb
@@ -23,7 +23,7 @@ describe 'User replaces files' do
visit(project_tree_path_root_ref)
end
- it 'replaces an existed file with a new one', js: true do
+ it 'replaces an existed file with a new one', :js do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
@@ -49,7 +49,7 @@ describe 'User replaces files' do
visit(project2_tree_path_root_ref)
end
- it 'replaces an existed file with a new one in a forked project', js: true do
+ it 'replaces an existed file with a new one in a forked project', :js do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
diff --git a/spec/features/projects/user_transfers_a_project_spec.rb b/spec/features/projects/user_transfers_a_project_spec.rb
new file mode 100644
index 00000000000..78f72b644ff
--- /dev/null
+++ b/spec/features/projects/user_transfers_a_project_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+feature 'User transfers a project', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+
+ before do
+ sign_in user
+ end
+
+ def transfer_project(project, group)
+ visit edit_project_path(project)
+
+ page.within('.js-project-transfer-form') do
+ page.find('.select2-container').click
+ end
+
+ page.find("div[role='option']", text: group.full_name).click
+
+ click_button('Transfer project')
+
+ fill_in 'confirm_name_input', with: project.name
+
+ click_button 'Confirm'
+
+ wait_for_requests
+ end
+
+ it 'allows transferring a project to a subgroup of a namespace' do
+ group = create(:group)
+ group.add_owner(user)
+
+ transfer_project(project, group)
+
+ expect(project.reload.namespace).to eq(group)
+ end
+
+ context 'when nested groups are available', :nested_groups do
+ it 'allows transferring a project to a subgroup' do
+ parent = create(:group)
+ parent.add_owner(user)
+ subgroup = create(:group, parent: parent)
+
+ transfer_project(project, subgroup)
+
+ expect(project.reload.namespace).to eq(subgroup)
+ end
+ end
+end
diff --git a/spec/features/projects/user_uploads_files_spec.rb b/spec/features/projects/user_uploads_files_spec.rb
index 8014c299980..ae51901adc6 100644
--- a/spec/features/projects/user_uploads_files_spec.rb
+++ b/spec/features/projects/user_uploads_files_spec.rb
@@ -23,7 +23,7 @@ describe 'User uploads files' do
visit(project_tree_path_root_ref)
end
- it 'uploads and commit a new file', js: true do
+ it 'uploads and commit a new file', :js do
find('.add-to-tree').click
click_link('Upload file')
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
@@ -54,7 +54,7 @@ describe 'User uploads files' do
visit(project2_tree_path_root_ref)
end
- it 'uploads and commit a new file to a forked project', js: true do
+ it 'uploads and commit a new file to a forked project', :js do
find('.add-to-tree').click
click_link('Upload file')
diff --git a/spec/features/projects/user_views_details_spec.rb b/spec/features/projects/user_views_details_spec.rb
new file mode 100644
index 00000000000..ffc063654cd
--- /dev/null
+++ b/spec/features/projects/user_views_details_spec.rb
@@ -0,0 +1,151 @@
+require 'spec_helper'
+
+describe 'User views details' do
+ set(:user) { create(:user) }
+
+ shared_examples_for 'redirects to the sign in page' do
+ it 'redirects to the sign in page' do
+ expect(current_path).to eq(new_user_session_path)
+ end
+ end
+
+ shared_examples_for 'shows details of empty project' do
+ let(:user_has_ssh_key) { false }
+
+ it 'shows details' do
+ expect(page).not_to have_content('Git global setup')
+
+ page.all(:css, '.git-empty .clone').each do |element|
+ expect(element.text).to include(project.http_url_to_repo)
+ end
+
+ expect(page).to have_field('project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
+ end
+ end
+
+ shared_examples_for 'shows details of non empty project' do
+ let(:user_has_ssh_key) { false }
+
+ it 'shows details' do
+ page.within('.breadcrumbs .breadcrumb-item-text') do
+ expect(page).to have_content(project.title)
+ end
+
+ expect(page).to have_field('project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
+ end
+ end
+
+ context 'when project is public' do
+ context 'when project is empty' do
+ set(:project) { create(:project_empty_repo, :public) }
+
+ context 'when not signed in' do
+ before do
+ visit(project_path(project))
+ end
+
+ include_examples 'shows details of empty project'
+ end
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when user does not have ssh keys' do
+ before do
+ visit(project_path(project))
+ end
+
+ include_examples 'shows details of empty project'
+ end
+
+ context 'when user has ssh keys' do
+ before do
+ create(:personal_key, user: user)
+
+ visit(project_path(project))
+ end
+
+ include_examples 'shows details of empty project' do
+ let(:user_has_ssh_key) { true }
+ end
+ end
+ end
+ end
+
+ context 'when project is not empty' do
+ set(:project) { create(:project, :public, :repository) }
+
+ before do
+ visit(project_path(project))
+ end
+
+ context 'when not signed in' do
+ before do
+ allow(Gitlab.config.gitlab).to receive(:host).and_return('www.example.com')
+ end
+
+ include_examples 'shows details of non empty project'
+ end
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when user does not have ssh keys' do
+ before do
+ visit(project_path(project))
+ end
+
+ include_examples 'shows details of non empty project'
+ end
+
+ context 'when user has ssh keys' do
+ before do
+ create(:personal_key, user: user)
+
+ visit(project_path(project))
+ end
+
+ include_examples 'shows details of non empty project' do
+ let(:user_has_ssh_key) { true }
+ end
+ end
+ end
+ end
+ end
+
+ context 'when project is internal' do
+ set(:project) { create(:project, :internal, :repository) }
+
+ context 'when not signed in' do
+ before do
+ visit(project_path(project))
+ end
+
+ include_examples 'redirects to the sign in page'
+ end
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+
+ visit(project_path(project))
+ end
+
+ include_examples 'shows details of non empty project'
+ end
+ end
+
+ context 'when project is private' do
+ set(:project) { create(:project, :private) }
+
+ before do
+ visit(project_path(project))
+ end
+
+ include_examples 'redirects to the sign in page'
+ end
+end
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index 2a316a0d0db..7f547a4ca1f 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'View on environment', js: true do
+describe 'View on environment', :js do
let(:branch_name) { 'feature' }
let(:file_path) { 'files/ruby/feature.rb' }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 9a4ccf3c54d..337baaf4dcd 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Wiki > User previews markdown changes', js: true do
+feature 'Projects > Wiki > User previews markdown changes', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:wiki_content) do
@@ -14,18 +14,17 @@ feature 'Projects > Wiki > User previews markdown changes', js: true do
background do
project.team << [user, :master]
- WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
sign_in(user)
visit project_path(project)
- find('.shortcuts-wiki').trigger('click')
+ find('.shortcuts-wiki').click
end
context "while creating a new wiki page" do
context "when there are no spaces or hyphens in the page name" do
it "rewrites relative links as expected" do
- find('.add-new-wiki').trigger('click')
+ find('.add-new-wiki').click
page.within '#modal-new-wiki' do
fill_in :new_wiki_path, with: 'a/b/c/d'
click_button 'Create page'
@@ -92,7 +91,7 @@ feature 'Projects > Wiki > User previews markdown changes', js: true do
context "while editing a wiki page" do
def create_wiki_page(path)
- find('.add-new-wiki').trigger('click')
+ find('.add-new-wiki').click
page.within '#modal-new-wiki' do
fill_in :new_wiki_path, with: path
diff --git a/spec/features/projects/wiki/shortcuts_spec.rb b/spec/features/projects/wiki/shortcuts_spec.rb
index eaff5f876b6..f70d1e710dd 100644
--- a/spec/features/projects/wiki/shortcuts_spec.rb
+++ b/spec/features/projects/wiki/shortcuts_spec.rb
@@ -3,9 +3,7 @@ require 'spec_helper'
feature 'Wiki shortcuts', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
- let(:wiki_page) do
- WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
- end
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: 'Home page' }) }
before do
sign_in(user)
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index e72b7dc0dd5..4a9d1cb87e1 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -77,14 +77,14 @@ describe 'User creates wiki page' do
[stem]
++++
- \sqrt{4} = 2
+ \\sqrt{4} = 2
++++
another part
[latexmath]
++++
- \beta_x \gamma
+ \\beta_x \\gamma
++++
stem:[2+2] is 4
diff --git a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
index 9a92622ba2b..37a118c34ab 100644
--- a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
@@ -3,14 +3,7 @@ require 'spec_helper'
describe 'Projects > Wiki > User views Git access wiki page' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
- let(:wiki_page) do
- WikiPages::CreateService.new(
- project,
- user,
- title: 'home',
- content: '[some link](other-page)'
- ).execute
- end
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) }
before do
sign_in(user)
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
index cf9fe4c1ad1..ebb3bd044c1 100644
--- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -18,12 +18,7 @@ describe 'Projects > Wiki > User views wiki in project page' do
context 'when wiki homepage contains a link' do
before do
- WikiPages::CreateService.new(
- project,
- user,
- title: 'home',
- content: '[some link](other-page)'
- ).execute
+ create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' })
end
it 'displays the correct URL for the link' do
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index 470391dc66b..ff325aeadd3 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -81,10 +81,15 @@ describe 'User views a wiki page' do
end
it 'shows a file stored in a page' do
- file = Gollum::File.new(project.wiki)
+ gollum_file_double = double('Gollum::File',
+ mime_type: 'image/jpeg',
+ name: 'images/image.jpg',
+ path: 'images/image.jpg',
+ raw_data: '')
+ wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double)
- allow_any_instance_of(Gollum::Wiki).to receive(:file).with('image.jpg', 'master').and_return(file)
- allow_any_instance_of(Gollum::File).to receive(:mime_type).and_return('image/jpeg')
+ allow(wiki_file).to receive(:mime_type).and_return('image/jpeg')
+ allow_any_instance_of(ProjectWiki).to receive(:find_file).with('image.jpg', nil).and_return(wiki_file)
expect(page).to have_xpath('//img[@data-src="image.jpg"]')
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg")
@@ -133,7 +138,7 @@ describe 'User views a wiki page' do
it 'opens a default wiki page', :js do
visit(project_path(project))
- find('.shortcuts-wiki').trigger('click')
+ find('.shortcuts-wiki').click
expect(page).to have_content('Home · Create Page')
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 4b2c54d54b5..63e6051b571 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -12,8 +12,9 @@ feature 'Project' do
visit new_project_path
end
- it "allows creation from templates" do
- page.choose(template.name)
+ it "allows creation from templates", :js do
+ find('#create-from-template-tab').click
+ find("label[for=#{template.name}]").click
fill_in("project_path", with: template.name)
page.within '#content-body' do
@@ -57,7 +58,7 @@ feature 'Project' do
end
end
- describe 'remove forked relationship', js: true do
+ describe 'remove forked relationship', :js do
let(:user) { create(:user) }
let(:project) { fork_project(create(:project, :public), user, namespace_id: user.namespace) }
@@ -126,7 +127,7 @@ feature 'Project' do
end
end
- describe 'removal', js: true do
+ describe 'removal', :js do
let(:user) { create(:user, username: 'test', name: 'test') }
let(:project) { create(:project, namespace: user.namespace, name: 'project1') }
@@ -138,7 +139,7 @@ feature 'Project' do
it 'removes a project' do
expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1)
- expect(page).to have_content "Project 'test / project1' will be deleted."
+ expect(page).to have_content "Project 'test / project1' is in the process of being deleted."
expect(Project.all.count).to be_zero
expect(project.issues).to be_empty
expect(project.merge_requests).to be_empty
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 2ab1eda90f1..a4084818284 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -48,7 +48,7 @@ feature 'Protected Branches', :js do
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
- page.find('[data-target="#modal-delete-branch"]').trigger(:click)
+ page.find('[data-target="#modal-delete-branch"]').click
expect(page).to have_css('.js-delete-branch[disabled]')
fill_in 'delete_branch_input', with: 'fix'
@@ -67,9 +67,9 @@ feature 'Protected Branches', :js do
form = '.js-new-protected-branch'
within form do
- find(".js-allowed-to-merge").trigger('click')
+ find(".js-allowed-to-merge").click
click_link 'No one'
- find(".js-allowed-to-push").trigger('click')
+ find(".js-allowed-to-push").click
click_link 'Developers + Masters'
end
@@ -171,7 +171,7 @@ feature 'Protected Branches', :js do
end
def set_protected_branch_name(branch_name)
- find(".js-protected-branch-select").trigger('click')
+ find(".js-protected-branch-select").click
find(".dropdown-input-field").set(branch_name)
click_on("Create wildcard #{branch_name}")
end
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index 8abd4403065..8cc6f17b8d9 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Protected Tags', js: true do
+feature 'Protected Tags', :js do
let(:user) { create(:user, :admin) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb
index b1f51959d54..74890c86047 100644
--- a/spec/features/raven_js_spec.rb
+++ b/spec/features/raven_js_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'RavenJS', :js do
+feature 'RavenJS' do
let(:raven_path) { '/raven.bundle.js' }
it 'should not load raven if sentry is disabled' do
@@ -18,6 +18,8 @@ feature 'RavenJS', :js do
end
def has_requested_raven
- page.driver.network_traffic.one? {|request| request.url.end_with?(raven_path)}
+ page.all('script', visible: false).one? do |elm|
+ elm[:src] =~ /#{raven_path}$/
+ end
end
end
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 0ed797a62ea..77212fb105b 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -32,14 +32,14 @@ describe 'User searches for code' do
include_examples 'top right search form'
it 'finds code' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: 'rspec')
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.results') do
expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions')
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
index 630a81b1c5e..ef9553f2a91 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -18,7 +18,7 @@ describe 'User searches for issues', :js do
it 'finds an issue' do
fill_in('dashboard_search', with: issue1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Issues')
@@ -31,14 +31,14 @@ describe 'User searches for issues', :js do
context 'when on a project page' do
it 'finds an issue' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: issue1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Issues')
@@ -62,7 +62,7 @@ describe 'User searches for issues', :js do
it 'finds an issue' do
fill_in('dashboard_search', with: issue1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Issues')
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
index 116256682f4..3b6739aecbd 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -17,7 +17,7 @@ describe 'User searches for merge requests', :js do
it 'finds a merge request' do
fill_in('dashboard_search', with: merge_request1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Merge requests')
@@ -30,14 +30,14 @@ describe 'User searches for merge requests', :js do
context 'when on a project page' do
it 'finds a merge request' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: merge_request1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Merge requests')
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index 4fa9fe9ce8c..6e197aee498 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -17,7 +17,7 @@ describe 'User searches for milestones', :js do
it 'finds a milestone' do
fill_in('dashboard_search', with: milestone1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Milestones')
@@ -30,14 +30,14 @@ describe 'User searches for milestones', :js do
context 'when on a project page' do
it 'finds a milestone' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: milestone1.title)
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Milestones')
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 1ea56479ecc..00af625dc86 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -15,14 +15,14 @@ describe 'User searches for wiki pages', :js do
include_examples 'top right search form'
it 'finds a page' do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.name_with_namespace)
end
fill_in('dashboard_search', with: 'content')
- find('.btn-search').trigger('click')
+ find('.btn-search').click
page.within('.search-filter') do
click_link('Wiki')
diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb
index 95f3eb5e805..aa883c964d2 100644
--- a/spec/features/search/user_uses_search_filters_spec.rb
+++ b/spec/features/search/user_uses_search_filters_spec.rb
@@ -16,7 +16,7 @@ describe 'User uses search filters', :js do
context' when filtering by group' do
it 'shows group projects' do
- find('.js-search-group-dropdown').trigger('click')
+ find('.js-search-group-dropdown').click
wait_for_requests
@@ -27,7 +27,7 @@ describe 'User uses search filters', :js do
expect(find('.js-search-group-dropdown')).to have_content(group.name)
page.within('.project-filter') do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
wait_for_requests
@@ -39,7 +39,7 @@ describe 'User uses search filters', :js do
context' when filtering by project' do
it 'shows a project' do
page.within('.project-filter') do
- find('.js-search-project-dropdown').trigger('click')
+ find('.js-search-project-dropdown').click
wait_for_requests
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index d70cf1527e7..a7928857b7d 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -181,6 +181,21 @@ describe "Internal Project Access" do
it { is_expected.to be_denied_for(:visitor) }
end
+ describe "GET /:project_path/issues/:id/edit" do
+ let(:issue) { create(:issue, project: project) }
+ subject { edit_project_issue_path(project, issue) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index ea130606545..a4396b20afd 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -181,6 +181,21 @@ describe "Private Project Access" do
it { is_expected.to be_denied_for(:visitor) }
end
+ describe "GET /:project_path/issues/:id/edit" do
+ let(:issue) { create(:issue, project: project) }
+ subject { edit_project_issue_path(project, issue) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index d15f5af66c9..fccdeb0e5b7 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -394,6 +394,21 @@ describe "Public Project Access" do
it { is_expected.to be_allowed_for(:visitor) }
end
+ describe "GET /:project_path/issues/:id/edit" do
+ let(:issue) { create(:issue, project: project) }
+ subject { edit_project_issue_path(project, issue) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
diff --git a/spec/features/snippets/internal_snippet_spec.rb b/spec/features/snippets/internal_snippet_spec.rb
index 3a229612235..3a2768c424f 100644
--- a/spec/features/snippets/internal_snippet_spec.rb
+++ b/spec/features/snippets/internal_snippet_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Internal Snippets', js: true do
+feature 'Internal Snippets', :js do
let(:internal_snippet) { create(:personal_snippet, :internal) }
describe 'normal user' do
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
index bf79974b8c6..269351e55c9 100644
--- a/spec/features/snippets/notes_on_personal_snippets_spec.rb
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -74,24 +74,21 @@ describe 'Comments on personal snippets', :js do
it 'should not have autocomplete' do
wait_for_requests
- request_count_before = page.driver.network_traffic.count
find('#note_note').native.send_keys('')
fill_in 'note[note]', with: '@'
wait_for_requests
- request_count_after = page.driver.network_traffic.count
# This selector probably won't be in place even if autocomplete was enabled
# but we want to make sure
expect(page).not_to have_selector('.atwho-view')
- expect(request_count_before).to eq(request_count_after)
end
end
context 'when editing a note' do
it 'changes the text' do
- find('.js-note-edit').trigger('click')
+ find('.js-note-edit').click
page.within('.current-note-edit-form') do
fill_in 'note[note]', with: 'new content'
@@ -113,7 +110,7 @@ describe 'Comments on personal snippets', :js do
open_more_actions_dropdown(snippet_notes[0])
page.within("#notes-list li#note_#{snippet_notes[0].id}") do
- click_on 'Delete comment'
+ accept_confirm { click_on 'Delete comment' }
end
wait_for_requests
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index d732383a1e1..941765b7578 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -14,7 +14,7 @@ feature 'User creates snippet', :js do
fill_in 'personal_snippet_title', with: 'My Snippet Title'
fill_in 'personal_snippet_description', with: 'My Snippet **Description**'
page.within('.file-editor') do
- find('.ace_editor').native.send_keys 'Hello World!'
+ find('.ace_text-input', visible: false).send_keys 'Hello World!'
end
end
@@ -43,8 +43,8 @@ feature 'User creates snippet', :js do
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/temp/\h{32}/banana_sample\.gif\z})
- visit(link)
- expect(page.status_code).to eq(200)
+ reqs = inspect_requests { visit(link) }
+ expect(reqs.first.status_code).to eq(200)
end
end
@@ -61,8 +61,8 @@ feature 'User creates snippet', :js do
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
- visit(link)
- expect(page.status_code).to eq(200)
+ reqs = inspect_requests { visit(link) }
+ expect(reqs.first.status_code).to eq(200)
end
scenario 'validation fails for the first time' do
@@ -86,15 +86,15 @@ feature 'User creates snippet', :js do
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
- visit(link)
- expect(page.status_code).to eq(200)
+ reqs = inspect_requests { visit(link) }
+ expect(reqs.first.status_code).to eq(200)
end
scenario 'Authenticated user creates a snippet with + in filename' do
fill_in 'personal_snippet_title', with: 'My Snippet Title'
page.within('.file-editor') do
find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name'
- find('.ace_editor').native.send_keys 'Hello World!'
+ find('.ace_text-input', visible: false).send_keys 'Hello World!'
end
click_button 'Create snippet'
diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb
index 39d79a3327b..1f8bd8d681e 100644
--- a/spec/features/tags/master_creates_tag_spec.rb
+++ b/spec/features/tags/master_creates_tag_spec.rb
@@ -55,7 +55,7 @@ feature 'Master creates tag' do
end
end
- scenario 'opens dropdown for ref', js: true do
+ scenario 'opens dropdown for ref', :js do
click_link 'New tag'
ref_row = find('.form-group:nth-of-type(2) .col-sm-10')
page.within ref_row do
@@ -63,7 +63,7 @@ feature 'Master creates tag' do
expect(ref_input.value).to eq 'master'
expect(find('.dropdown-toggle-text')).to have_content 'master'
- find('.js-branch-select').trigger('click')
+ find('.js-branch-select').click
expect(find('.dropdown-menu')).to have_content 'empty-branch'
end
diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
index 80750c904b5..dfda664d673 100644
--- a/spec/features/tags/master_deletes_tag_spec.rb
+++ b/spec/features/tags/master_deletes_tag_spec.rb
@@ -10,7 +10,7 @@ feature 'Master deletes tag' do
visit project_tags_path(project)
end
- context 'from the tags list page', js: true do
+ context 'from the tags list page', :js do
scenario 'deletes the tag' do
expect(page).to have_content 'v1.1.0'
@@ -34,7 +34,7 @@ feature 'Master deletes tag' do
end
end
- context 'when pre-receive hook fails', js: true do
+ context 'when pre-receive hook fails', :js do
context 'when Gitaly operation_user_delete_tag feature is enabled' do
before do
allow_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rm_tag)
@@ -48,7 +48,7 @@ feature 'Master deletes tag' do
end
end
- context 'when Gitaly operation_user_delete_tag feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly operation_user_delete_tag feature is disabled', :skip_gitaly_mock do
before do
allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
.and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags')
@@ -64,7 +64,7 @@ feature 'Master deletes tag' do
def delete_first_tag
page.within('.content') do
- first('.btn-remove').click
+ accept_confirm { first('.btn-remove').click }
end
end
end
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index aeb0534b733..2dc3c5e3927 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Task Lists' do
include Warden::Test::Helpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -63,7 +63,7 @@ feature 'Task Lists' do
end
describe 'for Issues' do
- describe 'multiple tasks', js: true do
+ describe 'multiple tasks', :js do
let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
it 'renders' do
@@ -103,7 +103,7 @@ feature 'Task Lists' do
end
end
- describe 'single incomplete task', js: true do
+ describe 'single incomplete task', :js do
let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) }
it 'renders' do
@@ -122,7 +122,7 @@ feature 'Task Lists' do
end
end
- describe 'single complete task', js: true do
+ describe 'single complete task', :js do
let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) }
it 'renders' do
@@ -141,7 +141,7 @@ feature 'Task Lists' do
end
end
- describe 'nested tasks', js: true do
+ describe 'nested tasks', :js do
let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) }
before do
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 47664de469a..bc472e74997 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Triggers', js: true do
+feature 'Triggers', :js do
let(:trigger_title) { 'trigger desc' }
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -45,7 +45,7 @@ feature 'Triggers', js: true do
visit project_settings_ci_cd_path(@project)
# See if edit page has correct descrption
- find('a[title="Edit"]').click
+ find('a[title="Edit"]').send_keys(:return)
expect(page.find('#trigger_description').value).to have_content 'trigger desc'
end
@@ -54,7 +54,7 @@ feature 'Triggers', js: true do
visit project_settings_ci_cd_path(@project)
# See if edit page opens, then fill in new description and save
- find('a[title="Edit"]').click
+ find('a[title="Edit"]').send_keys(:return)
fill_in 'trigger_description', with: new_trigger_title
click_button 'Save trigger'
@@ -70,7 +70,7 @@ feature 'Triggers', js: true do
visit project_settings_ci_cd_path(@project)
# See if the trigger can be edited and description is blank
- find('a[title="Edit"]').click
+ find('a[title="Edit"]').send_keys(:return)
expect(page.find('#trigger_description').value).to have_content ''
# See if trigger can be updated with description and saved successfully
@@ -94,12 +94,13 @@ feature 'Triggers', js: true do
scenario 'take trigger ownership' do
# See if "Take ownership" on trigger works post trigger creation
- find('a.btn-trigger-take-ownership').click
page.accept_confirm do
- expect(page.find('.flash-notice')).to have_content 'Trigger was re-assigned.'
- expect(page.find('.triggers-list')).to have_content trigger_title
- expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
+ first(:link, "Take ownership").send_keys(:return)
end
+
+ expect(page.find('.flash-notice')).to have_content 'Trigger was re-assigned.'
+ expect(page.find('.triggers-list')).to have_content trigger_title
+ expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
end
end
@@ -116,11 +117,12 @@ feature 'Triggers', js: true do
scenario 'revoke trigger' do
# See if "Revoke" on trigger works post trigger creation
- find('a.btn-trigger-revoke').click
page.accept_confirm do
- expect(page.find('.flash-notice')).to have_content 'Trigger removed'
- expect(page).to have_selector('p.settings-message.text-center.append-bottom-default')
+ find('a.btn-trigger-revoke').send_keys(:return)
end
+
+ expect(page.find('.flash-notice')).to have_content 'Trigger removed'
+ expect(page).to have_selector('p.settings-message.text-center.append-bottom-default')
end
end
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index f3662cb184f..c9afef2a8de 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -79,7 +79,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
first_u2f_device = register_u2f_device
second_u2f_device = register_u2f_device(name: 'My other device')
- click_on "Delete", match: :first
+ accept_confirm { click_on "Delete", match: :first }
expect(page).to have_content('Successfully deleted')
expect(page.body).not_to match(first_u2f_device.name)
@@ -162,7 +162,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
@u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_css('.sign-out-link', visible: false)
end
end
@@ -174,23 +173,10 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
@u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_css('.sign-out-link', visible: false)
end
end
- it 'persists remember_me value via hidden field' do
- gitlab_sign_in(user, remember: true)
-
- @u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
-
- within 'div#js-authenticate-u2f' do
- field = first('input#user_remember_me', visible: false)
- expect(field.value).to eq '1'
- end
- end
-
describe "when a given U2F device has already been registered by another user" do
describe "but not the current user" do
it "does not allow logging in with that particular device" do
@@ -205,7 +191,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
# Try authenticating user with the old U2F device
gitlab_sign_in(current_user)
@u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_content('Authentication via U2F device failed')
end
end
@@ -223,7 +208,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
# Try authenticating user with the same U2F device
gitlab_sign_in(current_user)
@u2f_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_css('.sign-out-link', visible: false)
end
@@ -235,7 +219,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
unregistered_device = FakeU2fDevice.new(page, 'My device')
gitlab_sign_in(user)
unregistered_device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_content('Authentication via U2F device failed')
end
@@ -260,7 +243,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
[first_device, second_device].each do |device|
gitlab_sign_in(user)
device.respond_to_u2f_authentication
- expect(page).to have_content('We heard back from your U2F device')
expect(page).to have_css('.sign-out-link', visible: false)
@@ -283,7 +265,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
it "deletes u2f registrations" do
visit profile_account_path
- expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1)
+ expect do
+ accept_confirm { click_on "Disable" }
+ end.to change { U2fRegistration.count }.by(-1)
end
end
end
diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb
index e1c95590af1..972c10aaf23 100644
--- a/spec/features/uploads/user_uploads_file_to_note_spec.rb
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -14,43 +14,43 @@ feature 'User uploads file to note' do
end
context 'before uploading' do
- it 'shows "Attach a file" button', js: true do
+ it 'shows "Attach a file" button', :js do
expect(page).to have_button('Attach a file')
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
end
context 'uploading is in progress' do
- it 'shows "Cancel" button on uploading', js: true do
- dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+ it 'cancels uploading on clicking to "Cancel" button', :js do
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
- expect(page).to have_button('Cancel')
- end
-
- it 'cancels uploading on clicking to "Cancel" button', js: true do
- dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
-
- click_button 'Cancel'
+ click_button 'Cancel'
+ end
expect(page).to have_button('Attach a file')
expect(page).not_to have_button('Cancel')
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
- it 'shows "Attaching a file" message on uploading 1 file', js: true do
- dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+ it 'shows "Attaching a file" message on uploading 1 file', :js do
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
- expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
+ expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
+ end
end
- it 'shows "Attaching 2 files" message on uploading 2 file', js: true do
- dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'),
- Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+ it 'shows "Attaching 2 files" message on uploading 2 file', :js do
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'),
+ Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
- expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -')
+ expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -')
+ end
end
- it 'shows error message, "retry" and "attach a new file" link a if file is too big', js: true do
+ it 'shows error message, "retry" and "attach a new file" link a if file is too big', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4')], 0.01)
error_text = 'File is too big (0.06MiB). Max filesize: 0.01MiB.'
@@ -63,7 +63,7 @@ feature 'User uploads file to note' do
end
context 'uploading is complete' do
- it 'shows "Attach a file" button on uploading complete', js: true do
+ it 'shows "Attach a file" button on uploading complete', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')])
wait_for_requests
@@ -71,7 +71,7 @@ feature 'User uploads file to note' do
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
- scenario 'they see the attached file', js: true do
+ scenario 'they see the attached file', :js do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')])
click_button 'Comment'
wait_for_requests
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index 13760b4c2fc..8c697e33436 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Snippets tab on a user profile', js: true do
+describe 'Snippets tab on a user profile', :js do
context 'when the user has snippets' do
let(:user) { create(:user) }
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index 15b89dac572..a9973cdf214 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Users', js: true do
+feature 'Users', :js do
let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') }
scenario 'GET /users/sign_in creates a new user account' do
@@ -24,6 +24,7 @@ feature 'Users', js: true do
user.reload
expect(user.reset_password_token).not_to be_nil
+ find('a[href="#login-pane"]').click
gitlab_sign_in(user)
expect(current_path).to eq root_path
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index 6794bf4f4ba..c78f7d0d9be 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Project variables', js: true do
+describe 'Project variables', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') }
@@ -82,7 +82,7 @@ describe 'Project variables', js: true do
it 'deletes variable' do
page.within('.variables-table') do
- click_on 'Remove'
+ accept_confirm { click_on 'Remove' }
end
expect(page).not_to have_selector('variables-table')
diff --git a/spec/finders/admin/projects_finder_spec.rb b/spec/finders/admin/projects_finder_spec.rb
index 4b67203a0df..7901d5fee28 100644
--- a/spec/finders/admin/projects_finder_spec.rb
+++ b/spec/finders/admin/projects_finder_spec.rb
@@ -136,7 +136,7 @@ describe Admin::ProjectsFinder do
context 'filter by name' do
let(:params) { { name: 'C' } }
- it { is_expected.to match_array([shared_project, public_project, private_project]) }
+ it { is_expected.to match_array([public_project]) }
end
context 'sorting' do
diff --git a/spec/finders/autocomplete_users_finder_spec.rb b/spec/finders/autocomplete_users_finder_spec.rb
index 684af74d750..dcf9111776e 100644
--- a/spec/finders/autocomplete_users_finder_spec.rb
+++ b/spec/finders/autocomplete_users_finder_spec.rb
@@ -42,6 +42,21 @@ describe AutocompleteUsersFinder do
it { is_expected.to match_array([user1]) }
end
+ context 'when passed a subgroup', :nested_groups do
+ let(:grandparent) { create(:group, :public) }
+ let(:parent) { create(:group, :public, parent: grandparent) }
+ let(:child) { create(:group, :public, parent: parent) }
+ let(:group) { parent }
+
+ let!(:grandparent_user) { create(:group_member, :developer, group: grandparent).user }
+ let!(:parent_user) { create(:group_member, :developer, group: parent).user }
+ let!(:child_user) { create(:group_member, :developer, group: child).user }
+
+ it 'includes users from parent groups as well' do
+ expect(subject).to match_array([grandparent_user, parent_user])
+ end
+ end
+
it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) }
context 'when filtered by search' do
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
index 91f34973ba5..9e3f2c69606 100644
--- a/spec/finders/branches_finder_spec.rb
+++ b/spec/finders/branches_finder_spec.rb
@@ -46,6 +46,15 @@ describe BranchesFinder do
expect(result.count).to eq(1)
end
+ it 'filters branches by name ignoring letter case' do
+ branches_finder = described_class.new(repository, { search: 'FiX' })
+
+ result = branches_finder.execute
+
+ expect(result.first.name).to eq('fix')
+ expect(result.count).to eq(1)
+ end
+
it 'does not find any branch with that name' do
branches_finder = described_class.new(repository, { search: 'random' })
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
new file mode 100644
index 00000000000..074914420a1
--- /dev/null
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -0,0 +1,166 @@
+require 'spec_helper'
+
+describe GroupDescendantsFinder do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:params) { {} }
+ subject(:finder) do
+ described_class.new(current_user: user, parent_group: group, params: params)
+ end
+
+ before do
+ group.add_owner(user)
+ end
+
+ describe '#has_children?' do
+ it 'is true when there are projects' do
+ create(:project, namespace: group)
+
+ expect(finder.has_children?).to be_truthy
+ end
+
+ context 'when there are subgroups', :nested_groups do
+ it 'is true when there are projects' do
+ create(:group, parent: group)
+
+ expect(finder.has_children?).to be_truthy
+ end
+ end
+ end
+
+ describe '#execute' do
+ it 'includes projects' do
+ project = create(:project, namespace: group)
+
+ expect(finder.execute).to contain_exactly(project)
+ end
+
+ context 'when archived is `true`' do
+ let(:params) { { archived: 'true' } }
+
+ it 'includes archived projects' do
+ archived_project = create(:project, namespace: group, archived: true)
+ project = create(:project, namespace: group)
+
+ expect(finder.execute).to contain_exactly(archived_project, project)
+ end
+ end
+
+ context 'when archived is `only`' do
+ let(:params) { { archived: 'only' } }
+
+ it 'includes only archived projects' do
+ archived_project = create(:project, namespace: group, archived: true)
+ _project = create(:project, namespace: group)
+
+ expect(finder.execute).to contain_exactly(archived_project)
+ end
+ end
+
+ it 'does not include archived projects' do
+ _archived_project = create(:project, :archived, namespace: group)
+
+ expect(finder.execute).to be_empty
+ end
+
+ context 'with a filter' do
+ let(:params) { { filter: 'test' } }
+
+ it 'includes only projects matching the filter' do
+ _other_project = create(:project, namespace: group)
+ matching_project = create(:project, namespace: group, name: 'testproject')
+
+ expect(finder.execute).to contain_exactly(matching_project)
+ end
+ end
+ end
+
+ context 'with nested groups', :nested_groups do
+ let!(:project) { create(:project, namespace: group) }
+ let!(:subgroup) { create(:group, :private, parent: group) }
+
+ describe '#execute' do
+ it 'contains projects and subgroups' do
+ expect(finder.execute).to contain_exactly(subgroup, project)
+ end
+
+ it 'does not include subgroups the user does not have access to' do
+ subgroup.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+
+ public_subgroup = create(:group, :public, parent: group, path: 'public-group')
+ other_subgroup = create(:group, :private, parent: group, path: 'visible-private-group')
+ other_user = create(:user)
+ other_subgroup.add_developer(other_user)
+
+ finder = described_class.new(current_user: other_user, parent_group: group)
+
+ expect(finder.execute).to contain_exactly(public_subgroup, other_subgroup)
+ end
+
+ it 'only includes public groups when no user is given' do
+ public_subgroup = create(:group, :public, parent: group)
+ _private_subgroup = create(:group, :private, parent: group)
+
+ finder = described_class.new(current_user: nil, parent_group: group)
+
+ expect(finder.execute).to contain_exactly(public_subgroup)
+ end
+
+ context 'when archived is `true`' do
+ let(:params) { { archived: 'true' } }
+
+ it 'includes archived projects in the count of subgroups' do
+ create(:project, namespace: subgroup, archived: true)
+
+ expect(finder.execute.first.preloaded_project_count).to eq(1)
+ end
+ end
+
+ context 'with a filter' do
+ let(:params) { { filter: 'test' } }
+
+ it 'contains only matching projects and subgroups' do
+ matching_project = create(:project, namespace: group, name: 'Testproject')
+ matching_subgroup = create(:group, name: 'testgroup', parent: group)
+
+ expect(finder.execute).to contain_exactly(matching_subgroup, matching_project)
+ end
+
+ it 'does not include subgroups the user does not have access to' do
+ _invisible_subgroup = create(:group, :private, parent: group, name: 'test1')
+ other_subgroup = create(:group, :private, parent: group, name: 'test2')
+ public_subgroup = create(:group, :public, parent: group, name: 'test3')
+ other_subsubgroup = create(:group, :private, parent: other_subgroup, name: 'test4')
+ other_user = create(:user)
+ other_subgroup.add_developer(other_user)
+
+ finder = described_class.new(current_user: other_user,
+ parent_group: group,
+ params: params)
+
+ expect(finder.execute).to contain_exactly(other_subgroup, public_subgroup, other_subsubgroup)
+ end
+
+ context 'with matching children' do
+ it 'includes a group that has a subgroup matching the query and its parent' do
+ matching_subgroup = create(:group, :private, name: 'testgroup', parent: subgroup)
+
+ expect(finder.execute).to contain_exactly(subgroup, matching_subgroup)
+ end
+
+ it 'includes the parent of a matching project' do
+ matching_project = create(:project, namespace: subgroup, name: 'Testproject')
+
+ expect(finder.execute).to contain_exactly(subgroup, matching_project)
+ end
+
+ it 'does not include the parent itself' do
+ group.update!(name: 'test')
+
+ expect(finder.execute).not_to include(group)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb
new file mode 100644
index 00000000000..4275b1a7ff1
--- /dev/null
+++ b/spec/finders/runner_jobs_finder_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe RunnerJobsFinder do
+ let(:project) { create(:project) }
+ let(:runner) { create(:ci_runner, :shared) }
+
+ subject { described_class.new(runner, params).execute }
+
+ describe '#execute' do
+ context 'when params is empty' do
+ let(:params) { {} }
+ let!(:job) { create(:ci_build, runner: runner, project: project) }
+ let!(:job1) { create(:ci_build, project: project) }
+
+ it 'returns all jobs assigned to Runner' do
+ is_expected.to match_array(job)
+ is_expected.not_to match_array(job1)
+ end
+ end
+
+ context 'when params contains status' do
+ HasStatus::AVAILABLE_STATUSES.each do |target_status|
+ context "when status is #{target_status}" do
+ let(:params) { { status: target_status } }
+ let!(:job) { create(:ci_build, runner: runner, project: project, status: target_status) }
+
+ before do
+ exception_status = HasStatus::AVAILABLE_STATUSES - [target_status]
+ create(:ci_build, runner: runner, project: project, status: exception_status.first)
+ end
+
+ it 'returns matched job' do
+ is_expected.to eq([job])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index 1f255a17881..489d563be2b 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -1,11 +1,38 @@
{
"type": "object",
"required" : [
- "status"
+ "status",
+ "applications"
],
"properties" : {
"status": { "type": "string" },
- "status_reason": { "type": ["string", "null"] }
+ "status_reason": { "type": ["string", "null"] },
+ "applications": {
+ "type": "array",
+ "items": { "$ref": "#/definitions/application_status" }
+ }
},
- "additionalProperties": false
+ "additionalProperties": false,
+ "definitions": {
+ "application_status": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties" : {
+ "name": { "type": "string" },
+ "status": {
+ "type": {
+ "enum": [
+ "installable",
+ "scheduled",
+ "installing",
+ "installed",
+ "errored"
+ ]
+ }
+ },
+ "status_reason": { "type": ["string", "null"] }
+ },
+ "required" : [ "name", "status" ]
+ }
+ }
}
diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json
new file mode 100644
index 00000000000..3d3329a3406
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/issue.json
@@ -0,0 +1,44 @@
+{
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "author_id": { "type": "integer" },
+ "description": { "type": ["string", "null"] },
+ "lock_version": { "type": ["string", "null"] },
+ "milestone_id": { "type": ["string", "null"] },
+ "title": { "type": "string" },
+ "moved_to_id": { "type": ["integer", "null"] },
+ "project_id": { "type": "integer" },
+ "web_url": { "type": "string" },
+ "state": { "type": "string" },
+ "create_note_path": { "type": "string" },
+ "preview_note_path": { "type": "string" },
+ "current_user": {
+ "type": "object",
+ "properties": {
+ "can_create_note": { "type": "boolean" },
+ "can_update": { "type": "boolean" }
+ }
+ },
+ "created_at": { "type": "date-time" },
+ "updated_at": { "type": "date-time" },
+ "branch_name": { "type": ["string", "null"] },
+ "due_date": { "type": "date" },
+ "confidential": { "type": "boolean" },
+ "discussion_locked": { "type": ["boolean", "null"] },
+ "updated_by_id": { "type": ["string", "null"] },
+ "deleted_at": { "type": ["string", "null"] },
+ "time_estimate": { "type": "integer" },
+ "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",
+ "items": { "$ref": "label.json" }
+ },
+ "assignees": { "type": ["array", "null"] }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/issue_sidebar.json b/spec/fixtures/api/schemas/entities/issue_sidebar.json
new file mode 100644
index 00000000000..682e345d5f5
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/issue_sidebar.json
@@ -0,0 +1,21 @@
+{
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "subscribed": { "type": "boolean" },
+ "time_estimate": { "type": "integer" },
+ "total_time_spent": { "type": "integer" },
+ "human_time_estimate": { "type": ["integer", "null"] },
+ "human_total_time_spent": { "type": ["integer", "null"] },
+ "participants": {
+ "type": "array",
+ "items": { "$ref": "../public_api/v4/user/basic.json" }
+ },
+ "assignees": {
+ "type": "array",
+ "items": { "$ref": "../public_api/v4/user/basic.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/label.json b/spec/fixtures/api/schemas/entities/label.json
new file mode 100644
index 00000000000..40dff764c17
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/label.json
@@ -0,0 +1,26 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "color",
+ "description",
+ "title",
+ "priority"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
+ },
+ "description": { "type": ["string", "null"] },
+ "text_color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
+ },
+ "type": { "type": "string" },
+ "title": { "type": "string" },
+ "priority": { "type": ["integer", "null"] }
+ },
+ "additionalProperties": false
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json
index 6b14188582a..995f13381ad 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_basic.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json
@@ -9,7 +9,9 @@
"human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] },
- "assignee_id": { "type": ["integer", "null"] }
+ "assignee_id": { "type": ["integer", "null"] },
+ "subscribed": { "type": ["boolean", "null"] },
+ "participants": { "type": "array" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index e1f62508933..b579e32c9aa 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -13,38 +13,15 @@
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"relative_position": { "type": "integer" },
+ "issue_sidebar_endpoint": { "type": "string" },
+ "toggle_subscription_endpoint": { "type": "string" },
"project": {
"id": { "type": "integer" },
"path": { "type": "string" }
},
"labels": {
"type": "array",
- "items": {
- "type": "object",
- "required": [
- "id",
- "color",
- "description",
- "title",
- "priority"
- ],
- "properties": {
- "id": { "type": "integer" },
- "color": {
- "type": "string",
- "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
- },
- "description": { "type": ["string", "null"] },
- "text_color": {
- "type": "string",
- "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
- },
- "type": { "type": "string" },
- "title": { "type": "string" },
- "priority": { "type": ["integer", "null"] }
- },
- "additionalProperties": false
- }
+ "items": { "$ref": "entities/label.json" }
},
"assignee": {
"id": { "type": "integet" },
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 5828be5255b..034509091a5 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
@@ -70,6 +70,7 @@
"sha": { "type": "string" },
"merge_commit_sha": { "type": ["string", "null"] },
"user_notes_count": { "type": "integer" },
+ "changes_count": { "type": "string" },
"should_remove_source_branch": { "type": ["boolean", "null"] },
"force_remove_source_branch": { "type": ["boolean", "null"] },
"discussion_locked": { "type": ["boolean", "null"] },
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
new file mode 100644
index 00000000000..4ba6422406c
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
@@ -0,0 +1,18 @@
+{
+ "type": "object",
+ "properties": {
+ "domain": { "type": "string" },
+ "url": { "type": "uri" },
+ "certificate_expiration": {
+ "type": "object",
+ "properties": {
+ "expired": { "type": "boolean" },
+ "expiration": { "type": "string" }
+ },
+ "required": ["expired", "expiration"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["domain", "url"],
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json
new file mode 100644
index 00000000000..08db8d47050
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "properties": {
+ "domain": { "type": "string" },
+ "url": { "type": "uri" },
+ "certificate": {
+ "type": "object",
+ "properties": {
+ "subject": { "type": "string" },
+ "expired": { "type": "boolean" },
+ "certificate": { "type": "string" },
+ "certificate_text": { "type": "string" }
+ },
+ "required": ["subject", "expired"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["domain", "url"],
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json
new file mode 100644
index 00000000000..c7d86de7d8e
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "pages_domain/basic.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domains.json b/spec/fixtures/api/schemas/public_api/v4/pages_domains.json
new file mode 100644
index 00000000000..7c27218dc5a
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domains.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "pages_domain/detail.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json
index e6c1d9c9d84..aa066883c47 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/login.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json
@@ -27,11 +27,9 @@
"can_create_group",
"can_create_project",
"two_factor_enabled",
- "external",
- "private_token"
+ "external"
],
"properties": {
- "$ref": "full.json",
- "private_token": { "type": "string" }
+ "$ref": "full.json"
}
}
diff --git a/spec/fixtures/api/schemas/registry/tag.json b/spec/fixtures/api/schemas/registry/tag.json
index 5bc307e0e64..3a2c88791e1 100644
--- a/spec/fixtures/api/schemas/registry/tag.json
+++ b/spec/fixtures/api/schemas/registry/tag.json
@@ -14,6 +14,11 @@
"revision": {
"type": "string"
},
+ "short_revision": {
+ "type": "string",
+ "minLength": 9,
+ "maxLength": 9
+ },
"total_size": {
"type": "integer"
},
diff --git a/spec/fixtures/clusters/sample_cert.pem b/spec/fixtures/clusters/sample_cert.pem
new file mode 100644
index 00000000000..e39a2b34416
--- /dev/null
+++ b/spec/fixtures/clusters/sample_cert.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFtTCCA52gAwIBAgIJAOutg3Kf2y5dMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTcxMDI5MTgxOTU3WhcNMTgxMDI5MTgxOTU3WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAvQysroM3TLxaavadSPnFIltrYnxCnU4PvCR8971HMWXsq7Z4ShU4BbbE
+8yp7oUFjulSwW6DhdIvnQb8ihLKictLmrA0isQqrD/iNpKZ6/lI4DGWw4QzrvMnW
+V4yy2QZNpg9tzQHd4+xkeeIoG23RijDU/sPd5dqxF+rPHBfCVInmYvSzLvMhneNj
+Bt6gV02gU9e9hsnMatsDvEbvWKp7wcbPot0nWrfZulx2QAWyXy+zG9mJQUds6yc0
+4agAeT9JEb/xtRgR/kS0aUHSGnfSnhZiEn17s0PhTmbu7qSHgzgB+7oJrC9jPoUh
+S2Wo3n0xykAjHrA8wC/Ddw3L38S41VQ58GEfNchistPswyMmXo/Oenv9P3s/kCOI
+fndiksFNdqVo51y9Vjngj589hpOseFDyKmWPIEQZ9kxW/crjP6RZWWLHgz26KtxZ
+uJaoYL8VBbYfrk/bucw0Ma2GEOp8rTsBE7SvgejXZa78q+381Kzc/utW6VwSXqzY
+xeIitft0rXi17SZ+XoiTkIXtHn0ZwMtOXNDBADTpFmKa6wVACQilvcpOYD8gUHyH
+pB+EDRdST3M4Fiq1MBAVhk8Lj3tHSJ/1ymeF1PWSu57AnJlzerzq2fcfPotNNd37
+ZPNkPh0kxPLwxbAyrHflzx9qVVdI1irY9055mNSnhzlec4qJ9cECAwEAAaOBpzCB
+pDAdBgNVHQ4EFgQUnVa5dYPoIG/3+qXml0bX8+N16GwwdQYDVR0jBG4wbIAUnVa5
+dYPoIG/3+qXml0bX8+N16GyhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT
+b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDr
+rYNyn9suXTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQAUg4cyxXi1
+VR8ejTpaAruRyJ1pEG9Kc3kiIRXODy60z3hJXnx9LkScPkWGiuL5XacfZ2rMd4bw
+oVXIyi8U1UHWfAH8EZdrFKkU92jCiL5soHUONxLAvQEJ/FTR/qijrpzLCxXBdVQE
+xFEDWUu6rxLFyjEwzwnRTLgpjR606fdb7qXHkuAMvZ/ezJj8j97hok3Odpn4lr2H
+6hMTpK7HmDBX+kmdJJ+yBrm9hG1Pzpl7QU0dkxZ+qJNFjYMLnziiTwkv0c5ZaA9E
+NykZUcOv3Sjb6spu1A/E2BSq4WTjkIjrogFlfimE1vmUmObTRJOqUB0Vky1kHEwN
+pg7QqIJQmof1EAIaSM/YpUWXyumBwGLDUEud1JUz05In9Q4IZjEwZSJwbQW4fUia
+A93m9rk3Lw3xsFcaUdPMFIXk0rPoF1IgmV/oqb0gK95lOWRLbN+AV8qpKPpcKXOc
+TkIdFE47ZisEDhIdF6wC1izEMLeMEsPAO7/Y6MY4nRxsinSe95lRaw+yQpzx+mvJ
+Q7n1kiHI9Pd5M3+CiQda0d/GO1o5ORJnUGJRvr9HKuNmE7Lif0As/N0AlywjzE7A
+6Z8AEiWyRV1ffshu1k2UKmzvZuZeGGKRtrIjbJIRAtpRVtVZZGzhq5/sojCLoJ+u
+texqFBUo/4mFRZa4pDItUdyOlDy2/LO/ag==
+-----END CERTIFICATE-----
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 4f46e40ce7a..638cd8b07c8 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -268,3 +268,37 @@ However the wrapping tags can not be mixed as such -
### Videos
![My Video](/assets/videos/gitlab-demo.mp4)
+
+### Mermaid
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#mermaid
+
+It is possible to generate diagrams and flowcharts from text using [Mermaid][mermaid].
+
+In order to generate a diagram or flowchart, you should write your text inside the `mermaid` block.
+
+Example:
+
+ ```mermaid
+ graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+ ```
+
+Becomes:
+
+```mermaid
+graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+```
+
+For details see the [Mermaid official page][mermaid].
+
+[mermaid]: https://mermaidjs.github.io/ "Mermaid website"
+
diff --git a/spec/fixtures/pages.tar.gz b/spec/fixtures/pages.tar.gz
index d0e89378b3e..5c4ea9690e8 100644
--- a/spec/fixtures/pages.tar.gz
+++ b/spec/fixtures/pages.tar.gz
Binary files differ
diff --git a/spec/fixtures/pages.zip b/spec/fixtures/pages.zip
index 9558fcd4b94..9bb75f953f8 100644
--- a/spec/fixtures/pages.zip
+++ b/spec/fixtures/pages.zip
Binary files differ
diff --git a/spec/fixtures/ssh_host_example_key.pub b/spec/fixtures/ssh_host_example_key.pub
new file mode 100644
index 00000000000..6bac42b3ad0
--- /dev/null
+++ b/spec/fixtures/ssh_host_example_key.pub
@@ -0,0 +1 @@
+random content
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 10bc5f2ecd2..5c5d53877a6 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -4,8 +4,6 @@ require 'spec_helper'
describe ApplicationHelper do
include UploadHelpers
- let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
-
describe 'current_controller?' do
it 'returns true when controller matches argument' do
stub_controller_name('foo')
@@ -58,27 +56,10 @@ describe ApplicationHelper do
describe 'project_icon' do
it 'returns an url for the avatar' do
- project = create(:project, avatar: File.open(uploaded_image_temp_path))
- avatar_url = "/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
-
- expect(helper.project_icon(project.full_path).to_s)
- .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
-
- allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
- avatar_url = "#{gitlab_host}/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
+ project = create(:project, :public, avatar: File.open(uploaded_image_temp_path))
expect(helper.project_icon(project.full_path).to_s)
- .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
- end
-
- it 'gives uploaded icon when present' do
- project = create(:project)
-
- allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
-
- avatar_url = "#{gitlab_host}#{project_avatar_path(project)}"
- expect(helper.project_icon(project.full_path).to_s)
- .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
+ .to eq "<img data-src=\"#{project.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
end
end
@@ -89,40 +70,7 @@ describe ApplicationHelper do
context 'when there is a matching user' do
it 'returns a relative URL for the avatar' do
expect(helper.avatar_icon(user.email).to_s)
- .to eq("/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
-
- context 'when an asset_host is set in the config' do
- let(:asset_host) { 'http://assets' }
-
- before do
- allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
- end
-
- it 'returns an absolute URL on that asset host' do
- expect(helper.avatar_icon(user.email, only_path: false).to_s)
- .to eq("#{asset_host}/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
- end
-
- context 'when only_path is set to false' do
- it 'returns an absolute URL for the avatar' do
- expect(helper.avatar_icon(user.email, only_path: false).to_s)
- .to eq("#{gitlab_host}/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
- end
-
- context 'when the GitLab instance is at a relative URL' do
- before do
- stub_config_setting(relative_url_root: '/gitlab')
- # Must be stubbed after the stub above, and separately
- stub_config_setting(url: Settings.send(:build_gitlab_url))
- end
-
- it 'returns a relative URL with the correct prefix' do
- expect(helper.avatar_icon(user.email).to_s)
- .to eq("/gitlab/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
+ .to eq(user.avatar.url)
end
end
@@ -136,18 +84,9 @@ describe ApplicationHelper do
end
describe 'using a user' do
- context 'when only_path is true' do
- it 'returns a relative URL for the avatar' do
- expect(helper.avatar_icon(user, only_path: true).to_s)
- .to eq("/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
- end
-
- context 'when only_path is false' do
- it 'returns an absolute URL for the avatar' do
- expect(helper.avatar_icon(user, only_path: false).to_s)
- .to eq("#{gitlab_host}/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif")
- end
+ it 'returns a relative URL for the avatar' do
+ expect(helper.avatar_icon(user).to_s)
+ .to eq(user.avatar.url)
end
end
end
@@ -307,4 +246,12 @@ describe ApplicationHelper do
end
end
end
+
+ describe '#locale_path' do
+ it 'returns the locale path with an `_`' do
+ Gitlab::I18n.with_locale('pt-BR') do
+ expect(helper.locale_path).to include('assets/locale/pt_BR/app')
+ end
+ end
+ end
end
diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb
index 5e272af6073..7266e1b84d1 100644
--- a/spec/helpers/auto_devops_helper_spec.rb
+++ b/spec/helpers/auto_devops_helper_spec.rb
@@ -82,4 +82,104 @@ describe AutoDevopsHelper do
it { is_expected.to eq(false) }
end
end
+
+ describe '.show_run_auto_devops_pipeline_checkbox_for_instance_setting?' do
+ subject { helper.show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project) }
+
+ context 'when master contains a .gitlab-ci.yml file' do
+ before do
+ allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']")
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when auto devops is explicitly enabled' do
+ before do
+ project.create_auto_devops!(enabled: true)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when auto devops is explicitly disabled' do
+ before do
+ project.create_auto_devops!(enabled: false)
+ end
+
+ context 'when auto devops is enabled system-wide' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when auto devops is disabled system-wide' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when auto devops is set to instance setting' do
+ before do
+ project.create_auto_devops!(enabled: nil)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '.show_run_auto_devops_pipeline_checkbox_for_explicit_setting?' do
+ subject { helper.show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project) }
+
+ context 'when master contains a .gitlab-ci.yml file' do
+ before do
+ allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']")
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when auto devops is explicitly enabled' do
+ before do
+ project.create_auto_devops!(enabled: true)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when auto devops is explicitly disabled' do
+ before do
+ project.create_auto_devops!(enabled: false)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when auto devops is set to instance setting' do
+ before do
+ project.create_auto_devops!(enabled: nil)
+ end
+
+ context 'when auto devops is enabled system-wide' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when auto devops is disabled system-wide' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+ end
+ end
end
diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb
index 4423560ecaa..d2c7867febb 100644
--- a/spec/helpers/button_helper_spec.rb
+++ b/spec/helpers/button_helper_spec.rb
@@ -26,30 +26,24 @@ describe ButtonHelper do
context 'when user has password automatically set' do
let(:user) { create(:user, password_automatically_set: true) }
- it 'shows a password tooltip' do
- expect(element.attr('class')).to include(has_tooltip_class)
- expect(element.attr('data-title')).to eq('Set a password on your account to pull or push via HTTP.')
+ it 'shows the password text on the dropdown' do
+ description = element.search('.dropdown-menu-inner-content').first
+
+ expect(description.inner_text).to eq 'Set a password on your account to pull or push via HTTP.'
end
end
end
context 'with internal auth disabled' do
before do
- stub_application_setting(password_authentication_enabled?: false)
+ stub_application_setting(password_authentication_enabled_for_git?: false)
end
context 'when user has no personal access tokens' do
- it 'has a personal access token tooltip ' do
- expect(element.attr('class')).to include(has_tooltip_class)
- expect(element.attr('data-title')).to eq('Create a personal access token on your account to pull or push via HTTP.')
- end
- end
-
- context 'when user has a personal access token' do
- it 'shows no tooltip' do
- create(:personal_access_token, user: user)
+ it 'has a personal access token text on the dropdown description ' do
+ description = element.search('.dropdown-menu-inner-content').first
- expect(element.attr('class')).not_to include(has_tooltip_class)
+ expect(description.inner_text).to eq 'Create a personal access token on your account to pull or push via HTTP.'
end
end
end
@@ -63,6 +57,41 @@ describe ButtonHelper do
end
end
+ describe 'ssh_button' do
+ let(:user) { create(:user) }
+ let(:project) { build_stubbed(:project) }
+
+ def element
+ element = helper.ssh_clone_button(project)
+
+ Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
+ end
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ context 'without an ssh key on the user' do
+ it 'shows a warning on the dropdown description' do
+ description = element.search('.dropdown-menu-inner-content').first
+
+ expect(description.inner_text).to eq "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+ end
+ end
+
+ context 'with an ssh key on the user' do
+ before do
+ create(:key, user: user)
+ end
+
+ it 'there is no warning on the dropdown description' do
+ description = element.search('.dropdown-menu-inner-content').first
+
+ expect(description).to eq nil
+ end
+ end
+ end
+
describe 'clipboard_button' do
let(:user) { create(:user) }
let(:project) { build_stubbed(:project) }
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index 6a3945c0ebc..bc2422aba90 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -8,17 +8,13 @@ describe CiStatusHelper do
describe '#ci_icon_for_status' do
it 'renders to correct svg on success' do
- expect(helper).to receive(:render)
- .with('shared/icons/icon_status_success.svg', anything)
-
- helper.ci_icon_for_status(success_commit.status)
+ expect(helper.ci_icon_for_status('success').to_s)
+ .to include 'status_success'
end
it 'renders the correct svg on failure' do
- expect(helper).to receive(:render)
- .with('shared/icons/icon_status_failed.svg', anything)
-
- helper.ci_icon_for_status(failed_commit.status)
+ expect(helper.ci_icon_for_status('failed').to_s)
+ .to include 'status_failed'
end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index d5536fcb22b..8a80b88da5d 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -1,96 +1,6 @@
require 'spec_helper'
describe EventsHelper do
- describe '#event_note' do
- let(:user) { build(:user) }
-
- before do
- allow(helper).to receive(:current_user).and_return(user)
- end
-
- it 'displays one line of plain text without alteration' do
- input = 'A short, plain note'
- expect(helper.event_note(input)).to match(input)
- expect(helper.event_note(input)).not_to match(/\.\.\.\z/)
- end
-
- it 'displays inline code' do
- input = 'A note with `inline code`'
- expected = 'A note with <code>inline code</code>'
-
- expect(helper.event_note(input)).to match(expected)
- end
-
- it 'truncates a note with multiple paragraphs' do
- input = "Paragraph 1\n\nParagraph 2"
- expected = 'Paragraph 1...'
-
- expect(helper.event_note(input)).to match(expected)
- end
-
- it 'displays the first line of a code block' do
- input = "```\nCode block\nwith two lines\n```"
- expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
-
- expect(helper.event_note(input)).to match(expected)
- end
-
- it 'truncates a single long line of text' do
- text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars
- input = text * 4
- expected = (text * 2).sub(/.{3}/, '...')
-
- expect(helper.event_note(input)).to match(expected)
- end
-
- it 'preserves a link href when link text is truncated' do
- text = 'The quick brown fox jumped over the lazy dog' # 44 chars
- input = "#{text}#{text}#{text} " # 133 chars
- link_url = 'http://example.com/foo/bar/baz' # 30 chars
- input << link_url
- expected_link_text = 'http://example...</a>'
-
- expect(helper.event_note(input)).to match(link_url)
- expect(helper.event_note(input)).to match(expected_link_text)
- end
-
- it 'preserves code color scheme' do
- input = "```ruby\ndef test\n 'hello world'\nend\n```"
- expected = "\n<pre class=\"code highlight js-syntax-highlight ruby\">" \
- "<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
- "</code></pre>"
- expect(helper.event_note(input)).to eq(expected)
- end
-
- it 'preserves data-src for lazy images' do
- input = "![ImageTest](/uploads/test.png)"
- image_url = "data-src=\"/uploads/test.png\""
- expect(helper.event_note(input)).to match(image_url)
- end
-
- context 'labels formatting' do
- let(:input) { 'this should be ~label_1' }
-
- def format_event_note(project)
- create(:label, title: 'label_1', project: project)
-
- helper.event_note(input, { project: project })
- end
-
- it 'preserves style attribute for a label that can be accessed by current_user' do
- project = create(:project, :public)
-
- expect(format_event_note(project)).to match(/span class=.*style=.*/)
- end
-
- it 'does not style a label that can not be accessed by current_user' do
- project = create(:project, :private)
-
- expect(format_event_note(project)).to eq("<p>#{input}</p>")
- end
- end
- end
-
describe '#event_commit_title' do
let(:message) { "foo & bar " + "A" * 70 + "\n" + "B" * 80 }
subject { helper.event_commit_title(message) }
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index a44b200c5da..6c4f7050ee0 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -63,4 +63,30 @@ describe GitlabRoutingHelper do
it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) }
end
end
+
+ describe '#preview_markdown_path' do
+ let(:project) { create(:project) }
+
+ it 'returns group preview markdown path for a group parent' do
+ group = create(:group)
+
+ expect(preview_markdown_path(group)).to eq("/groups/#{group.path}/preview_markdown")
+ end
+
+ it 'returns project preview markdown path for a project parent' do
+ expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown")
+ end
+
+ it 'returns snippet preview markdown path for a personal snippet' do
+ @snippet = create(:personal_snippet)
+
+ expect(preview_markdown_path(nil)).to eq("/snippets/preview_markdown")
+ end
+
+ it 'returns project preview markdown path for a project snippet' do
+ @snippet = create(:project_snippet, project: project)
+
+ expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown")
+ end
+ end
end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 76e5964ccf7..32432ee1e81 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -10,14 +10,27 @@ describe GroupsHelper do
group = create(:group)
group.avatar = fixture_file_upload(avatar_file_path)
group.save!
- expect(group_icon(group.path).to_s)
- .to match("/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif")
+
+ expect(helper.group_icon(group).to_s)
+ .to eq "<img data-src=\"#{group.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
+ end
+ end
+
+ describe 'group_icon_url' do
+ avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
+
+ it 'returns an url for the avatar' do
+ group = create(:group)
+ group.avatar = fixture_file_upload(avatar_file_path)
+ group.save!
+ expect(group_icon_url(group.path).to_s)
+ .to match(group.avatar.url)
end
it 'gives default avatar_icon when no avatar is present' do
group = create(:group)
group.save!
- expect(group_icon(group.path)).to match_asset_path('group_avatar.png')
+ expect(group_icon_url(group.path)).to match_asset_path('group_avatar.png')
end
end
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index 3d79dac284f..2f23ed55d99 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe IconsHelper do
+ let(:icons_path) { ActionController::Base.helpers.image_path("icons.svg") }
+
describe 'icon' do
it 'returns aria-hidden by default' do
star = icon('star')
@@ -16,22 +18,42 @@ describe IconsHelper do
end
end
+ describe 'sprite_icon_path' do
+ it 'returns relative path' do
+ expect(sprite_icon_path)
+ .to eq icons_path
+ end
+
+ context 'when an asset_host is set in the config it will return an absolute local URL' do
+ let(:asset_host) { 'http://assets' }
+
+ before do
+ allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
+ end
+
+ it 'returns an absolute URL on that asset host' do
+ expect(sprite_icon_path)
+ .to eq ActionController::Base.helpers.image_path("icons.svg", host: Gitlab.config.gitlab.url)
+ end
+ end
+ end
+
describe 'sprite_icon' do
icon_name = 'clock'
it 'returns svg icon html' do
expect(sprite_icon(icon_name).to_s)
- .to eq "<svg><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>"
+ .to eq "<svg><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html + size classes' do
expect(sprite_icon(icon_name, size: 72).to_s)
- .to eq "<svg class=\"s72\"><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>"
+ .to eq "<svg class=\"s72\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html + size classes + additional class' do
expect(sprite_icon(icon_name, size: 72, css_class: 'icon-danger').to_s)
- .to eq "<svg class=\"s72 icon-danger\"><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>"
+ .to eq "<svg class=\"s72 icon-danger\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
end
diff --git a/spec/helpers/instance_configuration_helper_spec.rb b/spec/helpers/instance_configuration_helper_spec.rb
new file mode 100644
index 00000000000..5d716b9191d
--- /dev/null
+++ b/spec/helpers/instance_configuration_helper_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe InstanceConfigurationHelper do
+ describe '#instance_configuration_cell_html' do
+ describe 'if not block is passed' do
+ it 'returns the parameter if present' do
+ expect(helper.instance_configuration_cell_html('gitlab')).to eq('gitlab')
+ end
+
+ it 'returns "-" if the parameter is blank' do
+ expect(helper.instance_configuration_cell_html(nil)).to eq('-')
+ expect(helper.instance_configuration_cell_html('')).to eq('-')
+ end
+ end
+
+ describe 'if a block is passed' do
+ let(:upcase_block) { ->(value) { value.upcase } }
+
+ it 'returns the result of the block' do
+ expect(helper.instance_configuration_cell_html('gitlab', &upcase_block)).to eq('GITLAB')
+ expect(helper.instance_configuration_cell_html('gitlab') { |v| v.upcase }).to eq('GITLAB')
+ end
+
+ it 'returns "-" if the parameter is blank' do
+ expect(helper.instance_configuration_cell_html(nil, &upcase_block)).to eq('-')
+ expect(helper.instance_configuration_cell_html(nil) { |v| v.upcase }).to eq('-')
+ expect(helper.instance_configuration_cell_html('', &upcase_block)).to eq('-')
+ end
+ end
+
+ it 'boolean are valid values to display' do
+ expect(helper.instance_configuration_cell_html(true)).to eq(true)
+ expect(helper.instance_configuration_cell_html(false)).to eq(false)
+ end
+ end
+
+ describe '#instance_configuration_human_size_cell' do
+ it 'returns "-" if the parameter is blank' do
+ expect(helper.instance_configuration_human_size_cell(nil)).to eq('-')
+ expect(helper.instance_configuration_human_size_cell('')).to eq('-')
+ end
+
+ it 'accepts the value in bytes' do
+ expect(helper.instance_configuration_human_size_cell(1024)).to eq('1 KB')
+ end
+
+ it 'returns the value in human size readable format' do
+ expect(helper.instance_configuration_human_size_cell(1048576)).to eq('1 MB')
+ end
+ end
+end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index ead3e28438e..d601cbdb39b 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -159,4 +159,37 @@ describe IssuablesHelper do
end
end
end
+
+ describe '#issuable_initial_data' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?).and_return(true)
+ end
+
+ it 'returns the correct json for an issue' do
+ issue = create(:issue, author: user, description: 'issue text')
+ @project = issue.project
+
+ expected_data = {
+ 'endpoint' => "/#{@project.full_path}/issues/#{issue.iid}",
+ 'updateEndpoint' => "/#{@project.full_path}/issues/#{issue.iid}.json",
+ 'canUpdate' => true,
+ 'canDestroy' => true,
+ 'issuableRef' => "##{issue.iid}",
+ 'markdownPreviewPath' => "/#{@project.full_path}/preview_markdown",
+ 'markdownDocsPath' => '/help/user/markdown',
+ 'issuableTemplates' => [],
+ 'projectPath' => @project.path,
+ 'projectNamespace' => @project.namespace.path,
+ 'initialTitleHtml' => issue.title,
+ 'initialTitleText' => issue.title,
+ 'initialDescriptionHtml' => '<p dir="auto">issue text</p>',
+ 'initialDescriptionText' => 'issue text',
+ 'initialTaskStatus' => '0 of 0 tasks completed'
+ }
+ expect(JSON.parse(helper.issuable_initial_data(issue))).to eq(expected_data)
+ end
+ end
end
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 36d6e495ed0..4ac4302adfd 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -24,7 +24,7 @@ describe LabelsHelper do
let(:group) { build(:group, name: 'bar') }
it 'links to group issues page' do
- expect(link_to_label(label, subject: group)).to match %r{<a href="/groups/bar/issues\?label_name%5B%5D=#{label.name}">.*</a>}
+ expect(link_to_label(label, subject: group)).to match %r{<a href="/groups/bar/-/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 03d706062b7..62ea6d48542 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -67,7 +67,7 @@ describe MarkupHelper do
describe 'without redacted attribute' do
it 'renders the markdown value' do
- expect(Banzai).to receive(:render_field).with(commit, attribute).and_call_original
+ expect(Banzai).to receive(:render_field).with(commit, attribute, {}).and_call_original
helper.markdown_field(commit, attribute)
end
@@ -252,38 +252,141 @@ describe MarkupHelper do
end
describe '#first_line_in_markdown' do
- it 'truncates Markdown properly' do
- text = "@#{user.username}, can you look at this?\nHello world\n"
- actual = first_line_in_markdown(text, 100, project: project)
+ shared_examples_for 'common markdown examples' do
+ let(:project_base) { build(:project, :repository) }
- doc = Nokogiri::HTML.parse(actual)
+ it 'displays inline code' do
+ object = create_object('Text with `inline code`')
+ expected = 'Text with <code>inline code</code>'
- # Make sure we didn't create invalid markup
- expect(doc.errors).to be_empty
+ expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
+ end
- # Leading user link
- expect(doc.css('a').length).to eq(1)
- expect(doc.css('a')[0].attr('href')).to eq user_path(user)
- expect(doc.css('a')[0].text).to eq "@#{user.username}"
+ it 'truncates the text with multiple paragraphs' do
+ object = create_object("Paragraph 1\n\nParagraph 2")
+ expected = 'Paragraph 1...'
- expect(doc.content).to eq "@#{user.username}, can you look at this?..."
- end
+ expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
+ end
- it 'truncates Markdown with emoji properly' do
- text = "foo :wink:\nbar :grinning:"
- actual = first_line_in_markdown(text, 100, project: project)
+ it 'displays the first line of a code block' do
+ object = create_object("```\nCode block\nwith two lines\n```")
+ expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
- doc = Nokogiri::HTML.parse(actual)
+ expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
+ end
- # Make sure we didn't create invalid markup
- # But also account for the 2 errors caused by the unknown `gl-emoji` elements
- expect(doc.errors.length).to eq(2)
+ it 'truncates a single long line of text' do
+ text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars
+ object = create_object(text * 4)
+ expected = (text * 2).sub(/.{3}/, '...')
+
+ expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected)
+ end
+
+ it 'preserves a link href when link text is truncated' do
+ text = 'The quick brown fox jumped over the lazy dog' # 44 chars
+ input = "#{text}#{text}#{text} " # 133 chars
+ link_url = 'http://example.com/foo/bar/baz' # 30 chars
+ input << link_url
+ object = create_object(input)
+ expected_link_text = 'http://example...</a>'
+
+ expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(link_url)
+ expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected_link_text)
+ end
+
+ it 'preserves code color scheme' do
+ object = create_object("```ruby\ndef test\n 'hello world'\nend\n```")
+ expected = "\n<pre class=\"code highlight js-syntax-highlight ruby\">" \
+ "<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
+ "</code></pre>"
+
+ expect(first_line_in_markdown(object, attribute, 150, project: project)).to eq(expected)
+ end
+
+ it 'preserves data-src for lazy images' do
+ object = create_object("![ImageTest](/uploads/test.png)")
+ image_url = "data-src=\".*/uploads/test.png\""
+
+ expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(image_url)
+ end
+
+ context 'labels formatting' do
+ let(:label_title) { 'this should be ~label_1' }
+
+ def create_and_format_label(project)
+ create(:label, title: 'label_1', project: project)
+ object = create_object(label_title, project: project)
- expect(doc.css('gl-emoji').length).to eq(2)
- expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink'
- expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning'
+ first_line_in_markdown(object, attribute, 150, project: project)
+ end
- expect(doc.content).to eq "foo 😉\nbar 😀"
+ it 'preserves style attribute for a label that can be accessed by current_user' do
+ project = create(:project, :public)
+
+ expect(create_and_format_label(project)).to match(/span class=.*style=.*/)
+ end
+
+ it 'does not style a label that can not be accessed by current_user' do
+ project = create(:project, :private)
+
+ expect(create_and_format_label(project)).to eq("<p>#{label_title}</p>")
+ end
+ end
+
+ it 'truncates Markdown properly' do
+ object = create_object("@#{user.username}, can you look at this?\nHello world\n")
+ actual = first_line_in_markdown(object, attribute, 100, project: project)
+
+ doc = Nokogiri::HTML.parse(actual)
+
+ # Make sure we didn't create invalid markup
+ expect(doc.errors).to be_empty
+
+ # Leading user link
+ expect(doc.css('a').length).to eq(1)
+ expect(doc.css('a')[0].attr('href')).to eq user_path(user)
+ expect(doc.css('a')[0].text).to eq "@#{user.username}"
+
+ expect(doc.content).to eq "@#{user.username}, can you look at this?..."
+ end
+
+ it 'truncates Markdown with emoji properly' do
+ object = create_object("foo :wink:\nbar :grinning:")
+ actual = first_line_in_markdown(object, attribute, 100, project: project)
+
+ doc = Nokogiri::HTML.parse(actual)
+
+ # Make sure we didn't create invalid markup
+ # But also account for the 2 errors caused by the unknown `gl-emoji` elements
+ expect(doc.errors.length).to eq(2)
+
+ expect(doc.css('gl-emoji').length).to eq(2)
+ expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink'
+ expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning'
+
+ expect(doc.content).to eq "foo 😉\nbar 😀"
+ end
+ end
+
+ context 'when the asked attribute can be redacted' do
+ include_examples 'common markdown examples' do
+ let(:attribute) { :note }
+ def create_object(title, project: project_base)
+ build(:note, note: title, project: project)
+ end
+ end
+ end
+
+ context 'when the asked attribute can not be redacted' do
+ include_examples 'common markdown examples' do
+ let(:attribute) { :body }
+ def create_object(title, project: project_base)
+ issue = build(:issue, title: title)
+ build(:todo, :done, project: project_base, author: user, target: issue)
+ end
+ end
end
end
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index 8365b3f5538..460d3b6a7e4 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -29,5 +29,30 @@ describe NamespacesHelper do
expect(options).not_to include(admin_group.name)
expect(options).to include(user_group.name)
end
+
+ context 'when nested groups are available', :nested_groups do
+ it 'includes groups nested in groups the user can administer' do
+ allow(helper).to receive(:current_user).and_return(user)
+ child_group = create(:group, :private, parent: user_group)
+
+ options = helper.namespaces_options
+
+ expect(options).to include(child_group.name)
+ end
+
+ it 'orders the groups correctly' do
+ allow(helper).to receive(:current_user).and_return(user)
+ child_group = create(:group, :private, parent: user_group)
+ other_child = create(:group, :private, parent: user_group)
+ sub_child = create(:group, :private, parent: child_group)
+
+ expect(helper).to receive(:options_for_group)
+ .with([user_group, child_group, sub_child, other_child], anything)
+ .and_call_original
+ allow(helper).to receive(:options_for_group).and_call_original
+
+ helper.namespaces_options
+ end
+ end
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 641971485de..ede9d232efd 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -63,7 +63,7 @@ describe ProjectsHelper do
end
end
- describe "#project_list_cache_key", clean_gitlab_redis_shared_state: true do
+ describe "#project_list_cache_key", :clean_gitlab_redis_shared_state do
let(:project) { create(:project, :repository) }
it "includes the route" do
@@ -150,17 +150,26 @@ describe ProjectsHelper do
end
end
- context 'user requires a password' do
- let(:user) { create(:user, password_automatically_set: true) }
+ context 'user has hidden the message' do
+ it 'returns false' do
+ allow(helper).to receive(:cookies).and_return(hide_no_password_message: true)
+
+ expect(helper.show_no_password_message?).to be_falsey
+ end
+ end
+ context 'user requires a password for Git' do
it 'returns true' do
+ allow(user).to receive(:require_password_creation_for_git?).and_return(true)
+
expect(helper.show_no_password_message?).to be_truthy
end
end
- context 'user requires a personal access token' do
+ context 'user requires a personal access token for Git' do
it 'returns true' do
- stub_application_setting(password_authentication_enabled?: false)
+ allow(user).to receive(:require_password_creation_for_git?).and_return(false)
+ allow(user).to receive(:require_personal_access_token_creation_for_git_auth?).and_return(true)
expect(helper.show_no_password_message?).to be_truthy
end
@@ -168,23 +177,23 @@ describe ProjectsHelper do
end
describe '#link_to_set_password' do
+ let(:user) { create(:user, password_automatically_set: true) }
+
before do
allow(helper).to receive(:current_user).and_return(user)
end
- context 'user requires a password' do
- let(:user) { create(:user, password_automatically_set: true) }
-
+ context 'password authentication is enabled for Git' do
it 'returns link to set a password' do
+ stub_application_setting(password_authentication_enabled_for_git?: true)
+
expect(helper.link_to_set_password).to match %r{<a href="#{edit_profile_password_path}">set a password</a>}
end
end
- context 'user requires a personal access token' do
- let(:user) { create(:user) }
-
+ context 'password authentication is disabled for Git' do
it 'returns link to create a personal access token' do
- stub_application_setting(password_authentication_enabled?: false)
+ stub_application_setting(password_authentication_enabled_for_git?: false)
expect(helper.link_to_set_password).to match %r{<a href="#{profile_personal_access_tokens_path}">create a personal access token</a>}
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index ab647401e14..6c9a7febf14 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -102,6 +102,10 @@ describe SearchHelper do
it 'includes project base-endpoint' do
expect(search_filter_input_options('')[:data]['base-endpoint']).to eq(project_path(@project))
end
+
+ it 'includes autocomplete=off flag' do
+ expect(search_filter_input_options('')[:autocomplete]).to eq('off')
+ end
end
context 'group' do
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index d7b66e6f078..c358ccae9c3 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -1,10 +1,36 @@
require 'spec_helper'
describe TreeHelper do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:sha) { 'ce369011c189f62c815f5971d096b26759bab0d1' }
+
+ describe '.render_tree' do
+ before do
+ @id = sha
+ @project = project
+ end
+
+ it 'displays all entries without a warning' do
+ tree = repository.tree(sha, 'files')
+
+ html = render_tree(tree)
+
+ expect(html).not_to have_selector('.tree-truncated-warning')
+ end
+
+ it 'truncates entries and adds a warning' do
+ stub_const('TreeHelper::FILE_LIMIT', 1)
+ tree = repository.tree(sha, 'files')
+
+ html = render_tree(tree)
+
+ expect(html).to have_selector('.tree-truncated-warning', count: 1)
+ expect(html).to have_selector('.tree-item-file-name', count: 1)
+ end
+ end
+
describe 'flatten_tree' do
- let(:project) { create(:project, :repository) }
- let(:repository) { project.repository }
- let(:sha) { 'ce369011c189f62c815f5971d096b26759bab0d1' }
let(:tree) { repository.tree(sha, 'files') }
let(:root_path) { 'files' }
let(:tree_item) { tree.entries.find { |entry| entry.path == path } }
diff --git a/spec/initializers/8_metrics_spec.rb b/spec/initializers/8_metrics_spec.rb
index 4e6052a9f80..80c77057065 100644
--- a/spec/initializers/8_metrics_spec.rb
+++ b/spec/initializers/8_metrics_spec.rb
@@ -3,7 +3,6 @@ require 'spec_helper'
describe 'instrument_classes' do
let(:config) { double(:config) }
- let(:unicorn_sampler) { double(:unicorn_sampler) }
let(:influx_sampler) { double(:influx_sampler) }
before do
@@ -11,9 +10,7 @@ describe 'instrument_classes' do
allow(config).to receive(:instrument_methods)
allow(config).to receive(:instrument_instance_method)
allow(config).to receive(:instrument_instance_methods)
- allow(Gitlab::Metrics::UnicornSampler).to receive(:initialize_instance).and_return(unicorn_sampler)
- allow(Gitlab::Metrics::InfluxSampler).to receive(:initialize_instance).and_return(influx_sampler)
- allow(unicorn_sampler).to receive(:start)
+ allow(Gitlab::Metrics::Samplers::InfluxSampler).to receive(:initialize_instance).and_return(influx_sampler)
allow(influx_sampler).to receive(:start)
allow(Gitlab::Application).to receive(:configure)
end
diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb
index 9a974e70e8c..a11824d0ac5 100644
--- a/spec/initializers/settings_spec.rb
+++ b/spec/initializers/settings_spec.rb
@@ -18,26 +18,6 @@ describe Settings do
end
end
- describe '#repositories' do
- it 'assigns the default failure attributes' do
- repository_settings = Gitlab.config.repositories.storages['broken']
-
- expect(repository_settings['failure_count_threshold']).to eq(10)
- expect(repository_settings['failure_wait_time']).to eq(30)
- expect(repository_settings['failure_reset_time']).to eq(1800)
- expect(repository_settings['storage_timeout']).to eq(5)
- end
-
- it 'can be accessed with dot syntax all the way down' do
- expect(Gitlab.config.repositories.storages.broken.failure_count_threshold).to eq(10)
- end
-
- it 'can be accessed in a very specific way that breaks without reassigning each element with Settingslogic' do
- storage_settings = Gitlab.config.repositories.storages['broken']
- expect(storage_settings.failure_count_threshold).to eq(10)
- end
- end
-
describe '#host_without_www' do
context 'URL with protocol' do
it 'returns the host' do
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index a22b71fd1dc..268b5b83b73 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -28,7 +28,7 @@ import '~/lib/utils/common_utils';
preloadFixtures('merge_requests/diff_comment.html.raw');
beforeEach(function(done) {
loadFixtures('merge_requests/diff_comment.html.raw');
- $('body').data('page', 'projects:merge_requests:show');
+ $('body').attr('data-page', 'projects:merge_requests:show');
loadAwardsHandler(true).then((obj) => {
awardsHandler = obj;
spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb());
@@ -55,6 +55,9 @@ import '~/lib/utils/common_utils';
// restore original url root value
gon.relative_url_root = urlRoot;
+ // Undo what we did to the shared <body>
+ $('body').removeAttr('data-page');
+
awardsHandler.destroy();
});
describe('::showEmojiMenu', function() {
diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js
index 67afba19190..960b731892a 100644
--- a/spec/javascripts/behaviors/autosize_spec.js
+++ b/spec/javascripts/behaviors/autosize_spec.js
@@ -1,21 +1,18 @@
-/* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, max-len */
-
import '~/behaviors/autosize';
-(function() {
- describe('Autosize behavior', function() {
- var load;
- beforeEach(function() {
- return setFixtures('<textarea class="js-autosize" style="resize: vertical"></textarea>');
- });
- it('does not overwrite the resize property', function() {
- load();
- return expect($('textarea')).toHaveCss({
- resize: 'vertical'
- });
+function load() {
+ $(document).trigger('load');
+}
+
+describe('Autosize behavior', () => {
+ beforeEach(() => {
+ setFixtures('<textarea class="js-autosize" style="resize: vertical"></textarea>');
+ });
+
+ it('does not overwrite the resize property', () => {
+ load();
+ expect($('textarea')).toHaveCss({
+ resize: 'vertical',
});
- return load = function() {
- return $(document).trigger('load');
- };
});
-}).call(window);
+});
diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js
new file mode 100644
index 00000000000..b8155144e2a
--- /dev/null
+++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js
@@ -0,0 +1,47 @@
+import { CopyAsGFM } from '~/behaviors/copy_as_gfm';
+
+describe('CopyAsGFM', () => {
+ describe('CopyAsGFM.pasteGFM', () => {
+ function callPasteGFM() {
+ const e = {
+ originalEvent: {
+ clipboardData: {
+ getData(mimeType) {
+ // When GFM code is copied, we put the regular plain text
+ // on the clipboard as `text/plain`, and the GFM as `text/x-gfm`.
+ // This emulates the behavior of `getData` with that data.
+ if (mimeType === 'text/plain') {
+ return 'code';
+ }
+ if (mimeType === 'text/x-gfm') {
+ return '`code`';
+ }
+ return null;
+ },
+ },
+ },
+ preventDefault() {},
+ };
+
+ CopyAsGFM.pasteGFM(e);
+ }
+
+ it('wraps pasted code when not already in code tags', () => {
+ spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => {
+ const insertedText = textFunc('This is code: ', '');
+ expect(insertedText).toEqual('`code`');
+ });
+
+ callPasteGFM();
+ });
+
+ it('does not wrap pasted code when already in code tags', () => {
+ spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => {
+ const insertedText = textFunc('This is code: `', '`');
+ expect(insertedText).toEqual('code');
+ });
+
+ callPasteGFM();
+ });
+ });
+});
diff --git a/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js
index ec2c549e032..f96f20ed4a5 100644
--- a/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js
+++ b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js
@@ -21,13 +21,18 @@ describe('Unicode Support Map', () => {
});
it('should call .getItem and .setItem', () => {
- const allArgs = window.localStorage.setItem.calls.allArgs();
-
- expect(window.localStorage.getItem).toHaveBeenCalledWith('gl-emoji-user-agent');
- expect(allArgs[0][0]).toBe('gl-emoji-user-agent');
- expect(allArgs[0][1]).toBe(navigator.userAgent);
- expect(allArgs[1][0]).toBe('gl-emoji-unicode-support-map');
- expect(allArgs[1][1]).toBe(stringSupportMap);
+ const getArgs = window.localStorage.getItem.calls.allArgs();
+ const setArgs = window.localStorage.setItem.calls.allArgs();
+
+ expect(getArgs[0][0]).toBe('gl-emoji-version');
+ expect(getArgs[1][0]).toBe('gl-emoji-user-agent');
+
+ expect(setArgs[0][0]).toBe('gl-emoji-version');
+ expect(setArgs[0][1]).toBe('0.2.0');
+ expect(setArgs[1][0]).toBe('gl-emoji-user-agent');
+ expect(setArgs[1][1]).toBe(navigator.userAgent);
+ expect(setArgs[2][0]).toBe('gl-emoji-unicode-support-map');
+ expect(setArgs[2][1]).toBe(stringSupportMap);
});
});
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index f62bf43adb9..d5300d9c63d 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -19,6 +19,11 @@ describe('Quick Submit behavior', () => {
this.textarea = $('.js-quick-submit textarea').first();
});
+ afterEach(() => {
+ // Undo what we did to the shared <body>
+ $('body').removeAttr('data-page');
+ });
+
it('does not respond to other keyCodes', () => {
this.textarea.trigger(keydownEvent({
keyCode: 32,
diff --git a/spec/javascripts/blob/blob_file_dropzone_spec.js b/spec/javascripts/blob/blob_file_dropzone_spec.js
index 2c8183ff77b..47de63e6690 100644
--- a/spec/javascripts/blob/blob_file_dropzone_spec.js
+++ b/spec/javascripts/blob/blob_file_dropzone_spec.js
@@ -1,4 +1,3 @@
-import 'dropzone';
import BlobFileDropzone from '~/blob/blob_file_dropzone';
describe('BlobFileDropzone', () => {
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 83b13b06dc1..8f607899b20 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -9,10 +9,11 @@
import Vue from 'vue';
import '~/boards/models/assignee';
+import eventHub from '~/boards/eventhub';
import '~/boards/models/list';
import '~/boards/models/label';
import '~/boards/stores/boards_store';
-import boardCard from '~/boards/components/board_card';
+import boardCard from '~/boards/components/board_card.vue';
import './mock_data';
describe('Board card', () => {
@@ -157,33 +158,35 @@ describe('Board card', () => {
});
it('sets detail issue to card issue on mouse up', () => {
+ spyOn(eventHub, '$emit');
+
triggerEvent('mousedown');
triggerEvent('mouseup');
- expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+ expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue);
expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list);
});
it('adds active class if detail issue is set', (done) => {
- triggerEvent('mousedown');
- triggerEvent('mouseup');
-
- setTimeout(() => {
- expect(vm.$el.classList.contains('is-active')).toBe(true);
- done();
- }, 0);
+ vm.detailIssue.issue = vm.issue;
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.classList.contains('is-active')).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
});
it('resets detail issue to empty if already set', () => {
- triggerEvent('mousedown');
- triggerEvent('mouseup');
+ spyOn(eventHub, '$emit');
- expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue);
+ gl.issueBoards.BoardsStore.detail.issue = vm.issue;
triggerEvent('mousedown');
triggerEvent('mouseup');
- expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+ expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue');
});
});
});
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index 022d286d5df..ccde657789a 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -133,6 +133,19 @@ describe('Issue model', () => {
expect(relativePositionIssue.position).toBe(1);
});
+ it('updates data', () => {
+ issue.updateData({ subscribed: true });
+ expect(issue.subscribed).toBe(true);
+ });
+
+ it('sets fetching state', () => {
+ expect(issue.isFetching.subscriptions).toBe(true);
+
+ issue.setFetchingState('subscriptions', false);
+
+ expect(issue.isFetching.subscriptions).toBe(false);
+ });
+
describe('update', () => {
it('passes assignee ids when there are assignees', (done) => {
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js
deleted file mode 100644
index d5b0f23e7b7..00000000000
--- a/spec/javascripts/build_spec.js
+++ /dev/null
@@ -1,306 +0,0 @@
-/* eslint-disable no-new */
-/* global Build */
-import { bytesToKiB } from '~/lib/utils/number_utils';
-import '~/lib/utils/datetime_utility';
-import '~/lib/utils/url_utility';
-import '~/build';
-import '~/breakpoints';
-
-describe('Build', () => {
- const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
-
- preloadFixtures('builds/build-with-artifacts.html.raw');
-
- beforeEach(() => {
- loadFixtures('builds/build-with-artifacts.html.raw');
- });
-
- describe('class constructor', () => {
- beforeEach(() => {
- jasmine.clock().install();
- });
-
- afterEach(() => {
- jasmine.clock().uninstall();
- });
-
- describe('setup', () => {
- beforeEach(function () {
- this.build = new Build();
- });
-
- it('copies build options', function () {
- expect(this.build.pageUrl).toBe(BUILD_URL);
- expect(this.build.buildStatus).toBe('success');
- expect(this.build.buildStage).toBe('test');
- expect(this.build.state).toBe('');
- });
-
- it('only shows the jobs matching the current stage', () => {
- expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false);
- expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true);
- expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
- });
-
- it('selects the current stage in the build dropdown menu', () => {
- expect($('.stage-selection').text()).toBe('test');
- });
-
- it('updates the jobs when the build dropdown changes', () => {
- $('.stage-item:contains("build")').click();
-
- expect($('.stage-selection').text()).toBe('build');
- expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true);
- expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false);
- expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
- });
-
- it('displays the remove date correctly', () => {
- const removeDateElement = document.querySelector('.js-artifacts-remove');
- expect(removeDateElement.innerText.trim()).toBe('1 year remaining');
- });
- });
-
- describe('running build', () => {
- it('updates the build trace on an interval', function () {
- const deferred1 = $.Deferred();
- const deferred2 = $.Deferred();
- const deferred3 = $.Deferred();
- spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
- spyOn(gl.utils, 'visitUrl');
-
- deferred1.resolve({
- html: '<span>Update<span>',
- status: 'running',
- state: 'newstate',
- append: true,
- complete: false,
- });
-
- deferred2.resolve();
-
- deferred3.resolve({
- html: '<span>More</span>',
- status: 'running',
- state: 'finalstate',
- append: true,
- complete: true,
- });
-
- this.build = new Build();
-
- expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
- expect(this.build.state).toBe('newstate');
-
- jasmine.clock().tick(4001);
-
- expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
- expect(this.build.state).toBe('finalstate');
- });
-
- it('replaces the entire build trace', () => {
- const deferred1 = $.Deferred();
- const deferred2 = $.Deferred();
- const deferred3 = $.Deferred();
-
- spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
-
- spyOn(gl.utils, 'visitUrl');
-
- deferred1.resolve({
- html: '<span>Update<span>',
- status: 'running',
- append: false,
- complete: false,
- });
-
- deferred2.resolve();
-
- deferred3.resolve({
- html: '<span>Different</span>',
- status: 'running',
- append: false,
- });
-
- this.build = new Build();
-
- expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
-
- jasmine.clock().tick(4001);
-
- expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
- expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
- });
- });
-
- describe('truncated information', () => {
- describe('when size is less than total', () => {
- it('shows information about truncated log', () => {
- spyOn(gl.utils, 'visitUrl');
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
-
- deferred.resolve({
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 50,
- total: 100,
- });
-
- this.build = new Build();
-
- expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
- });
-
- it('shows the size in KiB', () => {
- const size = 50;
- spyOn(gl.utils, 'visitUrl');
- const deferred = $.Deferred();
-
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- deferred.resolve({
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size,
- total: 100,
- });
-
- this.build = new Build();
-
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${bytesToKiB(size)}`);
- });
-
- it('shows incremented size', () => {
- const deferred1 = $.Deferred();
- const deferred2 = $.Deferred();
- const deferred3 = $.Deferred();
-
- spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
-
- spyOn(gl.utils, 'visitUrl');
-
- deferred1.resolve({
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 50,
- total: 100,
- });
-
- deferred2.resolve();
-
- this.build = new Build();
-
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${bytesToKiB(50)}`);
-
- jasmine.clock().tick(4001);
-
- deferred3.resolve({
- html: '<span>Update</span>',
- status: 'success',
- append: true,
- size: 10,
- total: 100,
- });
-
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${bytesToKiB(60)}`);
- });
-
- it('renders the raw link', () => {
- const deferred = $.Deferred();
- spyOn(gl.utils, 'visitUrl');
-
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- deferred.resolve({
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 50,
- total: 100,
- });
-
- this.build = new Build();
-
- expect(
- document.querySelector('.js-raw-link').textContent.trim(),
- ).toContain('Complete Raw');
- });
- });
-
- describe('when size is equal than total', () => {
- it('does not show the trunctated information', () => {
- const deferred = $.Deferred();
- spyOn(gl.utils, 'visitUrl');
-
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- deferred.resolve({
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 100,
- total: 100,
- });
-
- this.build = new Build();
-
- expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
- });
- });
- });
-
- describe('output trace', () => {
- beforeEach(() => {
- const deferred = $.Deferred();
- spyOn(gl.utils, 'visitUrl');
-
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- deferred.resolve({
- html: '<span>Update</span>',
- status: 'success',
- append: false,
- size: 50,
- total: 100,
- });
-
- this.build = new Build();
- });
-
- it('should render trace controls', () => {
- const controllers = document.querySelector('.controllers');
-
- expect(controllers.querySelector('.js-raw-link-controller')).toBeDefined();
- expect(controllers.querySelector('.js-erase-link')).toBeDefined();
- expect(controllers.querySelector('.js-scroll-up')).toBeDefined();
- expect(controllers.querySelector('.js-scroll-down')).toBeDefined();
- });
-
- it('should render received output', () => {
- expect(
- document.querySelector('.js-build-output').innerHTML,
- ).toEqual('<span>Update</span>');
- });
- });
- });
-
- describe('getBuildTrace', () => {
- it('should request build trace with state parameter', (done) => {
- spyOn(jQuery, 'ajax').and.callThrough();
- new Build();
-
- setTimeout(() => {
- expect(jQuery.ajax).toHaveBeenCalledWith(
- { url: `${BUILD_URL}/trace.json`, data: { state: '' } },
- );
- done();
- }, 0);
- });
- });
-});
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
new file mode 100644
index 00000000000..027e8001053
--- /dev/null
+++ b/spec/javascripts/clusters/clusters_bundle_spec.js
@@ -0,0 +1,257 @@
+import Clusters from '~/clusters/clusters_bundle';
+import {
+ APPLICATION_INSTALLABLE,
+ APPLICATION_INSTALLING,
+ APPLICATION_INSTALLED,
+ REQUEST_LOADING,
+ REQUEST_SUCCESS,
+ REQUEST_FAILURE,
+} from '~/clusters/constants';
+import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
+
+describe('Clusters', () => {
+ let cluster;
+ preloadFixtures('clusters/show_cluster.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('clusters/show_cluster.html.raw');
+ cluster = new Clusters();
+ });
+
+ afterEach(() => {
+ cluster.destroy();
+ });
+
+ describe('toggle', () => {
+ it('should update the button and the input field on click', () => {
+ cluster.toggleButton.click();
+
+ expect(
+ cluster.toggleButton.classList,
+ ).not.toContain('checked');
+
+ expect(
+ cluster.toggleInput.getAttribute('value'),
+ ).toEqual('false');
+ });
+ });
+
+ describe('checkForNewInstalls', () => {
+ const INITIAL_APP_MAP = {
+ helm: { status: null, title: 'Helm Tiller' },
+ ingress: { status: null, title: 'Ingress' },
+ runner: { status: null, title: 'GitLab Runner' },
+ };
+
+ it('does not show alert when things transition from initial null state to something', () => {
+ cluster.checkForNewInstalls(INITIAL_APP_MAP, {
+ ...INITIAL_APP_MAP,
+ helm: { status: APPLICATION_INSTALLABLE, title: 'Helm Tiller' },
+ });
+
+ expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeNull();
+ });
+
+ it('shows an alert when something gets newly installed', () => {
+ cluster.checkForNewInstalls({
+ ...INITIAL_APP_MAP,
+ helm: { status: APPLICATION_INSTALLING, title: 'Helm Tiller' },
+ }, {
+ ...INITIAL_APP_MAP,
+ helm: { status: APPLICATION_INSTALLED, title: 'Helm Tiller' },
+ });
+
+ expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined();
+ expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller was successfully installed on your cluster');
+ });
+
+ it('shows an alert when multiple things gets newly installed', () => {
+ cluster.checkForNewInstalls({
+ ...INITIAL_APP_MAP,
+ helm: { status: APPLICATION_INSTALLING, title: 'Helm Tiller' },
+ ingress: { status: APPLICATION_INSTALLABLE, title: 'Ingress' },
+ }, {
+ ...INITIAL_APP_MAP,
+ helm: { status: APPLICATION_INSTALLED, title: 'Helm Tiller' },
+ ingress: { status: APPLICATION_INSTALLED, title: 'Ingress' },
+ });
+
+ expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined();
+ expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller, Ingress was successfully installed on your cluster');
+ });
+ });
+
+ describe('updateContainer', () => {
+ describe('when creating cluster', () => {
+ it('should show the creating container', () => {
+ cluster.updateContainer(null, 'creating');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+
+ it('should continue to show `creating` banner with subsequent updates of the same status', () => {
+ cluster.updateContainer('creating', 'creating');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('when cluster is created', () => {
+ it('should show the success container', () => {
+ cluster.updateContainer(null, 'created');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+
+ it('should not show a banner when status is already `created`', () => {
+ cluster.updateContainer('created', 'created');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('when cluster has error', () => {
+ it('should show the error container', () => {
+ cluster.updateContainer(null, 'errored', 'this is an error');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+
+ expect(
+ cluster.errorReasonContainer.textContent,
+ ).toContain('this is an error');
+ });
+
+ it('should show `error` banner when previously `creating`', () => {
+ cluster.updateContainer('creating', 'errored');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ });
+ });
+ });
+
+ describe('installApplication', () => {
+ it('tries to install helm', (done) => {
+ spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+ expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
+
+ cluster.installApplication('helm');
+
+ expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING);
+ expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('helm');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUCCESS);
+ expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('tries to install ingress', (done) => {
+ spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+ expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null);
+
+ cluster.installApplication('ingress');
+
+ expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_LOADING);
+ expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_SUCCESS);
+ expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('tries to install runner', (done) => {
+ spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+ expect(cluster.store.state.applications.runner.requestStatus).toEqual(null);
+
+ cluster.installApplication('runner');
+
+ expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_LOADING);
+ expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('runner');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_SUCCESS);
+ expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('sets error request status when the request fails', (done) => {
+ spyOn(cluster.service, 'installApplication').and.returnValue(Promise.reject(new Error('STUBBED ERROR')));
+ expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
+
+ cluster.installApplication('helm');
+
+ expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING);
+ expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
+ expect(cluster.service.installApplication).toHaveBeenCalled();
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_FAILURE);
+ expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/javascripts/clusters/components/application_row_spec.js
new file mode 100644
index 00000000000..e671c18e1a5
--- /dev/null
+++ b/spec/javascripts/clusters/components/application_row_spec.js
@@ -0,0 +1,237 @@
+import Vue from 'vue';
+import eventHub from '~/clusters/event_hub';
+import {
+ APPLICATION_NOT_INSTALLABLE,
+ APPLICATION_SCHEDULED,
+ APPLICATION_INSTALLABLE,
+ APPLICATION_INSTALLING,
+ APPLICATION_INSTALLED,
+ APPLICATION_ERROR,
+ REQUEST_LOADING,
+ REQUEST_SUCCESS,
+ REQUEST_FAILURE,
+} from '~/clusters/constants';
+import applicationRow from '~/clusters/components/application_row.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
+
+describe('Application Row', () => {
+ let vm;
+ let ApplicationRow;
+
+ beforeEach(() => {
+ ApplicationRow = Vue.extend(applicationRow);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('Title', () => {
+ it('shows title', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ titleLink: null,
+ });
+ const title = vm.$el.querySelector('.js-cluster-application-title');
+
+ expect(title.tagName).toEqual('SPAN');
+ expect(title.textContent.trim()).toEqual(DEFAULT_APPLICATION_STATE.title);
+ });
+
+ it('shows title link', () => {
+ expect(DEFAULT_APPLICATION_STATE.titleLink).toBeDefined();
+
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ });
+ const title = vm.$el.querySelector('.js-cluster-application-title');
+
+ expect(title.tagName).toEqual('A');
+ expect(title.textContent.trim()).toEqual(DEFAULT_APPLICATION_STATE.title);
+ });
+ });
+
+ describe('Install button', () => {
+ it('has indeterminate state on page load', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: null,
+ });
+
+ expect(vm.installButtonLabel).toBeUndefined();
+ });
+
+ it('has disabled "Install" when APPLICATION_NOT_INSTALLABLE', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_NOT_INSTALLABLE,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Install');
+ expect(vm.installButtonLoading).toEqual(false);
+ expect(vm.installButtonDisabled).toEqual(true);
+ });
+
+ it('has enabled "Install" when APPLICATION_INSTALLABLE', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Install');
+ expect(vm.installButtonLoading).toEqual(false);
+ expect(vm.installButtonDisabled).toEqual(false);
+ });
+
+ it('has loading "Installing" when APPLICATION_SCHEDULED', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_SCHEDULED,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Installing');
+ expect(vm.installButtonLoading).toEqual(true);
+ expect(vm.installButtonDisabled).toEqual(true);
+ });
+
+ it('has loading "Installing" when APPLICATION_INSTALLING', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLING,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Installing');
+ expect(vm.installButtonLoading).toEqual(true);
+ expect(vm.installButtonDisabled).toEqual(true);
+ });
+
+ it('has disabled "Installed" when APPLICATION_INSTALLED', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLED,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Installed');
+ expect(vm.installButtonLoading).toEqual(false);
+ expect(vm.installButtonDisabled).toEqual(true);
+ });
+
+ it('has enabled "Install" when APPLICATION_ERROR', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_ERROR,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Install');
+ expect(vm.installButtonLoading).toEqual(false);
+ expect(vm.installButtonDisabled).toEqual(false);
+ });
+
+ it('has loading "Install" when REQUEST_LOADING', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ requestStatus: REQUEST_LOADING,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Install');
+ expect(vm.installButtonLoading).toEqual(true);
+ expect(vm.installButtonDisabled).toEqual(true);
+ });
+
+ it('has disabled "Install" when REQUEST_SUCCESS', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ requestStatus: REQUEST_SUCCESS,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Install');
+ expect(vm.installButtonLoading).toEqual(false);
+ expect(vm.installButtonDisabled).toEqual(true);
+ });
+
+ it('has enabled "Install" when REQUEST_FAILURE (so you can try installing again)', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ requestStatus: REQUEST_FAILURE,
+ });
+
+ expect(vm.installButtonLabel).toEqual('Install');
+ expect(vm.installButtonLoading).toEqual(false);
+ expect(vm.installButtonDisabled).toEqual(false);
+ });
+
+ it('clicking install button emits event', () => {
+ spyOn(eventHub, '$emit');
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ });
+ const installButton = vm.$el.querySelector('.js-cluster-application-install-button');
+
+ installButton.click();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', DEFAULT_APPLICATION_STATE.id);
+ });
+
+ it('clicking disabled install button emits nothing', () => {
+ spyOn(eventHub, '$emit');
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLING,
+ });
+ const installButton = vm.$el.querySelector('.js-cluster-application-install-button');
+
+ expect(vm.installButtonDisabled).toEqual(true);
+
+ installButton.click();
+
+ expect(eventHub.$emit).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Error block', () => {
+ it('does not show error block when there is no error', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: null,
+ requestStatus: null,
+ });
+ const generalErrorMessage = vm.$el.querySelector('.js-cluster-application-general-error-message');
+
+ expect(generalErrorMessage).toBeNull();
+ });
+
+ it('shows status reason when APPLICATION_ERROR', () => {
+ const statusReason = 'We broke it 0.0';
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_ERROR,
+ statusReason,
+ });
+ const generalErrorMessage = vm.$el.querySelector('.js-cluster-application-general-error-message');
+ const statusErrorMessage = vm.$el.querySelector('.js-cluster-application-status-error-message');
+
+ expect(generalErrorMessage.textContent.trim()).toEqual(`Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`);
+ expect(statusErrorMessage.textContent.trim()).toEqual(statusReason);
+ });
+
+ it('shows request reason when REQUEST_FAILURE', () => {
+ const requestReason = 'We broke thre request 0.0';
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ requestStatus: REQUEST_FAILURE,
+ requestReason,
+ });
+ const generalErrorMessage = vm.$el.querySelector('.js-cluster-application-general-error-message');
+ const requestErrorMessage = vm.$el.querySelector('.js-cluster-application-request-error-message');
+
+ expect(generalErrorMessage.textContent.trim()).toEqual(`Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`);
+ expect(requestErrorMessage.textContent.trim()).toEqual(requestReason);
+ });
+ });
+});
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
new file mode 100644
index 00000000000..7460da031c4
--- /dev/null
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import applications from '~/clusters/components/applications.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Applications', () => {
+ let vm;
+ let Applications;
+
+ beforeEach(() => {
+ Applications = Vue.extend(applications);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('', () => {
+ beforeEach(() => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller' },
+ ingress: { title: 'Ingress' },
+ runner: { title: 'GitLab Runner' },
+ },
+ });
+ });
+
+ it('renders a row for Helm Tiller', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-helm')).toBeDefined();
+ });
+
+ it('renders a row for Ingress', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).toBeDefined();
+ });
+
+ /* * /
+ it('renders a row for GitLab Runner', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined();
+ });
+ /* */
+ });
+});
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
new file mode 100644
index 00000000000..af6b6a73819
--- /dev/null
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -0,0 +1,50 @@
+import {
+ APPLICATION_INSTALLABLE,
+ APPLICATION_INSTALLING,
+ APPLICATION_ERROR,
+} from '~/clusters/constants';
+
+const CLUSTERS_MOCK_DATA = {
+ GET: {
+ '/gitlab-org/gitlab-shell/clusters/1/status.json': {
+ data: {
+ status: 'errored',
+ status_reason: 'Failed to request to CloudPlatform.',
+ applications: [{
+ name: 'helm',
+ status: APPLICATION_INSTALLABLE,
+ status_reason: null,
+ }, {
+ name: 'ingress',
+ status: APPLICATION_ERROR,
+ status_reason: 'Cannot connect',
+ }, {
+ name: 'runner',
+ status: APPLICATION_INSTALLING,
+ status_reason: null,
+ }],
+ },
+ },
+ },
+ POST: {
+ '/gitlab-org/gitlab-shell/clusters/1/applications/helm': { },
+ '/gitlab-org/gitlab-shell/clusters/1/applications/ingress': { },
+ '/gitlab-org/gitlab-shell/clusters/1/applications/runner': { },
+ },
+};
+
+const DEFAULT_APPLICATION_STATE = {
+ id: 'some-app',
+ title: 'My App',
+ titleLink: 'https://about.gitlab.com/',
+ description: 'Some description about this interesting application!',
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+};
+
+export {
+ CLUSTERS_MOCK_DATA,
+ DEFAULT_APPLICATION_STATE,
+};
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
new file mode 100644
index 00000000000..cb8b3d38e2e
--- /dev/null
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -0,0 +1,89 @@
+import ClustersStore from '~/clusters/stores/clusters_store';
+import { APPLICATION_INSTALLING } from '~/clusters/constants';
+import { CLUSTERS_MOCK_DATA } from '../services/mock_data';
+
+describe('Clusters Store', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new ClustersStore();
+ });
+
+ describe('updateStatus', () => {
+ it('should store new status', () => {
+ expect(store.state.status).toEqual(null);
+
+ const newStatus = 'errored';
+ store.updateStatus(newStatus);
+
+ expect(store.state.status).toEqual(newStatus);
+ });
+ });
+
+ describe('updateStatusReason', () => {
+ it('should store new reason', () => {
+ expect(store.state.statusReason).toEqual(null);
+
+ const newReason = 'Something went wrong!';
+ store.updateStatusReason(newReason);
+
+ expect(store.state.statusReason).toEqual(newReason);
+ });
+ });
+
+ describe('updateAppProperty', () => {
+ it('should store new request status', () => {
+ expect(store.state.applications.helm.requestStatus).toEqual(null);
+
+ const newStatus = APPLICATION_INSTALLING;
+ store.updateAppProperty('helm', 'requestStatus', newStatus);
+
+ expect(store.state.applications.helm.requestStatus).toEqual(newStatus);
+ });
+
+ it('should store new request reason', () => {
+ expect(store.state.applications.helm.requestReason).toEqual(null);
+
+ const newReason = 'We broke it.';
+ store.updateAppProperty('helm', 'requestReason', newReason);
+
+ expect(store.state.applications.helm.requestReason).toEqual(newReason);
+ });
+ });
+
+ describe('updateStateFromServer', () => {
+ it('should store new polling data from server', () => {
+ const mockResponseData = CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/1/status.json'].data;
+ store.updateStateFromServer(mockResponseData);
+
+ expect(store.state).toEqual({
+ helpPath: null,
+ status: mockResponseData.status,
+ statusReason: mockResponseData.status_reason,
+ applications: {
+ helm: {
+ title: 'Helm Tiller',
+ status: mockResponseData.applications[0].status,
+ statusReason: mockResponseData.applications[0].status_reason,
+ requestStatus: null,
+ requestReason: null,
+ },
+ ingress: {
+ title: 'Ingress',
+ status: mockResponseData.applications[1].status,
+ statusReason: mockResponseData.applications[1].status_reason,
+ requestStatus: null,
+ requestReason: null,
+ },
+ runner: {
+ title: 'GitLab Runner',
+ status: mockResponseData.applications[2].status,
+ statusReason: mockResponseData.applications[2].status_reason,
+ requestStatus: null,
+ requestReason: null,
+ },
+ },
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/clusters_spec.js b/spec/javascripts/clusters_spec.js
deleted file mode 100644
index eb1cd6eb804..00000000000
--- a/spec/javascripts/clusters_spec.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import Clusters from '~/clusters';
-
-describe('Clusters', () => {
- let cluster;
- preloadFixtures('clusters/show_cluster.html.raw');
-
- beforeEach(() => {
- loadFixtures('clusters/show_cluster.html.raw');
- cluster = new Clusters();
- });
-
- describe('toggle', () => {
- it('should update the button and the input field on click', () => {
- cluster.toggleButton.click();
-
- expect(
- cluster.toggleButton.classList,
- ).not.toContain('checked');
-
- expect(
- cluster.toggleInput.getAttribute('value'),
- ).toEqual('false');
- });
- });
-
- describe('updateContainer', () => {
- describe('when creating cluster', () => {
- it('should show the creating container', () => {
- cluster.updateContainer('creating');
-
- expect(
- cluster.creatingContainer.classList.contains('hidden'),
- ).toBeFalsy();
- expect(
- cluster.successContainer.classList.contains('hidden'),
- ).toBeTruthy();
- expect(
- cluster.errorContainer.classList.contains('hidden'),
- ).toBeTruthy();
- });
- });
-
- describe('when cluster is created', () => {
- it('should show the success container', () => {
- cluster.updateContainer('created');
-
- expect(
- cluster.creatingContainer.classList.contains('hidden'),
- ).toBeTruthy();
- expect(
- cluster.successContainer.classList.contains('hidden'),
- ).toBeFalsy();
- expect(
- cluster.errorContainer.classList.contains('hidden'),
- ).toBeTruthy();
- });
- });
-
- describe('when cluster has error', () => {
- it('should show the error container', () => {
- cluster.updateContainer('errored', 'this is an error');
-
- expect(
- cluster.creatingContainer.classList.contains('hidden'),
- ).toBeTruthy();
- expect(
- cluster.successContainer.classList.contains('hidden'),
- ).toBeTruthy();
- expect(
- cluster.errorContainer.classList.contains('hidden'),
- ).toBeFalsy();
-
- expect(
- cluster.errorReasonContainer.textContent,
- ).toContain('this is an error');
- });
- });
- });
-});
diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js
index ace95000468..e5a5e3293b9 100644
--- a/spec/javascripts/commits_spec.js
+++ b/spec/javascripts/commits_spec.js
@@ -1,77 +1,73 @@
-/* global CommitsList */
-
import 'vendor/jquery.endless-scroll';
import '~/pager';
-import '~/commits';
-
-(() => {
- describe('Commits List', () => {
- beforeEach(() => {
- setFixtures(`
- <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master">
- <input id="commits-search">
- </form>
- <ol id="commits-list"></ol>
- `);
- });
+import CommitsList from '~/commits';
- it('should be defined', () => {
- expect(CommitsList).toBeDefined();
- });
+describe('Commits List', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master">
+ <input id="commits-search">
+ </form>
+ <ol id="commits-list"></ol>
+ `);
+ });
- describe('processCommits', () => {
- it('should join commit headers', () => {
- CommitsList.$contentList = $(`
- <div>
- <li class="commit-header" data-day="2016-09-20">
- <span class="day">20 Sep, 2016</span>
- <span class="commits-count">1 commit</span>
- </li>
- <li class="commit"></li>
- </div>
- `);
+ it('should be defined', () => {
+ expect(CommitsList).toBeDefined();
+ });
- const data = `
+ describe('processCommits', () => {
+ it('should join commit headers', () => {
+ CommitsList.$contentList = $(`
+ <div>
<li class="commit-header" data-day="2016-09-20">
<span class="day">20 Sep, 2016</span>
<span class="commits-count">1 commit</span>
</li>
<li class="commit"></li>
- `;
+ </div>
+ `);
- // The last commit header should be removed
- // since the previous one has the same data-day value.
- expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0);
- });
+ const data = `
+ <li class="commit-header" data-day="2016-09-20">
+ <span class="day">20 Sep, 2016</span>
+ <span class="commits-count">1 commit</span>
+ </li>
+ <li class="commit"></li>
+ `;
+
+ // The last commit header should be removed
+ // since the previous one has the same data-day value.
+ expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0);
});
+ });
- describe('on entering input', () => {
- let ajaxSpy;
+ describe('on entering input', () => {
+ let ajaxSpy;
- beforeEach(() => {
- CommitsList.init(25);
- CommitsList.searchField.val('');
+ beforeEach(() => {
+ CommitsList.init(25);
+ CommitsList.searchField.val('');
- spyOn(history, 'replaceState').and.stub();
- ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => {
- req.success({
- data: '<li>Result</li>',
- });
+ spyOn(history, 'replaceState').and.stub();
+ ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => {
+ req.success({
+ data: '<li>Result</li>',
});
});
+ });
- it('should save the last search string', () => {
- CommitsList.searchField.val('GitLab');
- CommitsList.filterResults();
- expect(ajaxSpy).toHaveBeenCalled();
- expect(CommitsList.lastSearch).toEqual('GitLab');
- });
+ it('should save the last search string', () => {
+ CommitsList.searchField.val('GitLab');
+ CommitsList.filterResults();
+ expect(ajaxSpy).toHaveBeenCalled();
+ expect(CommitsList.lastSearch).toEqual('GitLab');
+ });
- it('should not make ajax call if the input does not change', () => {
- CommitsList.filterResults();
- expect(ajaxSpy).not.toHaveBeenCalled();
- expect(CommitsList.lastSearch).toEqual('');
- });
+ it('should not make ajax call if the input does not change', () => {
+ CommitsList.filterResults();
+ expect(ajaxSpy).not.toHaveBeenCalled();
+ expect(CommitsList.lastSearch).toEqual('');
});
});
-})();
+});
diff --git a/spec/javascripts/copy_as_gfm_spec.js b/spec/javascripts/copy_as_gfm_spec.js
deleted file mode 100644
index ded450749d3..00000000000
--- a/spec/javascripts/copy_as_gfm_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import '~/copy_as_gfm';
-
-(() => {
- describe('gl.CopyAsGFM', () => {
- describe('gl.CopyAsGFM.pasteGFM', () => {
- function callPasteGFM() {
- const e = {
- originalEvent: {
- clipboardData: {
- getData(mimeType) {
- // When GFM code is copied, we put the regular plain text
- // on the clipboard as `text/plain`, and the GFM as `text/x-gfm`.
- // This emulates the behavior of `getData` with that data.
- if (mimeType === 'text/plain') {
- return 'code';
- }
- if (mimeType === 'text/x-gfm') {
- return '`code`';
- }
- return null;
- },
- },
- },
- preventDefault() {},
- };
-
- window.gl.CopyAsGFM.pasteGFM(e);
- }
-
- it('wraps pasted code when not already in code tags', () => {
- spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => {
- const insertedText = textFunc('This is code: ', '');
- expect(insertedText).toEqual('`code`');
- });
-
- callPasteGFM();
- });
-
- it('does not wrap pasted code when already in code tags', () => {
- spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => {
- const insertedText = textFunc('This is code: `', '`');
- expect(insertedText).toEqual('code');
- });
-
- callPasteGFM();
- });
- });
- });
-})();
diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js
index 3391cade541..0f7bf9ec712 100644
--- a/spec/javascripts/datetime_utility_spec.js
+++ b/spec/javascripts/datetime_utility_spec.js
@@ -1,4 +1,4 @@
-import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
+import * as datetimeUtility from '~/lib/utils/datetime_utility';
(() => {
describe('Date time utils', () => {
@@ -89,10 +89,22 @@ import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
describe('timeIntervalInWords', () => {
it('should return string with number of minutes and seconds', () => {
- expect(timeIntervalInWords(9.54)).toEqual('9 seconds');
- expect(timeIntervalInWords(1)).toEqual('1 second');
- expect(timeIntervalInWords(200)).toEqual('3 minutes 20 seconds');
- expect(timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds');
+ expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds');
+ expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second');
+ expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds');
+ expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds');
+ });
+ });
+
+ describe('dateInWords', () => {
+ const date = new Date('07/01/2016');
+
+ it('should return date in words', () => {
+ expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016');
+ });
+
+ it('should return abbreviated month name', () => {
+ expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016');
});
});
})();
diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js
index 1ef494a00b8..1225fe2cb66 100644
--- a/spec/javascripts/droplab/drop_down_spec.js
+++ b/spec/javascripts/droplab/drop_down_spec.js
@@ -279,7 +279,12 @@ describe('DropDown', function () {
describe('addEvents', function () {
beforeEach(function () {
this.list = { addEventListener: () => {} };
- this.dropdown = { list: this.list, clickEvent: () => {}, eventWrapper: {} };
+ this.dropdown = {
+ list: this.list,
+ clickEvent: () => {},
+ closeDropdown: () => {},
+ eventWrapper: {},
+ };
spyOn(this.list, 'addEventListener');
@@ -288,6 +293,7 @@ describe('DropDown', function () {
it('should call .addEventListener', function () {
expect(this.list.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function));
+ expect(this.list.addEventListener).toHaveBeenCalledWith('keyup', jasmine.any(Function));
});
});
diff --git a/spec/javascripts/droplab/hook_spec.js b/spec/javascripts/droplab/hook_spec.js
index 75bf5f3d611..3d39bd0812b 100644
--- a/spec/javascripts/droplab/hook_spec.js
+++ b/spec/javascripts/droplab/hook_spec.js
@@ -24,7 +24,7 @@ describe('Hook', function () {
});
it('should call DropDown constructor', function () {
- expect(dropdownSrc.default).toHaveBeenCalledWith(this.list);
+ expect(dropdownSrc.default).toHaveBeenCalledWith(this.list, this.config);
});
it('should set .type', function () {
diff --git a/spec/javascripts/emoji_spec.js b/spec/javascripts/emoji_spec.js
index fa11c602ec3..124d91f4477 100644
--- a/spec/javascripts/emoji_spec.js
+++ b/spec/javascripts/emoji_spec.js
@@ -1,6 +1,7 @@
import { glEmojiTag } from '~/emoji';
import isEmojiUnicodeSupported, {
isFlagEmoji,
+ isRainbowFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji,
@@ -217,6 +218,24 @@ describe('gl_emoji', () => {
});
});
+ describe('isRainbowFlagEmoji', () => {
+ it('should gracefully handle empty string', () => {
+ expect(isRainbowFlagEmoji('')).toBeFalsy();
+ });
+ it('should detect rainbow_flag', () => {
+ expect(isRainbowFlagEmoji('ðŸ³ðŸŒˆ')).toBeTruthy();
+ });
+ it('should not detect flag_white on its\' own', () => {
+ expect(isRainbowFlagEmoji('ðŸ³')).toBeFalsy();
+ });
+ it('should not detect rainbow on its\' own', () => {
+ expect(isRainbowFlagEmoji('🌈')).toBeFalsy();
+ });
+ it('should not detect flag_white with something else', () => {
+ expect(isRainbowFlagEmoji('ðŸ³ðŸ”µ')).toBeFalsy();
+ });
+ });
+
describe('isKeycapEmoji', () => {
it('should gracefully handle empty string', () => {
expect(isKeycapEmoji('')).toBeFalsy();
diff --git a/spec/javascripts/environments/emtpy_state_spec.js b/spec/javascripts/environments/emtpy_state_spec.js
new file mode 100644
index 00000000000..82de35933f5
--- /dev/null
+++ b/spec/javascripts/environments/emtpy_state_spec.js
@@ -0,0 +1,57 @@
+
+import Vue from 'vue';
+import emptyState from '~/environments/components/empty_state.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('environments empty state', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(emptyState);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('With permissions', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ newPath: 'foo',
+ canCreateEnvironment: true,
+ helpPath: 'bar',
+ });
+ });
+
+ it('renders empty state and new environment button', () => {
+ expect(
+ vm.$el.querySelector('.js-blank-state-title').textContent.trim(),
+ ).toEqual('You don\'t have any environments right now.');
+
+ expect(
+ vm.$el.querySelector('.js-new-environment-button').getAttribute('href'),
+ ).toEqual('foo');
+ });
+ });
+
+ describe('Without permission', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ newPath: 'foo',
+ canCreateEnvironment: false,
+ helpPath: 'bar',
+ });
+ });
+
+ it('renders empty state without new button', () => {
+ expect(
+ vm.$el.querySelector('.js-blank-state-title').textContent.trim(),
+ ).toEqual('You don\'t have any environments right now.');
+
+ expect(
+ vm.$el.querySelector('.js-new-environment-button'),
+ ).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js
deleted file mode 100644
index 0c8817a8148..00000000000
--- a/spec/javascripts/environments/environment_spec.js
+++ /dev/null
@@ -1,292 +0,0 @@
-import Vue from 'vue';
-import '~/flash';
-import environmentsComponent from '~/environments/components/environment.vue';
-import { environment, folder } from './mock_data';
-import { headersInterceptor } from '../helpers/vue_resource_helper';
-
-describe('Environment', () => {
- preloadFixtures('static/environments/environments.html.raw');
-
- let EnvironmentsComponent;
- let component;
-
- beforeEach(() => {
- loadFixtures('static/environments/environments.html.raw');
-
- EnvironmentsComponent = Vue.extend(environmentsComponent);
- });
-
- describe('successfull request', () => {
- describe('without environments', () => {
- const environmentsEmptyResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 200,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
- Vue.http.interceptors.push(headersInterceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsEmptyResponseInterceptor,
- );
- Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
- });
-
- it('should render the empty state', (done) => {
- component = new EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- });
-
- setTimeout(() => {
- expect(
- component.$el.querySelector('.js-new-environment-button').textContent,
- ).toContain('New environment');
-
- expect(
- component.$el.querySelector('.js-blank-state-title').textContent,
- ).toContain('You don\'t have any environments right now.');
-
- done();
- }, 0);
- });
- });
-
- describe('with paginated environments', () => {
- const environmentsResponseInterceptor = (request, next) => {
- next((response) => {
- response.headers.set('X-nExt-pAge', '2');
- });
-
- next(request.respondWith(JSON.stringify({
- environments: [environment],
- stopped_count: 1,
- available_count: 0,
- }), {
- status: 200,
- headers: {
- 'X-nExt-pAge': '2',
- 'x-page': '1',
- 'X-Per-Page': '1',
- 'X-Prev-Page': '',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '2',
- },
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsResponseInterceptor);
- Vue.http.interceptors.push(headersInterceptor);
- component = new EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- });
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsResponseInterceptor,
- );
- Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
- });
-
- it('should render a table with environments', (done) => {
- setTimeout(() => {
- expect(component.$el.querySelectorAll('table')).toBeDefined();
- expect(
- component.$el.querySelector('.environment-name').textContent.trim(),
- ).toEqual(environment.name);
- done();
- }, 0);
- });
-
- describe('pagination', () => {
- afterEach(() => {
- window.history.pushState({}, null, '');
- });
-
- it('should render pagination', (done) => {
- setTimeout(() => {
- expect(
- component.$el.querySelectorAll('.gl-pagination li').length,
- ).toEqual(5);
- done();
- }, 0);
- });
-
- it('should update url when no search params are present', (done) => {
- spyOn(gl.utils, 'visitUrl');
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
- done();
- }, 0);
- });
-
- it('should update url when page is already present', (done) => {
- spyOn(gl.utils, 'visitUrl');
- window.history.pushState({}, null, '?page=1');
-
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
- done();
- }, 0);
- });
-
- it('should update url when page and scope are already present', (done) => {
- spyOn(gl.utils, 'visitUrl');
- window.history.pushState({}, null, '?scope=all&page=1');
-
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2');
- done();
- }, 0);
- });
-
- it('should update url when page and scope are already present and page is first param', (done) => {
- spyOn(gl.utils, 'visitUrl');
- window.history.pushState({}, null, '?page=1&scope=all');
-
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
- done();
- }, 0);
- });
- });
- });
- });
-
- describe('unsuccessfull request', () => {
- const environmentsErrorResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 500,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsErrorResponseInterceptor,
- );
- });
-
- it('should render empty state', (done) => {
- component = new EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- });
-
- setTimeout(() => {
- expect(
- component.$el.querySelector('.js-blank-state-title').textContent,
- ).toContain('You don\'t have any environments right now.');
- done();
- }, 0);
- });
- });
-
- describe('expandable folders', () => {
- const environmentsResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({
- environments: [folder],
- stopped_count: 0,
- available_count: 1,
- }), {
- status: 200,
- headers: {
- 'X-nExt-pAge': '2',
- 'x-page': '1',
- 'X-Per-Page': '1',
- 'X-Prev-Page': '',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '2',
- },
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsResponseInterceptor);
- component = new EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- });
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsResponseInterceptor,
- );
- });
-
- it('should open a closed folder', (done) => {
- setTimeout(() => {
- component.$el.querySelector('.folder-name').click();
-
- Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.folder-icon i.fa-caret-right').getAttribute('style'),
- ).toContain('display: none');
- expect(
- component.$el.querySelector('.folder-icon i.fa-caret-down').getAttribute('style'),
- ).not.toContain('display: none');
- done();
- });
- });
- });
-
- it('should close an opened folder', (done) => {
- setTimeout(() => {
- // open folder
- component.$el.querySelector('.folder-name').click();
-
- Vue.nextTick(() => {
- // close folder
- component.$el.querySelector('.folder-name').click();
-
- Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.folder-icon i.fa-caret-down').getAttribute('style'),
- ).toContain('display: none');
- expect(
- component.$el.querySelector('.folder-icon i.fa-caret-right').getAttribute('style'),
- ).not.toContain('display: none');
- done();
- });
- });
- });
- });
-
- it('should show children environments and a button to show all environments', (done) => {
- setTimeout(() => {
- // open folder
- component.$el.querySelector('.folder-name').click();
-
- Vue.nextTick(() => {
- const folderInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({
- environments: [environment],
- }), { status: 200 }));
- };
-
- Vue.http.interceptors.push(folderInterceptor);
-
- // wait for next async request
- setTimeout(() => {
- expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
- expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all');
-
- Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor);
- done();
- });
- });
- });
- });
- });
-});
diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js
index 2862971bec4..9bd42863759 100644
--- a/spec/javascripts/environments/environment_table_spec.js
+++ b/spec/javascripts/environments/environment_table_spec.js
@@ -1,10 +1,17 @@
import Vue from 'vue';
import environmentTableComp from '~/environments/components/environments_table.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Environment table', () => {
+ let Component;
+ let vm;
-describe('Environment item', () => {
- preloadFixtures('static/environments/element.html.raw');
beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
+ Component = Vue.extend(environmentTableComp);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
});
it('Should render a table', () => {
@@ -17,18 +24,12 @@ describe('Environment item', () => {
},
};
- const EnvironmentTable = Vue.extend(environmentTableComp);
-
- const component = new EnvironmentTable({
- el: document.querySelector('.test-dom-element'),
- propsData: {
- environments: [{ mockItem }],
- canCreateDeployment: false,
- canReadEnvironment: true,
- service: {},
- },
- }).$mount();
+ vm = mountComponent(Component, {
+ environments: [mockItem],
+ canCreateDeployment: false,
+ canReadEnvironment: true,
+ });
- expect(component.$el.getAttribute('class')).toContain('ci-table');
+ expect(vm.$el.getAttribute('class')).toContain('ci-table');
});
});
diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js
new file mode 100644
index 00000000000..d02adb25b4e
--- /dev/null
+++ b/spec/javascripts/environments/environments_app_spec.js
@@ -0,0 +1,318 @@
+import Vue from 'vue';
+import environmentsComponent from '~/environments/components/environments_app.vue';
+import { environment, folder } from './mock_data';
+import { headersInterceptor } from '../helpers/vue_resource_helper';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Environment', () => {
+ const mockData = {
+ endpoint: 'environments.json',
+ canCreateEnvironment: true,
+ canCreateDeployment: true,
+ canReadEnvironment: true,
+ cssContainerClass: 'container',
+ newEnvironmentPath: 'environments/new',
+ helpPagePath: 'help',
+ };
+
+ let EnvironmentsComponent;
+ let component;
+
+ beforeEach(() => {
+ EnvironmentsComponent = Vue.extend(environmentsComponent);
+ });
+
+ describe('successfull request', () => {
+ describe('without environments', () => {
+ const environmentsEmptyResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
+ Vue.http.interceptors.push(headersInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsEmptyResponseInterceptor,
+ );
+ Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
+ });
+
+ it('should render the empty state', (done) => {
+ component = mountComponent(EnvironmentsComponent, mockData);
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-new-environment-button').textContent,
+ ).toContain('New environment');
+
+ expect(
+ component.$el.querySelector('.js-blank-state-title').textContent,
+ ).toContain('You don\'t have any environments right now.');
+
+ done();
+ }, 0);
+ });
+ });
+
+ describe('with paginated environments', () => {
+ const environmentsResponseInterceptor = (request, next) => {
+ next((response) => {
+ response.headers.set('X-nExt-pAge', '2');
+ });
+
+ next(request.respondWith(JSON.stringify({
+ environments: [environment],
+ stopped_count: 1,
+ available_count: 0,
+ }), {
+ status: 200,
+ headers: {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ },
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsResponseInterceptor);
+ Vue.http.interceptors.push(headersInterceptor);
+ component = mountComponent(EnvironmentsComponent, mockData);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsResponseInterceptor,
+ );
+ Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
+ });
+
+ it('should render a table with environments', (done) => {
+ setTimeout(() => {
+ expect(component.$el.querySelectorAll('table')).not.toBeNull();
+ expect(
+ component.$el.querySelector('.environment-name').textContent.trim(),
+ ).toEqual(environment.name);
+ done();
+ }, 0);
+ });
+
+ describe('pagination', () => {
+ it('should render pagination', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelectorAll('.gl-pagination li').length,
+ ).toEqual(5);
+ done();
+ }, 0);
+ });
+
+ it('should make an API request when page is clicked', (done) => {
+ spyOn(component, 'updateContent');
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'available', page: '2' });
+ done();
+ }, 0);
+ });
+
+ it('should make an API request when using tabs', (done) => {
+ setTimeout(() => {
+ spyOn(component, 'updateContent');
+ component.$el.querySelector('.js-environments-tab-stopped').click();
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ describe('unsuccessfull request', () => {
+ const environmentsErrorResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 500,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsErrorResponseInterceptor,
+ );
+ });
+
+ it('should render empty state', (done) => {
+ component = mountComponent(EnvironmentsComponent, mockData);
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-blank-state-title').textContent,
+ ).toContain('You don\'t have any environments right now.');
+ done();
+ }, 0);
+ });
+ });
+
+ describe('expandable folders', () => {
+ const environmentsResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({
+ environments: [folder],
+ stopped_count: 0,
+ available_count: 1,
+ }), {
+ status: 200,
+ headers: {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ },
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsResponseInterceptor);
+ component = mountComponent(EnvironmentsComponent, mockData);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsResponseInterceptor,
+ );
+ });
+
+ it('should open a closed folder', (done) => {
+ setTimeout(() => {
+ component.$el.querySelector('.folder-name').click();
+
+ Vue.nextTick(() => {
+ expect(
+ component.$el.querySelector('.folder-icon i.fa-caret-right').getAttribute('style'),
+ ).toContain('display: none');
+ expect(
+ component.$el.querySelector('.folder-icon i.fa-caret-down').getAttribute('style'),
+ ).not.toContain('display: none');
+ done();
+ });
+ });
+ });
+
+ it('should close an opened folder', (done) => {
+ setTimeout(() => {
+ // open folder
+ component.$el.querySelector('.folder-name').click();
+
+ Vue.nextTick(() => {
+ // close folder
+ component.$el.querySelector('.folder-name').click();
+
+ Vue.nextTick(() => {
+ expect(
+ component.$el.querySelector('.folder-icon i.fa-caret-down').getAttribute('style'),
+ ).toContain('display: none');
+ expect(
+ component.$el.querySelector('.folder-icon i.fa-caret-right').getAttribute('style'),
+ ).not.toContain('display: none');
+ done();
+ });
+ });
+ });
+ });
+
+ it('should show children environments and a button to show all environments', (done) => {
+ setTimeout(() => {
+ // open folder
+ component.$el.querySelector('.folder-name').click();
+
+ Vue.nextTick(() => {
+ const folderInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({
+ environments: [environment],
+ }), { status: 200 }));
+ };
+
+ Vue.http.interceptors.push(folderInterceptor);
+
+ // wait for next async request
+ setTimeout(() => {
+ expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
+ expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all');
+
+ Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ describe('methods', () => {
+ const environmentsEmptyResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
+ Vue.http.interceptors.push(headersInterceptor);
+
+ component = mountComponent(EnvironmentsComponent, mockData);
+ spyOn(history, 'pushState').and.stub();
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsEmptyResponseInterceptor,
+ );
+ Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
+ });
+
+ describe('updateContent', () => {
+ it('should set given parameters', (done) => {
+ component.updateContent({ scope: 'stopped', page: '3' })
+ .then(() => {
+ expect(component.page).toEqual('3');
+ expect(component.scope).toEqual('stopped');
+ expect(component.requestData.scope).toEqual('stopped');
+ expect(component.requestData.page).toEqual('3');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('onChangeTab', () => {
+ it('should set page to 1', () => {
+ spyOn(component, 'updateContent');
+ component.onChangeTab('stopped');
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
+ });
+ });
+
+ describe('onChangePage', () => {
+ it('should update page and keep scope', () => {
+ spyOn(component, 'updateContent');
+ component.onChangePage(4);
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index 7e62d356bd2..4ea4d9d7499 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -1,25 +1,28 @@
import Vue from 'vue';
-import '~/flash';
import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
import { environmentsList } from '../mock_data';
import { headersInterceptor } from '../../helpers/vue_resource_helper';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Environments Folder View', () => {
- preloadFixtures('static/environments/environments_folder_view.html.raw');
- let EnvironmentsFolderViewComponent;
+ let Component;
+ let component;
+ const mockData = {
+ endpoint: 'environments.json',
+ folderName: 'review',
+ canCreateDeployment: true,
+ canReadEnvironment: true,
+ cssContainerClass: 'container',
+ };
beforeEach(() => {
- loadFixtures('static/environments/environments_folder_view.html.raw');
- EnvironmentsFolderViewComponent = Vue.extend(environmentsFolderViewComponent);
- window.history.pushState({}, null, 'environments/folders/build');
+ Component = Vue.extend(environmentsFolderViewComponent);
});
afterEach(() => {
- window.history.pushState({}, null, '/');
+ component.$destroy();
});
- let component;
-
describe('successfull request', () => {
const environmentsResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({
@@ -31,10 +34,10 @@ describe('Environments Folder View', () => {
headers: {
'X-nExt-pAge': '2',
'x-page': '1',
- 'X-Per-Page': '1',
+ 'X-Per-Page': '2',
'X-Prev-Page': '',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '2',
+ 'X-TOTAL': '20',
+ 'X-Total-Pages': '10',
},
}));
};
@@ -43,9 +46,7 @@ describe('Environments Folder View', () => {
Vue.http.interceptors.push(environmentsResponseInterceptor);
Vue.http.interceptors.push(headersInterceptor);
- component = new EnvironmentsFolderViewComponent({
- el: document.querySelector('#environments-folder-list-view'),
- });
+ component = mountComponent(Component, mockData);
});
afterEach(() => {
@@ -57,7 +58,7 @@ describe('Environments Folder View', () => {
it('should render a table with environments', (done) => {
setTimeout(() => {
- expect(component.$el.querySelectorAll('table')).toBeDefined();
+ expect(component.$el.querySelectorAll('table')).not.toBeNull();
expect(
component.$el.querySelector('.environment-name').textContent.trim(),
).toEqual(environmentsList[0].name);
@@ -68,11 +69,11 @@ describe('Environments Folder View', () => {
it('should render available tab with count', (done) => {
setTimeout(() => {
expect(
- component.$el.querySelector('.js-available-environments-folder-tab').textContent,
+ component.$el.querySelector('.js-environments-tab-available').textContent,
).toContain('Available');
expect(
- component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
+ component.$el.querySelector('.js-environments-tab-available .badge').textContent,
).toContain('0');
done();
}, 0);
@@ -81,11 +82,11 @@ describe('Environments Folder View', () => {
it('should render stopped tab with count', (done) => {
setTimeout(() => {
expect(
- component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
+ component.$el.querySelector('.js-environments-tab-stopped').textContent,
).toContain('Stopped');
expect(
- component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
+ component.$el.querySelector('.js-environments-tab-stopped .badge').textContent,
).toContain('1');
done();
}, 0);
@@ -94,8 +95,8 @@ describe('Environments Folder View', () => {
it('should render parent folder name', (done) => {
setTimeout(() => {
expect(
- component.$el.querySelector('.js-folder-name').textContent,
- ).toContain('Environments / build');
+ component.$el.querySelector('.js-folder-name').textContent.trim(),
+ ).toContain('Environments / review');
done();
}, 0);
});
@@ -104,52 +105,30 @@ describe('Environments Folder View', () => {
it('should render pagination', (done) => {
setTimeout(() => {
expect(
- component.$el.querySelectorAll('.gl-pagination li').length,
- ).toEqual(5);
+ component.$el.querySelectorAll('.gl-pagination'),
+ ).not.toBeNull();
done();
}, 0);
});
- it('should update url when no search params are present', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ it('should make an API request when changing page', (done) => {
+ spyOn(component, 'updateContent');
setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
- done();
- }, 0);
- });
-
- it('should update url when page is already present', (done) => {
- spyOn(gl.utils, 'visitUrl');
- window.history.pushState({}, null, '?page=1');
+ component.$el.querySelector('.gl-pagination .js-last-button a').click();
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '10' });
done();
}, 0);
});
- it('should update url when page and scope are already present', (done) => {
- spyOn(gl.utils, 'visitUrl');
- window.history.pushState({}, null, '?scope=all&page=1');
-
+ it('should make an API request when using tabs', (done) => {
setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2');
- done();
- }, 0);
- });
-
- it('should update url when page and scope are already present and page is first param', (done) => {
- spyOn(gl.utils, 'visitUrl');
- window.history.pushState({}, null, '?page=1&scope=all');
+ spyOn(component, 'updateContent');
+ component.$el.querySelector('.js-environments-tab-stopped').click();
- setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
done();
- }, 0);
+ });
});
});
});
@@ -172,9 +151,7 @@ describe('Environments Folder View', () => {
});
it('should not render a table', (done) => {
- component = new EnvironmentsFolderViewComponent({
- el: document.querySelector('#environments-folder-list-view'),
- });
+ component = mountComponent(Component, mockData);
setTimeout(() => {
expect(
@@ -187,11 +164,11 @@ describe('Environments Folder View', () => {
it('should render available tab with count 0', (done) => {
setTimeout(() => {
expect(
- component.$el.querySelector('.js-available-environments-folder-tab').textContent,
+ component.$el.querySelector('.js-environments-tab-available').textContent,
).toContain('Available');
expect(
- component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
+ component.$el.querySelector('.js-environments-tab-available .badge').textContent,
).toContain('0');
done();
}, 0);
@@ -200,14 +177,70 @@ describe('Environments Folder View', () => {
it('should render stopped tab with count 0', (done) => {
setTimeout(() => {
expect(
- component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
+ component.$el.querySelector('.js-environments-tab-stopped').textContent,
).toContain('Stopped');
expect(
- component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
+ component.$el.querySelector('.js-environments-tab-stopped .badge').textContent,
).toContain('0');
done();
}, 0);
});
});
+
+ describe('methods', () => {
+ const environmentsEmptyResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
+ Vue.http.interceptors.push(headersInterceptor);
+
+ component = mountComponent(Component, mockData);
+ spyOn(history, 'pushState').and.stub();
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsEmptyResponseInterceptor,
+ );
+ Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
+ });
+
+ describe('updateContent', () => {
+ it('should set given parameters', (done) => {
+ component.updateContent({ scope: 'stopped', page: '4' })
+ .then(() => {
+ expect(component.page).toEqual('4');
+ expect(component.scope).toEqual('stopped');
+ expect(component.requestData.scope).toEqual('stopped');
+ expect(component.requestData.page).toEqual('4');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('onChangeTab', () => {
+ it('should set page to 1', () => {
+ spyOn(component, 'updateContent');
+ component.onChangeTab('stopped');
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
+ });
+ });
+
+ describe('onChangePage', () => {
+ it('should update page and keep scope', () => {
+ spyOn(component, 'updateContent');
+
+ component.onChangePage(4);
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' });
+ });
+ });
+ });
});
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index f209328dee1..230c15e5de6 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -396,6 +396,25 @@ describe('Filtered Search Manager', () => {
});
});
+ describe('Clearing search', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ it('Clicking the "x" clear button, clears the input', () => {
+ const inputValue = 'label:~bug ';
+ manager.filteredSearchInput.value = inputValue;
+ manager.filteredSearchInput.dispatchEvent(new Event('input'));
+
+ expect(gl.DropdownUtils.getSearchQuery()).toEqual(inputValue);
+
+ manager.clearSearchButton.click();
+
+ expect(manager.filteredSearchInput.value).toEqual('');
+ expect(gl.DropdownUtils.getSearchQuery()).toEqual('');
+ });
+ });
+
describe('toggleInputContainerFocus', () => {
beforeEach(() => {
initializeManager();
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
index 67166802c70..2ecb64d84b5 100644
--- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -791,6 +791,29 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
const avatar = tokenValueElement.querySelector('img.avatar');
expect(avatar.src).toBe(dummyUser.avatar_url);
+ expect(avatar.alt).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('escapes user name when creating token', (done) => {
+ const dummyUser = {
+ name: '<script>',
+ avatar_url: `${gl.TEST_HOST}/mypics/avatar.png`,
+ };
+ const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = (username) => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.resolve(dummyUser);
+ };
+
+ subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
+ tokenValueElement.querySelector('.avatar').remove();
+ expect(tokenValueElement.innerHTML.trim()).toBe(_.escape(dummyUser.name));
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb
index 5774f36f026..8e74c4f859c 100644
--- a/spec/javascripts/fixtures/clusters.rb
+++ b/spec/javascripts/fixtures/clusters.rb
@@ -6,7 +6,7 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace) }
- let(:cluster) { project.create_cluster!(gcp_cluster_name: "gke-test-creation-1", gcp_project_id: 'gitlab-internal-153318', gcp_cluster_zone: 'us-central1-a', gcp_cluster_size: '1', project_namespace: 'aaa', gcp_machine_type: 'n1-standard-1')}
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
render_views
diff --git a/spec/javascripts/fixtures/environments/element.html.haml b/spec/javascripts/fixtures/environments/element.html.haml
deleted file mode 100644
index 8d7aeb23356..00000000000
--- a/spec/javascripts/fixtures/environments/element.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-.test-dom-element
diff --git a/spec/javascripts/fixtures/environments/environments.html.haml b/spec/javascripts/fixtures/environments/environments.html.haml
deleted file mode 100644
index e6000fbb553..00000000000
--- a/spec/javascripts/fixtures/environments/environments.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-%div
- #environments-list-view{ data: { environments_data: "foo/environments",
- "can-create-deployment" => "true",
- "can-read-environment" => "true",
- "can-create-environment" => "true",
- "project-environments-path" => "https://gitlab.com/foo/environments",
- "project-stopped-environments-path" => "https://gitlab.com/foo/environments?scope=stopped",
- "new-environment-path" => "https://gitlab.com/foo/environments/new",
- "help-page-path" => "https://gitlab.com/help_page"}}
diff --git a/spec/javascripts/fixtures/environments/environments_folder_view.html.haml b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml
deleted file mode 100644
index aceec139730..00000000000
--- a/spec/javascripts/fixtures/environments/environments_folder_view.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-%div
- #environments-folder-list-view{ data: { "can-create-deployment" => "true",
- "can-read-environment" => "true",
- "css-class" => "",
- "commit-icon-svg" => custom_icon("icon_commit"),
- "terminal-icon-svg" => custom_icon("icon_terminal"),
- "play-icon-svg" => custom_icon("icon_play") } }
diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml
index 97b0c25c923..85ee61f0b54 100644
--- a/spec/javascripts/fixtures/pipelines.html.haml
+++ b/spec/javascripts/fixtures/pipelines.html.haml
@@ -1,16 +1,10 @@
%div
#pipelines-list-vue{ data: { endpoint: 'foo',
- "css-class" => 'foo',
"help-page-path" => 'foo',
+ "help-auto-devops-path" => 'foo',
"empty-state-svg-path" => 'foo',
"error-state-svg-path" => 'foo',
"new-pipeline-path" => 'foo',
"can-create-pipeline" => 'true',
- "all-path" => 'foo',
- "pending-path" => 'foo',
- "running-path" => 'foo',
- "finished-path" => 'foo',
- "branches-path" => 'foo',
- "tags-path" => 'foo',
"has-ci" => 'foo',
"ci-lint-path" => 'foo' } }
diff --git a/spec/javascripts/fixtures/search_autocomplete.html.haml b/spec/javascripts/fixtures/search_autocomplete.html.haml
index 7785120da5b..0421ed2182f 100644
--- a/spec/javascripts/fixtures/search_autocomplete.html.haml
+++ b/spec/javascripts/fixtures/search_autocomplete.html.haml
@@ -8,3 +8,4 @@
%input#search.search-input.dropdown-menu-toggle
.dropdown-menu.dropdown-select
.dropdown-content
+ %input{ type: "hidden", class: "js-search-project-options" }
diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js
new file mode 100644
index 00000000000..97e3ab682c5
--- /dev/null
+++ b/spec/javascripts/flash_spec.js
@@ -0,0 +1,290 @@
+import flash, {
+ createFlashEl,
+ createAction,
+ hideFlash,
+ removeFlashClickListener,
+} from '~/flash';
+
+describe('Flash', () => {
+ describe('createFlashEl', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ });
+
+ afterEach(() => {
+ el.innerHTML = '';
+ });
+
+ it('creates flash element with type', () => {
+ el.innerHTML = createFlashEl('testing', 'alert');
+
+ expect(
+ el.querySelector('.flash-alert'),
+ ).not.toBeNull();
+ });
+
+ it('escapes text', () => {
+ el.innerHTML = createFlashEl('<script>alert("a");</script>', 'alert');
+
+ expect(
+ el.querySelector('.flash-text').textContent.trim(),
+ ).toBe('<script>alert("a");</script>');
+ });
+
+ it('adds container classes when inside content wrapper', () => {
+ el.innerHTML = createFlashEl('testing', 'alert', true);
+
+ expect(
+ el.querySelector('.flash-text').classList.contains('container-fluid'),
+ ).toBeTruthy();
+ expect(
+ el.querySelector('.flash-text').classList.contains('container-limited'),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('hideFlash', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ el.className = 'js-testing';
+ });
+
+ it('sets transition style', () => {
+ hideFlash(el);
+
+ expect(
+ el.style.transition,
+ ).toBe('opacity 0.3s');
+ });
+
+ it('sets opacity style', () => {
+ hideFlash(el);
+
+ expect(
+ el.style.opacity,
+ ).toBe('0');
+ });
+
+ it('does not set styles when fadeTransition is false', () => {
+ hideFlash(el, false);
+
+ expect(
+ el.style.opacity,
+ ).toBe('');
+ expect(
+ el.style.transition,
+ ).toBe('');
+ });
+
+ it('removes element after transitionend', () => {
+ document.body.appendChild(el);
+
+ hideFlash(el);
+ el.dispatchEvent(new Event('transitionend'));
+
+ expect(
+ document.querySelector('.js-testing'),
+ ).toBeNull();
+ });
+
+ it('calls event listener callback once', () => {
+ spyOn(el, 'remove').and.callThrough();
+ document.body.appendChild(el);
+
+ hideFlash(el);
+
+ el.dispatchEvent(new Event('transitionend'));
+ el.dispatchEvent(new Event('transitionend'));
+
+ expect(
+ el.remove.calls.count(),
+ ).toBe(1);
+ });
+ });
+
+ describe('createAction', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ });
+
+ it('creates link with href', () => {
+ el.innerHTML = createAction({
+ href: 'testing',
+ title: 'test',
+ });
+
+ expect(
+ el.querySelector('.flash-action').href,
+ ).toContain('testing');
+ });
+
+ it('uses hash as href when no href is present', () => {
+ el.innerHTML = createAction({
+ title: 'test',
+ });
+
+ expect(
+ el.querySelector('.flash-action').href,
+ ).toContain('#');
+ });
+
+ it('adds role when no href is present', () => {
+ el.innerHTML = createAction({
+ title: 'test',
+ });
+
+ expect(
+ el.querySelector('.flash-action').getAttribute('role'),
+ ).toBe('button');
+ });
+
+ it('escapes the title text', () => {
+ el.innerHTML = createAction({
+ title: '<script>alert("a")</script>',
+ });
+
+ expect(
+ el.querySelector('.flash-action').textContent.trim(),
+ ).toBe('<script>alert("a")</script>');
+ });
+ });
+
+ describe('createFlash', () => {
+ describe('no flash-container', () => {
+ it('does not add to the DOM', () => {
+ const flashEl = flash('testing');
+
+ expect(
+ flashEl,
+ ).toBeNull();
+ expect(
+ document.querySelector('.flash-alert'),
+ ).toBeNull();
+ });
+ });
+
+ describe('with flash-container', () => {
+ beforeEach(() => {
+ document.body.innerHTML += `
+ <div class="content-wrapper js-content-wrapper">
+ <div class="flash-container"></div>
+ </div>
+ `;
+ });
+
+ afterEach(() => {
+ document.querySelector('.js-content-wrapper').remove();
+ });
+
+ it('adds flash element into container', () => {
+ flash('test');
+
+ expect(
+ document.querySelector('.flash-alert'),
+ ).not.toBeNull();
+ });
+
+ it('adds flash into specified parent', () => {
+ flash(
+ 'test',
+ 'alert',
+ document.querySelector('.content-wrapper'),
+ );
+
+ expect(
+ document.querySelector('.content-wrapper .flash-alert'),
+ ).not.toBeNull();
+ });
+
+ it('adds container classes when inside content-wrapper', () => {
+ flash('test');
+
+ expect(
+ document.querySelector('.flash-text').className,
+ ).toBe('flash-text container-fluid container-limited');
+ });
+
+ it('does not add container when outside of content-wrapper', () => {
+ document.querySelector('.content-wrapper').className = 'js-content-wrapper';
+ flash('test');
+
+ expect(
+ document.querySelector('.flash-text').className.trim(),
+ ).toBe('flash-text');
+ });
+
+ it('removes element after clicking', () => {
+ flash('test', 'alert', document, null, false);
+
+ document.querySelector('.flash-alert').click();
+
+ expect(
+ document.querySelector('.flash-alert'),
+ ).toBeNull();
+ });
+
+ describe('with actionConfig', () => {
+ it('adds action link', () => {
+ flash(
+ 'test',
+ 'alert',
+ document,
+ {
+ title: 'test',
+ },
+ );
+
+ expect(
+ document.querySelector('.flash-action'),
+ ).not.toBeNull();
+ });
+
+ it('calls actionConfig clickHandler on click', () => {
+ const actionConfig = {
+ title: 'test',
+ clickHandler: jasmine.createSpy('actionConfig'),
+ };
+
+ flash(
+ 'test',
+ 'alert',
+ document,
+ actionConfig,
+ );
+
+ document.querySelector('.flash-action').click();
+
+ expect(
+ actionConfig.clickHandler,
+ ).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+
+ describe('removeFlashClickListener', () => {
+ beforeEach(() => {
+ document.body.innerHTML += '<div class="flash-container"><div class="flash"></div></div>';
+ });
+
+ it('removes global flash on click', (done) => {
+ const flashEl = document.querySelector('.flash');
+
+ removeFlashClickListener(flashEl, false);
+
+ flashEl.click();
+
+ setTimeout(() => {
+ expect(document.querySelector('.flash')).toBeNull();
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js
index ad0c7264616..6f357306ec7 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js
+++ b/spec/javascripts/gfm_auto_complete_spec.js
@@ -67,6 +67,28 @@ describe('GfmAutoComplete', function () {
});
});
+ describe('DefaultOptions.beforeInsert', () => {
+ const beforeInsert = (context, value) => (
+ gfmAutoCompleteCallbacks.beforeInsert.call(context, value)
+ );
+
+ const atwhoInstance = { setting: { skipSpecialCharacterTest: false } };
+
+ it('should not quote if value only contains alphanumeric charecters', () => {
+ expect(beforeInsert(atwhoInstance, '@user1')).toBe('@user1');
+ expect(beforeInsert(atwhoInstance, '~label1')).toBe('~label1');
+ });
+
+ it('should quote if value contains any non-alphanumeric characters', () => {
+ expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label-20"');
+ expect(beforeInsert(atwhoInstance, '~label 20')).toBe('~"label 20"');
+ });
+
+ it('should quote integer labels', () => {
+ expect(beforeInsert(atwhoInstance, '~1234')).toBe('~"1234"');
+ });
+ });
+
describe('DefaultOptions.matcher', function () {
const defaultMatcher = (context, flag, subtext) => (
gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext)
diff --git a/spec/javascripts/gl_field_errors_spec.js b/spec/javascripts/gl_field_errors_spec.js
index fa24aa426b6..2779686a6f5 100644
--- a/spec/javascripts/gl_field_errors_spec.js
+++ b/spec/javascripts/gl_field_errors_spec.js
@@ -1,110 +1,108 @@
/* eslint-disable space-before-function-paren, arrow-body-style */
-import '~/gl_field_errors';
+import GlFieldErrors from '~/gl_field_errors';
-((global) => {
+describe('GL Style Field Errors', function() {
preloadFixtures('static/gl_field_errors.html.raw');
- describe('GL Style Field Errors', function() {
- beforeEach(function() {
- loadFixtures('static/gl_field_errors.html.raw');
- const $form = this.$form = $('form.gl-show-field-errors');
- this.fieldErrors = new global.GlFieldErrors($form);
- });
+ beforeEach(function() {
+ loadFixtures('static/gl_field_errors.html.raw');
+ const $form = this.$form = $('form.gl-show-field-errors');
+ this.fieldErrors = new GlFieldErrors($form);
+ });
- it('should select the correct input elements', function() {
- expect(this.$form).toBeDefined();
- expect(this.$form.length).toBe(1);
- expect(this.fieldErrors).toBeDefined();
- const inputs = this.fieldErrors.state.inputs;
- expect(inputs.length).toBe(4);
- });
+ it('should select the correct input elements', function() {
+ expect(this.$form).toBeDefined();
+ expect(this.$form.length).toBe(1);
+ expect(this.fieldErrors).toBeDefined();
+ const inputs = this.fieldErrors.state.inputs;
+ expect(inputs.length).toBe(4);
+ });
- it('should ignore elements with custom error handling', function() {
- const customErrorFlag = 'gl-field-error-ignore';
- const customErrorElem = $(`.${customErrorFlag}`);
+ it('should ignore elements with custom error handling', function() {
+ const customErrorFlag = 'gl-field-error-ignore';
+ const customErrorElem = $(`.${customErrorFlag}`);
- expect(customErrorElem.length).toBe(1);
+ expect(customErrorElem.length).toBe(1);
- const customErrors = this.fieldErrors.state.inputs.filter((input) => {
- return input.inputElement.hasClass(customErrorFlag);
- });
- expect(customErrors.length).toBe(0);
+ const customErrors = this.fieldErrors.state.inputs.filter((input) => {
+ return input.inputElement.hasClass(customErrorFlag);
});
+ expect(customErrors.length).toBe(0);
+ });
- it('should not show any errors before submit attempt', function() {
- this.$form.find('.email').val('not-a-valid-email').keyup();
- this.$form.find('.text-required').val('').keyup();
- this.$form.find('.alphanumberic').val('?---*').keyup();
+ it('should not show any errors before submit attempt', function() {
+ this.$form.find('.email').val('not-a-valid-email').keyup();
+ this.$form.find('.text-required').val('').keyup();
+ this.$form.find('.alphanumberic').val('?---*').keyup();
- const errorsShown = this.$form.find('.gl-field-error-outline');
- expect(errorsShown.length).toBe(0);
- });
+ const errorsShown = this.$form.find('.gl-field-error-outline');
+ expect(errorsShown.length).toBe(0);
+ });
- it('should show errors when input valid is submitted', function() {
- this.$form.find('.email').val('not-a-valid-email').keyup();
- this.$form.find('.text-required').val('').keyup();
- this.$form.find('.alphanumberic').val('?---*').keyup();
+ it('should show errors when input valid is submitted', function() {
+ this.$form.find('.email').val('not-a-valid-email').keyup();
+ this.$form.find('.text-required').val('').keyup();
+ this.$form.find('.alphanumberic').val('?---*').keyup();
- this.$form.submit();
+ this.$form.submit();
- const errorsShown = this.$form.find('.gl-field-error-outline');
- expect(errorsShown.length).toBe(4);
- });
+ const errorsShown = this.$form.find('.gl-field-error-outline');
+ expect(errorsShown.length).toBe(4);
+ });
- it('should properly track validity state on input after invalid submission attempt', function() {
- this.$form.submit();
-
- const emailInputModel = this.fieldErrors.state.inputs[1];
- const fieldState = emailInputModel.state;
- const emailInputElement = emailInputModel.inputElement;
-
- // No input
- expect(emailInputElement).toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(true);
- expect(fieldState.valid).toBe(false);
-
- // Then invalid input
- emailInputElement.val('not-a-valid-email').keyup();
- expect(emailInputElement).toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(false);
- expect(fieldState.valid).toBe(false);
-
- // Then valid input
- emailInputElement.val('email@gitlab.com').keyup();
- expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(false);
- expect(fieldState.valid).toBe(true);
-
- // Then invalid input
- emailInputElement.val('not-a-valid-email').keyup();
- expect(emailInputElement).toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(false);
- expect(fieldState.valid).toBe(false);
-
- // Then empty input
- emailInputElement.val('').keyup();
- expect(emailInputElement).toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(true);
- expect(fieldState.valid).toBe(false);
-
- // Then valid input
- emailInputElement.val('email@gitlab.com').keyup();
- expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
- expect(fieldState.empty).toBe(false);
- expect(fieldState.valid).toBe(true);
- });
+ it('should properly track validity state on input after invalid submission attempt', function() {
+ this.$form.submit();
+
+ const emailInputModel = this.fieldErrors.state.inputs[1];
+ const fieldState = emailInputModel.state;
+ const emailInputElement = emailInputModel.inputElement;
+
+ // No input
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(true);
+ expect(fieldState.valid).toBe(false);
+
+ // Then invalid input
+ emailInputElement.val('not-a-valid-email').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(false);
+
+ // Then valid input
+ emailInputElement.val('email@gitlab.com').keyup();
+ expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(true);
+
+ // Then invalid input
+ emailInputElement.val('not-a-valid-email').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(false);
+
+ // Then empty input
+ emailInputElement.val('').keyup();
+ expect(emailInputElement).toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(true);
+ expect(fieldState.valid).toBe(false);
+
+ // Then valid input
+ emailInputElement.val('email@gitlab.com').keyup();
+ expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
+ expect(fieldState.empty).toBe(false);
+ expect(fieldState.valid).toBe(true);
+ });
- it('should properly infer error messages', function() {
- this.$form.submit();
- const trackedInputs = this.fieldErrors.state.inputs;
- const inputHasTitle = trackedInputs[1];
- const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error');
- const inputNoTitle = trackedInputs[2];
- const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error');
+ it('should properly infer error messages', function() {
+ this.$form.submit();
+ const trackedInputs = this.fieldErrors.state.inputs;
+ const inputHasTitle = trackedInputs[1];
+ const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error');
+ const inputNoTitle = trackedInputs[2];
+ const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error');
- expect(noTitleErrorElem.text()).toBe('This field is required.');
- expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
- });
+ expect(noTitleErrorElem.text()).toBe('This field is required.');
+ expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
});
-})(window.gl || (window.gl = {}));
+});
diff --git a/spec/javascripts/gl_form_spec.js b/spec/javascripts/gl_form_spec.js
index 837feacec1d..5a8009e57fd 100644
--- a/spec/javascripts/gl_form_spec.js
+++ b/spec/javascripts/gl_form_spec.js
@@ -1,18 +1,11 @@
-import autosize from 'vendor/autosize';
-import '~/gl_form';
+import Autosize from 'autosize';
+import GLForm from '~/gl_form';
import '~/lib/utils/text_utility';
import '~/lib/utils/common_utils';
-window.autosize = autosize;
+window.autosize = Autosize;
describe('GLForm', () => {
- const global = window.gl || (window.gl = {});
- const GLForm = global.GLForm;
-
- it('should be defined in the global scope', () => {
- expect(GLForm).toBeDefined();
- });
-
describe('when instantiated', function () {
beforeEach((done) => {
this.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>');
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
new file mode 100644
index 00000000000..59d4f7c45c6
--- /dev/null
+++ b/spec/javascripts/groups/components/app_spec.js
@@ -0,0 +1,443 @@
+import Vue from 'vue';
+
+import appComponent from '~/groups/components/app.vue';
+import groupFolderComponent from '~/groups/components/group_folder.vue';
+import groupItemComponent from '~/groups/components/group_item.vue';
+
+import eventHub from '~/groups/event_hub';
+import GroupsStore from '~/groups/store/groups_store';
+import GroupsService from '~/groups/service/groups_service';
+
+import {
+ mockEndpoint, mockGroups, mockSearchedGroups,
+ mockRawPageInfo, mockParentGroupItem, mockRawChildren,
+ mockChildren, mockPageInfo,
+} from '../mock_data';
+
+const createComponent = (hideProjects = false) => {
+ const Component = Vue.extend(appComponent);
+ const store = new GroupsStore(false);
+ const service = new GroupsService(mockEndpoint);
+
+ return new Component({
+ propsData: {
+ store,
+ service,
+ hideProjects,
+ },
+ });
+};
+
+const returnServicePromise = (data, failed) => new Promise((resolve, reject) => {
+ if (failed) {
+ reject(data);
+ } else {
+ resolve({
+ json() {
+ return data;
+ },
+ });
+ }
+});
+
+describe('AppComponent', () => {
+ let vm;
+
+ beforeEach((done) => {
+ Vue.component('group-folder', groupFolderComponent);
+ Vue.component('group-item', groupItemComponent);
+
+ vm = createComponent();
+
+ Vue.nextTick(() => {
+ done();
+ });
+ });
+
+ describe('computed', () => {
+ beforeEach(() => {
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('groups', () => {
+ it('should return list of groups from store', () => {
+ spyOn(vm.store, 'getGroups');
+
+ const groups = vm.groups;
+ expect(vm.store.getGroups).toHaveBeenCalled();
+ expect(groups).not.toBeDefined();
+ });
+ });
+
+ describe('pageInfo', () => {
+ it('should return pagination info from store', () => {
+ spyOn(vm.store, 'getPaginationInfo');
+
+ const pageInfo = vm.pageInfo;
+ expect(vm.store.getPaginationInfo).toHaveBeenCalled();
+ expect(pageInfo).not.toBeDefined();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ beforeEach(() => {
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('fetchGroups', () => {
+ it('should call `getGroups` with all the params provided', (done) => {
+ spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise(mockGroups));
+
+ vm.fetchGroups({
+ parentId: 1,
+ page: 2,
+ filterGroupsBy: 'git',
+ sortBy: 'created_desc',
+ archived: true,
+ });
+ setTimeout(() => {
+ expect(vm.service.getGroups).toHaveBeenCalledWith(1, 2, 'git', 'created_desc', true);
+ done();
+ }, 0);
+ });
+
+ it('should set headers to store for building pagination info when called with `updatePagination`', (done) => {
+ spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise({ headers: mockRawPageInfo }));
+ spyOn(vm, 'updatePagination');
+
+ vm.fetchGroups({ updatePagination: true });
+ setTimeout(() => {
+ expect(vm.service.getGroups).toHaveBeenCalled();
+ expect(vm.updatePagination).toHaveBeenCalled();
+ done();
+ }, 0);
+ });
+
+ it('should show flash error when request fails', (done) => {
+ spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise(null, true));
+ spyOn($, 'scrollTo');
+ spyOn(window, 'Flash');
+
+ vm.fetchGroups({});
+ setTimeout(() => {
+ expect(vm.isLoading).toBeFalsy();
+ expect($.scrollTo).toHaveBeenCalledWith(0);
+ expect(window.Flash).toHaveBeenCalledWith('An error occurred. Please try again.');
+ done();
+ }, 0);
+ });
+ });
+
+ describe('fetchAllGroups', () => {
+ it('should fetch default set of groups', (done) => {
+ spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups));
+ spyOn(vm, 'updatePagination').and.callThrough();
+ spyOn(vm, 'updateGroups').and.callThrough();
+
+ vm.fetchAllGroups();
+ expect(vm.isLoading).toBeTruthy();
+ expect(vm.fetchGroups).toHaveBeenCalled();
+ setTimeout(() => {
+ expect(vm.isLoading).toBeFalsy();
+ expect(vm.updateGroups).toHaveBeenCalled();
+ done();
+ }, 0);
+ });
+
+ it('should fetch matching set of groups when app is loaded with search query', (done) => {
+ spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockSearchedGroups));
+ spyOn(vm, 'updateGroups').and.callThrough();
+
+ vm.fetchAllGroups();
+ expect(vm.fetchGroups).toHaveBeenCalledWith({
+ page: null,
+ filterGroupsBy: null,
+ sortBy: null,
+ updatePagination: true,
+ archived: null,
+ });
+ setTimeout(() => {
+ expect(vm.updateGroups).toHaveBeenCalled();
+ done();
+ }, 0);
+ });
+ });
+
+ describe('fetchPage', () => {
+ it('should fetch groups for provided page details and update window state', (done) => {
+ spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups));
+ spyOn(vm, 'updateGroups').and.callThrough();
+ spyOn(gl.utils, 'mergeUrlParams').and.callThrough();
+ spyOn(window.history, 'replaceState');
+ spyOn($, 'scrollTo');
+
+ vm.fetchPage(2, null, null, true);
+ expect(vm.isLoading).toBeTruthy();
+ expect(vm.fetchGroups).toHaveBeenCalledWith({
+ page: 2,
+ filterGroupsBy: null,
+ sortBy: null,
+ updatePagination: true,
+ archived: true,
+ });
+ setTimeout(() => {
+ expect(vm.isLoading).toBeFalsy();
+ expect($.scrollTo).toHaveBeenCalledWith(0);
+ expect(gl.utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
+ expect(window.history.replaceState).toHaveBeenCalledWith({
+ page: jasmine.any(String),
+ }, jasmine.any(String), jasmine.any(String));
+ expect(vm.updateGroups).toHaveBeenCalled();
+ done();
+ }, 0);
+ });
+ });
+
+ describe('toggleChildren', () => {
+ let groupItem;
+
+ beforeEach(() => {
+ groupItem = Object.assign({}, mockParentGroupItem);
+ groupItem.isOpen = false;
+ groupItem.isChildrenLoading = false;
+ });
+
+ it('should fetch children of given group and expand it if group is collapsed and children are not loaded', (done) => {
+ spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockRawChildren));
+ spyOn(vm.store, 'setGroupChildren');
+
+ vm.toggleChildren(groupItem);
+ expect(groupItem.isChildrenLoading).toBeTruthy();
+ expect(vm.fetchGroups).toHaveBeenCalledWith({
+ parentId: groupItem.id,
+ });
+ setTimeout(() => {
+ expect(vm.store.setGroupChildren).toHaveBeenCalled();
+ done();
+ }, 0);
+ });
+
+ it('should skip network request while expanding group if children are already loaded', () => {
+ spyOn(vm, 'fetchGroups');
+ groupItem.children = mockRawChildren;
+
+ vm.toggleChildren(groupItem);
+ expect(vm.fetchGroups).not.toHaveBeenCalled();
+ expect(groupItem.isOpen).toBeTruthy();
+ });
+
+ it('should collapse group if it is already expanded', () => {
+ spyOn(vm, 'fetchGroups');
+ groupItem.isOpen = true;
+
+ vm.toggleChildren(groupItem);
+ expect(vm.fetchGroups).not.toHaveBeenCalled();
+ expect(groupItem.isOpen).toBeFalsy();
+ });
+
+ it('should set `isChildrenLoading` back to `false` if load request fails', (done) => {
+ spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise({}, true));
+
+ vm.toggleChildren(groupItem);
+ expect(groupItem.isChildrenLoading).toBeTruthy();
+ setTimeout(() => {
+ expect(groupItem.isChildrenLoading).toBeFalsy();
+ done();
+ }, 0);
+ });
+ });
+
+ describe('leaveGroup', () => {
+ let groupItem;
+ let childGroupItem;
+
+ beforeEach(() => {
+ groupItem = Object.assign({}, mockParentGroupItem);
+ groupItem.children = mockChildren;
+ childGroupItem = groupItem.children[0];
+ groupItem.isChildrenLoading = false;
+ });
+
+ it('should leave group and remove group item from tree', (done) => {
+ const notice = `You left the "${childGroupItem.fullName}" group.`;
+ spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ notice }));
+ spyOn(vm.store, 'removeGroup').and.callThrough();
+ spyOn(window, 'Flash');
+ spyOn($, 'scrollTo');
+
+ vm.leaveGroup(childGroupItem, groupItem);
+ expect(childGroupItem.isBeingRemoved).toBeTruthy();
+ expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
+ setTimeout(() => {
+ expect($.scrollTo).toHaveBeenCalledWith(0);
+ expect(vm.store.removeGroup).toHaveBeenCalledWith(childGroupItem, groupItem);
+ expect(window.Flash).toHaveBeenCalledWith(notice, 'notice');
+ done();
+ }, 0);
+ });
+
+ it('should show error flash message if request failed to leave group', (done) => {
+ const message = 'An error occurred. Please try again.';
+ spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ status: 500 }, true));
+ spyOn(vm.store, 'removeGroup').and.callThrough();
+ spyOn(window, 'Flash');
+
+ vm.leaveGroup(childGroupItem, groupItem);
+ expect(childGroupItem.isBeingRemoved).toBeTruthy();
+ expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
+ setTimeout(() => {
+ expect(vm.store.removeGroup).not.toHaveBeenCalled();
+ expect(window.Flash).toHaveBeenCalledWith(message);
+ expect(childGroupItem.isBeingRemoved).toBeFalsy();
+ done();
+ }, 0);
+ });
+
+ it('should show appropriate error flash message if request forbids to leave group', (done) => {
+ const message = 'Failed to leave the group. Please make sure you are not the only owner.';
+ spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ status: 403 }, true));
+ spyOn(vm.store, 'removeGroup').and.callThrough();
+ spyOn(window, 'Flash');
+
+ vm.leaveGroup(childGroupItem, groupItem);
+ expect(childGroupItem.isBeingRemoved).toBeTruthy();
+ expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
+ setTimeout(() => {
+ expect(vm.store.removeGroup).not.toHaveBeenCalled();
+ expect(window.Flash).toHaveBeenCalledWith(message);
+ expect(childGroupItem.isBeingRemoved).toBeFalsy();
+ done();
+ }, 0);
+ });
+ });
+
+ describe('updatePagination', () => {
+ it('should set pagination info to store from provided headers', () => {
+ spyOn(vm.store, 'setPaginationInfo');
+
+ vm.updatePagination(mockRawPageInfo);
+ expect(vm.store.setPaginationInfo).toHaveBeenCalledWith(mockRawPageInfo);
+ });
+ });
+
+ describe('updateGroups', () => {
+ it('should call setGroups on store if method was called directly', () => {
+ spyOn(vm.store, 'setGroups');
+
+ vm.updateGroups(mockGroups);
+ expect(vm.store.setGroups).toHaveBeenCalledWith(mockGroups);
+ });
+
+ it('should call setSearchedGroups on store if method was called with fromSearch param', () => {
+ spyOn(vm.store, 'setSearchedGroups');
+
+ vm.updateGroups(mockGroups, true);
+ expect(vm.store.setSearchedGroups).toHaveBeenCalledWith(mockGroups);
+ });
+
+ it('should set `isSearchEmpty` prop based on groups count', () => {
+ vm.updateGroups(mockGroups);
+ expect(vm.isSearchEmpty).toBeFalsy();
+
+ vm.updateGroups([]);
+ expect(vm.isSearchEmpty).toBeTruthy();
+ });
+ });
+ });
+
+ describe('created', () => {
+ it('should bind event listeners on eventHub', (done) => {
+ spyOn(eventHub, '$on');
+
+ const newVm = createComponent();
+ newVm.$mount();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', jasmine.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', jasmine.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', jasmine.any(Function));
+ newVm.$destroy();
+ done();
+ });
+ });
+
+ it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', (done) => {
+ const newVm = createComponent();
+ newVm.$mount();
+ Vue.nextTick(() => {
+ expect(newVm.searchEmptyMessage).toBe('Sorry, no groups or projects matched your search');
+ newVm.$destroy();
+ done();
+ });
+ });
+
+ it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', (done) => {
+ const newVm = createComponent(true);
+ newVm.$mount();
+ Vue.nextTick(() => {
+ expect(newVm.searchEmptyMessage).toBe('Sorry, no groups matched your search');
+ newVm.$destroy();
+ done();
+ });
+ });
+ });
+
+ describe('beforeDestroy', () => {
+ it('should unbind event listeners on eventHub', (done) => {
+ spyOn(eventHub, '$off');
+
+ const newVm = createComponent();
+ newVm.$mount();
+ newVm.$destroy();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', jasmine.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', jasmine.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', jasmine.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render loading icon', (done) => {
+ vm.isLoading = true;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.loading-animation')).toBeDefined();
+ expect(vm.$el.querySelector('i.fa').getAttribute('aria-label')).toBe('Loading groups');
+ done();
+ });
+ });
+
+ it('should render groups tree', (done) => {
+ vm.store.state.groups = [mockParentGroupItem];
+ vm.isLoading = false;
+ vm.store.state.pageInfo = mockPageInfo;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/groups/components/group_folder_spec.js b/spec/javascripts/groups/components/group_folder_spec.js
new file mode 100644
index 00000000000..4eb198595fb
--- /dev/null
+++ b/spec/javascripts/groups/components/group_folder_spec.js
@@ -0,0 +1,66 @@
+import Vue from 'vue';
+
+import groupFolderComponent from '~/groups/components/group_folder.vue';
+import groupItemComponent from '~/groups/components/group_item.vue';
+import { mockGroups, mockParentGroupItem } from '../mock_data';
+
+const createComponent = (groups = mockGroups, parentGroup = mockParentGroupItem) => {
+ const Component = Vue.extend(groupFolderComponent);
+
+ return new Component({
+ propsData: {
+ groups,
+ parentGroup,
+ },
+ });
+};
+
+describe('GroupFolderComponent', () => {
+ let vm;
+
+ beforeEach((done) => {
+ Vue.component('group-item', groupItemComponent);
+
+ vm = createComponent();
+ vm.$mount();
+
+ Vue.nextTick(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('hasMoreChildren', () => {
+ it('should return false when childrenCount of group is less than MAX_CHILDREN_COUNT', () => {
+ expect(vm.hasMoreChildren).toBeFalsy();
+ });
+ });
+
+ describe('moreChildrenStats', () => {
+ it('should return message with count of excess children over MAX_CHILDREN_COUNT limit', () => {
+ expect(vm.moreChildrenStats).toBe('3 more items');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component template correctly', () => {
+ expect(vm.$el.classList.contains('group-list-tree')).toBeTruthy();
+ expect(vm.$el.querySelectorAll('li.group-row').length).toBe(7);
+ });
+
+ it('should render more children link when groups list has children over MAX_CHILDREN_COUNT limit', () => {
+ const parentGroup = Object.assign({}, mockParentGroupItem);
+ parentGroup.childrenCount = 21;
+
+ const newVm = createComponent(mockGroups, parentGroup);
+ newVm.$mount();
+ expect(newVm.$el.querySelector('li.group-row a.has-more-items')).toBeDefined();
+ newVm.$destroy();
+ });
+ });
+});
diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js
new file mode 100644
index 00000000000..0f4fbdae445
--- /dev/null
+++ b/spec/javascripts/groups/components/group_item_spec.js
@@ -0,0 +1,177 @@
+import Vue from 'vue';
+
+import groupItemComponent from '~/groups/components/group_item.vue';
+import groupFolderComponent from '~/groups/components/group_folder.vue';
+import eventHub from '~/groups/event_hub';
+import { mockParentGroupItem, mockChildren } from '../mock_data';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => {
+ const Component = Vue.extend(groupItemComponent);
+
+ return mountComponent(Component, {
+ group,
+ parentGroup,
+ });
+};
+
+describe('GroupItemComponent', () => {
+ let vm;
+
+ beforeEach((done) => {
+ Vue.component('group-folder', groupFolderComponent);
+
+ vm = createComponent();
+
+ Vue.nextTick(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('groupDomId', () => {
+ it('should return ID string suffixed with group ID', () => {
+ expect(vm.groupDomId).toBe('group-55');
+ });
+ });
+
+ describe('rowClass', () => {
+ it('should return map of classes based on group details', () => {
+ const classes = ['is-open', 'has-children', 'has-description', 'being-removed'];
+ const rowClass = vm.rowClass;
+
+ expect(Object.keys(rowClass).length).toBe(classes.length);
+ Object.keys(rowClass).forEach((className) => {
+ expect(classes.indexOf(className) > -1).toBeTruthy();
+ });
+ });
+ });
+
+ describe('hasChildren', () => {
+ it('should return boolean value representing if group has any children present', () => {
+ let newVm;
+ const group = Object.assign({}, mockParentGroupItem);
+
+ group.childrenCount = 5;
+ newVm = createComponent(group);
+ expect(newVm.hasChildren).toBeTruthy();
+ newVm.$destroy();
+
+ group.childrenCount = 0;
+ newVm = createComponent(group);
+ expect(newVm.hasChildren).toBeFalsy();
+ newVm.$destroy();
+ });
+ });
+
+ describe('hasAvatar', () => {
+ it('should return boolean value representing if group has any avatar present', () => {
+ let newVm;
+ const group = Object.assign({}, mockParentGroupItem);
+
+ group.avatarUrl = null;
+ newVm = createComponent(group);
+ expect(newVm.hasAvatar).toBeFalsy();
+ newVm.$destroy();
+
+ group.avatarUrl = '/uploads/group_avatar.png';
+ newVm = createComponent(group);
+ expect(newVm.hasAvatar).toBeTruthy();
+ newVm.$destroy();
+ });
+ });
+
+ describe('isGroup', () => {
+ it('should return boolean value representing if group item is of type `group` or not', () => {
+ let newVm;
+ const group = Object.assign({}, mockParentGroupItem);
+
+ group.type = 'group';
+ newVm = createComponent(group);
+ expect(newVm.isGroup).toBeTruthy();
+ newVm.$destroy();
+
+ group.type = 'project';
+ newVm = createComponent(group);
+ expect(newVm.isGroup).toBeFalsy();
+ newVm.$destroy();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('onClickRowGroup', () => {
+ let event;
+
+ beforeEach(() => {
+ const classList = {
+ contains() {
+ return false;
+ },
+ };
+
+ event = {
+ target: {
+ classList,
+ parentElement: {
+ classList,
+ },
+ },
+ };
+ });
+
+ it('should emit `toggleChildren` event when expand is clicked on a group and it has children present', () => {
+ spyOn(eventHub, '$emit');
+
+ vm.onClickRowGroup(event);
+ expect(eventHub.$emit).toHaveBeenCalledWith('toggleChildren', vm.group);
+ });
+
+ it('should navigate page to group homepage if group does not have any children present', (done) => {
+ const group = Object.assign({}, mockParentGroupItem);
+ group.childrenCount = 0;
+ const newVm = createComponent(group);
+ spyOn(gl.utils, 'visitUrl').and.stub();
+ spyOn(eventHub, '$emit');
+
+ newVm.onClickRowGroup(event);
+ setTimeout(() => {
+ expect(eventHub.$emit).not.toHaveBeenCalled();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
+ done();
+ }, 0);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component template correctly', () => {
+ expect(vm.$el.getAttribute('id')).toBe('group-55');
+ expect(vm.$el.classList.contains('group-row')).toBeTruthy();
+
+ expect(vm.$el.querySelector('.group-row-contents')).toBeDefined();
+ expect(vm.$el.querySelector('.group-row-contents .controls')).toBeDefined();
+ expect(vm.$el.querySelector('.group-row-contents .stats')).toBeDefined();
+
+ expect(vm.$el.querySelector('.folder-toggle-wrap')).toBeDefined();
+ expect(vm.$el.querySelector('.folder-toggle-wrap .folder-caret')).toBeDefined();
+ expect(vm.$el.querySelector('.folder-toggle-wrap .item-type-icon')).toBeDefined();
+
+ expect(vm.$el.querySelector('.avatar-container')).toBeDefined();
+ expect(vm.$el.querySelector('.avatar-container a.no-expand')).toBeDefined();
+ expect(vm.$el.querySelector('.avatar-container .avatar')).toBeDefined();
+
+ expect(vm.$el.querySelector('.title')).toBeDefined();
+ expect(vm.$el.querySelector('.title a.no-expand')).toBeDefined();
+ expect(vm.$el.querySelector('.access-type')).toBeDefined();
+ expect(vm.$el.querySelector('.description')).toBeDefined();
+
+ expect(vm.$el.querySelector('.group-list-tree')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/groups/components/groups_spec.js b/spec/javascripts/groups/components/groups_spec.js
new file mode 100644
index 00000000000..90e818c1545
--- /dev/null
+++ b/spec/javascripts/groups/components/groups_spec.js
@@ -0,0 +1,70 @@
+import Vue from 'vue';
+
+import groupsComponent from '~/groups/components/groups.vue';
+import groupFolderComponent from '~/groups/components/group_folder.vue';
+import groupItemComponent from '~/groups/components/group_item.vue';
+import eventHub from '~/groups/event_hub';
+import { mockGroups, mockPageInfo } from '../mock_data';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const createComponent = (searchEmpty = false) => {
+ const Component = Vue.extend(groupsComponent);
+
+ return mountComponent(Component, {
+ groups: mockGroups,
+ pageInfo: mockPageInfo,
+ searchEmptyMessage: 'No matching results',
+ searchEmpty,
+ });
+};
+
+describe('GroupsComponent', () => {
+ let vm;
+
+ beforeEach((done) => {
+ Vue.component('group-folder', groupFolderComponent);
+ Vue.component('group-item', groupItemComponent);
+
+ vm = createComponent();
+
+ Vue.nextTick(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('methods', () => {
+ describe('change', () => {
+ it('should emit `fetchPage` event when page is changed via pagination', () => {
+ spyOn(eventHub, '$emit').and.stub();
+
+ vm.change(2);
+ expect(eventHub.$emit).toHaveBeenCalledWith('fetchPage', 2, jasmine.any(Object), jasmine.any(Object), jasmine.any(Object));
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component template correctly', (done) => {
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
+ expect(vm.$el.querySelector('.group-list-tree')).toBeDefined();
+ expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
+ expect(vm.$el.querySelectorAll('.has-no-search-results').length === 0).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should render empty search message when `searchEmpty` is `true`', (done) => {
+ vm.searchEmpty = true;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.has-no-search-results')).toBeDefined();
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js
new file mode 100644
index 00000000000..2ce1a749a96
--- /dev/null
+++ b/spec/javascripts/groups/components/item_actions_spec.js
@@ -0,0 +1,110 @@
+import Vue from 'vue';
+
+import itemActionsComponent from '~/groups/components/item_actions.vue';
+import eventHub from '~/groups/event_hub';
+import { mockParentGroupItem, mockChildren } from '../mock_data';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => {
+ const Component = Vue.extend(itemActionsComponent);
+
+ return mountComponent(Component, {
+ group,
+ parentGroup,
+ });
+};
+
+describe('ItemActionsComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('leaveConfirmationMessage', () => {
+ it('should return appropriate string for leave group confirmation', () => {
+ expect(vm.leaveConfirmationMessage).toBe('Are you sure you want to leave the "platform / hardware" group?');
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('onLeaveGroup', () => {
+ it('should change `dialogStatus` prop to `true` which shows confirmation dialog', () => {
+ expect(vm.dialogStatus).toBeFalsy();
+ vm.onLeaveGroup();
+ expect(vm.dialogStatus).toBeTruthy();
+ });
+ });
+
+ describe('leaveGroup', () => {
+ it('should change `dialogStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => {
+ spyOn(eventHub, '$emit');
+ vm.dialogStatus = true;
+ vm.leaveGroup(true);
+ expect(vm.dialogStatus).toBeFalsy();
+ expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup);
+ });
+
+ it('should change `dialogStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => {
+ spyOn(eventHub, '$emit');
+ vm.dialogStatus = true;
+ vm.leaveGroup(false);
+ expect(vm.dialogStatus).toBeFalsy();
+ expect(eventHub.$emit).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component template correctly', () => {
+ expect(vm.$el.classList.contains('controls')).toBeTruthy();
+ });
+
+ it('should render Edit Group button with correct attribute values', () => {
+ const group = Object.assign({}, mockParentGroupItem);
+ group.canEdit = true;
+ const newVm = createComponent(group);
+
+ const editBtn = newVm.$el.querySelector('a.edit-group');
+ expect(editBtn).toBeDefined();
+ expect(editBtn.classList.contains('no-expand')).toBeTruthy();
+ expect(editBtn.getAttribute('href')).toBe(group.editPath);
+ expect(editBtn.getAttribute('aria-label')).toBe('Edit group');
+ expect(editBtn.dataset.originalTitle).toBe('Edit group');
+ expect(editBtn.querySelector('i.fa.fa-cogs')).toBeDefined();
+
+ newVm.$destroy();
+ });
+
+ it('should render Leave Group button with correct attribute values', () => {
+ const group = Object.assign({}, mockParentGroupItem);
+ group.canLeave = true;
+ const newVm = createComponent(group);
+
+ const leaveBtn = newVm.$el.querySelector('a.leave-group');
+ expect(leaveBtn).toBeDefined();
+ expect(leaveBtn.classList.contains('no-expand')).toBeTruthy();
+ expect(leaveBtn.getAttribute('href')).toBe(group.leavePath);
+ expect(leaveBtn.getAttribute('aria-label')).toBe('Leave this group');
+ expect(leaveBtn.dataset.originalTitle).toBe('Leave this group');
+ expect(leaveBtn.querySelector('i.fa.fa-sign-out')).toBeDefined();
+
+ newVm.$destroy();
+ });
+
+ it('should show modal dialog when `dialogStatus` is set to `true`', () => {
+ vm.dialogStatus = true;
+ const modalDialogEl = vm.$el.querySelector('.modal.popup-dialog');
+ expect(modalDialogEl).toBeDefined();
+ expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?');
+ expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave');
+ });
+ });
+});
diff --git a/spec/javascripts/groups/components/item_caret_spec.js b/spec/javascripts/groups/components/item_caret_spec.js
new file mode 100644
index 00000000000..4310a07e6e6
--- /dev/null
+++ b/spec/javascripts/groups/components/item_caret_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+
+import itemCaretComponent from '~/groups/components/item_caret.vue';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const createComponent = (isGroupOpen = false) => {
+ const Component = Vue.extend(itemCaretComponent);
+
+ return mountComponent(Component, {
+ isGroupOpen,
+ });
+};
+
+describe('ItemCaretComponent', () => {
+ describe('template', () => {
+ it('should render component template correctly', () => {
+ const vm = createComponent();
+ vm.$mount();
+ expect(vm.$el.classList.contains('folder-caret')).toBeTruthy();
+ vm.$destroy();
+ });
+
+ it('should render caret down icon if `isGroupOpen` prop is `true`', () => {
+ const vm = createComponent(true);
+ vm.$mount();
+ expect(vm.$el.querySelectorAll('i.fa.fa-caret-down').length).toBe(1);
+ expect(vm.$el.querySelectorAll('i.fa.fa-caret-right').length).toBe(0);
+ vm.$destroy();
+ });
+
+ it('should render caret right icon if `isGroupOpen` prop is `false`', () => {
+ const vm = createComponent();
+ vm.$mount();
+ expect(vm.$el.querySelectorAll('i.fa.fa-caret-down').length).toBe(0);
+ expect(vm.$el.querySelectorAll('i.fa.fa-caret-right').length).toBe(1);
+ vm.$destroy();
+ });
+ });
+});
diff --git a/spec/javascripts/groups/components/item_stats_spec.js b/spec/javascripts/groups/components/item_stats_spec.js
new file mode 100644
index 00000000000..e200f9f08bd
--- /dev/null
+++ b/spec/javascripts/groups/components/item_stats_spec.js
@@ -0,0 +1,159 @@
+import Vue from 'vue';
+
+import itemStatsComponent from '~/groups/components/item_stats.vue';
+import {
+ mockParentGroupItem,
+ ITEM_TYPE,
+ VISIBILITY_TYPE_ICON,
+ GROUP_VISIBILITY_TYPE,
+ PROJECT_VISIBILITY_TYPE,
+} from '../mock_data';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const createComponent = (item = mockParentGroupItem) => {
+ const Component = Vue.extend(itemStatsComponent);
+
+ return mountComponent(Component, {
+ item,
+ });
+};
+
+describe('ItemStatsComponent', () => {
+ describe('computed', () => {
+ describe('visibilityIcon', () => {
+ it('should return icon class based on `item.visibility` value', () => {
+ Object.keys(VISIBILITY_TYPE_ICON).forEach((visibility) => {
+ const item = Object.assign({}, mockParentGroupItem, { visibility });
+ const vm = createComponent(item);
+ vm.$mount();
+ expect(vm.visibilityIcon).toBe(VISIBILITY_TYPE_ICON[visibility]);
+ vm.$destroy();
+ });
+ });
+ });
+
+ describe('visibilityTooltip', () => {
+ it('should return tooltip string for Group based on `item.visibility` value', () => {
+ Object.keys(GROUP_VISIBILITY_TYPE).forEach((visibility) => {
+ const item = Object.assign({}, mockParentGroupItem, {
+ visibility,
+ type: ITEM_TYPE.GROUP,
+ });
+ const vm = createComponent(item);
+ vm.$mount();
+ expect(vm.visibilityTooltip).toBe(GROUP_VISIBILITY_TYPE[visibility]);
+ vm.$destroy();
+ });
+ });
+
+ it('should return tooltip string for Project based on `item.visibility` value', () => {
+ Object.keys(PROJECT_VISIBILITY_TYPE).forEach((visibility) => {
+ const item = Object.assign({}, mockParentGroupItem, {
+ visibility,
+ type: ITEM_TYPE.PROJECT,
+ });
+ const vm = createComponent(item);
+ vm.$mount();
+ expect(vm.visibilityTooltip).toBe(PROJECT_VISIBILITY_TYPE[visibility]);
+ vm.$destroy();
+ });
+ });
+ });
+
+ describe('isProject', () => {
+ it('should return boolean value representing whether `item.type` is Project or not', () => {
+ let item;
+ let vm;
+
+ item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT });
+ vm = createComponent(item);
+ vm.$mount();
+ expect(vm.isProject).toBeTruthy();
+ vm.$destroy();
+
+ item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP });
+ vm = createComponent(item);
+ vm.$mount();
+ expect(vm.isProject).toBeFalsy();
+ vm.$destroy();
+ });
+ });
+
+ describe('isGroup', () => {
+ it('should return boolean value representing whether `item.type` is Group or not', () => {
+ let item;
+ let vm;
+
+ item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP });
+ vm = createComponent(item);
+ vm.$mount();
+ expect(vm.isGroup).toBeTruthy();
+ vm.$destroy();
+
+ item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT });
+ vm = createComponent(item);
+ vm.$mount();
+ expect(vm.isGroup).toBeFalsy();
+ vm.$destroy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component template correctly', () => {
+ const vm = createComponent();
+ vm.$mount();
+
+ const visibilityIconEl = vm.$el.querySelector('.item-visibility');
+ expect(vm.$el.classList.contains('.stats')).toBeDefined();
+ expect(visibilityIconEl).toBeDefined();
+ expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip);
+ expect(visibilityIconEl.querySelector('i.fa')).toBeDefined();
+
+ vm.$destroy();
+ });
+
+ it('should render stat icons if `item.type` is Group', () => {
+ const item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP });
+ const vm = createComponent(item);
+ vm.$mount();
+
+ const subgroupIconEl = vm.$el.querySelector('span.number-subgroups');
+ expect(subgroupIconEl).toBeDefined();
+ expect(subgroupIconEl.dataset.originalTitle).toBe('Subgroups');
+ expect(subgroupIconEl.querySelector('i.fa.fa-folder')).toBeDefined();
+ expect(subgroupIconEl.innerText.trim()).toBe(`${vm.item.subgroupCount}`);
+
+ const projectsIconEl = vm.$el.querySelector('span.number-projects');
+ expect(projectsIconEl).toBeDefined();
+ expect(projectsIconEl.dataset.originalTitle).toBe('Projects');
+ expect(projectsIconEl.querySelector('i.fa.fa-bookmark')).toBeDefined();
+ expect(projectsIconEl.innerText.trim()).toBe(`${vm.item.projectCount}`);
+
+ const membersIconEl = vm.$el.querySelector('span.number-users');
+ expect(membersIconEl).toBeDefined();
+ expect(membersIconEl.dataset.originalTitle).toBe('Members');
+ expect(membersIconEl.querySelector('i.fa.fa-users')).toBeDefined();
+ expect(membersIconEl.innerText.trim()).toBe(`${vm.item.memberCount}`);
+
+ vm.$destroy();
+ });
+
+ it('should render stat icons if `item.type` is Project', () => {
+ const item = Object.assign({}, mockParentGroupItem, {
+ type: ITEM_TYPE.PROJECT,
+ starCount: 4,
+ });
+ const vm = createComponent(item);
+ vm.$mount();
+
+ const projectStarIconEl = vm.$el.querySelector('.project-stars');
+ expect(projectStarIconEl).toBeDefined();
+ expect(projectStarIconEl.querySelector('i.fa.fa-star')).toBeDefined();
+ expect(projectStarIconEl.innerText.trim()).toBe(`${vm.item.starCount}`);
+
+ vm.$destroy();
+ });
+ });
+});
diff --git a/spec/javascripts/groups/components/item_type_icon_spec.js b/spec/javascripts/groups/components/item_type_icon_spec.js
new file mode 100644
index 00000000000..528e6ed1b4c
--- /dev/null
+++ b/spec/javascripts/groups/components/item_type_icon_spec.js
@@ -0,0 +1,54 @@
+import Vue from 'vue';
+
+import itemTypeIconComponent from '~/groups/components/item_type_icon.vue';
+import { ITEM_TYPE } from '../mock_data';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const createComponent = (itemType = ITEM_TYPE.GROUP, isGroupOpen = false) => {
+ const Component = Vue.extend(itemTypeIconComponent);
+
+ return mountComponent(Component, {
+ itemType,
+ isGroupOpen,
+ });
+};
+
+describe('ItemTypeIconComponent', () => {
+ describe('template', () => {
+ it('should render component template correctly', () => {
+ const vm = createComponent();
+ vm.$mount();
+ expect(vm.$el.classList.contains('item-type-icon')).toBeTruthy();
+ vm.$destroy();
+ });
+
+ it('should render folder open or close icon based `isGroupOpen` prop value', () => {
+ let vm;
+
+ vm = createComponent(ITEM_TYPE.GROUP, true);
+ vm.$mount();
+ expect(vm.$el.querySelector('i.fa.fa-folder-open')).toBeDefined();
+ vm.$destroy();
+
+ vm = createComponent(ITEM_TYPE.GROUP);
+ vm.$mount();
+ expect(vm.$el.querySelector('i.fa.fa-folder')).toBeDefined();
+ vm.$destroy();
+ });
+
+ it('should render bookmark icon based on `isProject` prop value', () => {
+ let vm;
+
+ vm = createComponent(ITEM_TYPE.PROJECT);
+ vm.$mount();
+ expect(vm.$el.querySelectorAll('i.fa.fa-bookmark').length).toBe(1);
+ vm.$destroy();
+
+ vm = createComponent(ITEM_TYPE.GROUP);
+ vm.$mount();
+ expect(vm.$el.querySelectorAll('i.fa.fa-bookmark').length).toBe(0);
+ vm.$destroy();
+ });
+ });
+});
diff --git a/spec/javascripts/groups/group_item_spec.js b/spec/javascripts/groups/group_item_spec.js
deleted file mode 100644
index 25e10552d95..00000000000
--- a/spec/javascripts/groups/group_item_spec.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import Vue from 'vue';
-import groupItemComponent from '~/groups/components/group_item.vue';
-import GroupsStore from '~/groups/stores/groups_store';
-import { group1 } from './mock_data';
-
-describe('Groups Component', () => {
- let GroupItemComponent;
- let component;
- let store;
- let group;
-
- describe('group with default data', () => {
- beforeEach((done) => {
- GroupItemComponent = Vue.extend(groupItemComponent);
- store = new GroupsStore();
- group = store.decorateGroup(group1);
-
- component = new GroupItemComponent({
- propsData: {
- group,
- },
- }).$mount();
-
- Vue.nextTick(() => {
- done();
- });
- });
-
- afterEach(() => {
- component.$destroy();
- });
-
- it('should render the group item correctly', () => {
- expect(component.$el.classList.contains('group-row')).toBe(true);
- expect(component.$el.classList.contains('.no-description')).toBe(false);
- expect(component.$el.querySelector('.number-projects').textContent).toContain(group.numberProjects);
- expect(component.$el.querySelector('.number-users').textContent).toContain(group.numberUsers);
- expect(component.$el.querySelector('.group-visibility')).toBeDefined();
- expect(component.$el.querySelector('.avatar-container')).toBeDefined();
- expect(component.$el.querySelector('.title').textContent).toContain(group.name);
- expect(component.$el.querySelector('.access-type').textContent).toContain(group.permissions.humanGroupAccess);
- expect(component.$el.querySelector('.description').textContent).toContain(group.description);
- expect(component.$el.querySelector('.edit-group')).toBeDefined();
- expect(component.$el.querySelector('.leave-group')).toBeDefined();
- });
- });
-
- describe('group without description', () => {
- beforeEach((done) => {
- GroupItemComponent = Vue.extend(groupItemComponent);
- store = new GroupsStore();
- group1.description = '';
- group = store.decorateGroup(group1);
-
- component = new GroupItemComponent({
- propsData: {
- group,
- },
- }).$mount();
-
- Vue.nextTick(() => {
- done();
- });
- });
-
- afterEach(() => {
- component.$destroy();
- });
-
- it('should render group item correctly', () => {
- expect(component.$el.querySelector('.description').textContent).toBe('');
- expect(component.$el.classList.contains('.no-description')).toBe(false);
- });
- });
-
- describe('user has not access to group', () => {
- beforeEach((done) => {
- GroupItemComponent = Vue.extend(groupItemComponent);
- store = new GroupsStore();
- group1.permissions.human_group_access = null;
- group = store.decorateGroup(group1);
-
- component = new GroupItemComponent({
- propsData: {
- group,
- },
- }).$mount();
-
- Vue.nextTick(() => {
- done();
- });
- });
-
- afterEach(() => {
- component.$destroy();
- });
-
- it('should not display access type', () => {
- expect(component.$el.querySelector('.access-type')).toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/groups/groups_spec.js b/spec/javascripts/groups/groups_spec.js
deleted file mode 100644
index b14153dbbfa..00000000000
--- a/spec/javascripts/groups/groups_spec.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import Vue from 'vue';
-import eventHub from '~/groups/event_hub';
-import groupFolderComponent from '~/groups/components/group_folder.vue';
-import groupItemComponent from '~/groups/components/group_item.vue';
-import groupsComponent from '~/groups/components/groups.vue';
-import GroupsStore from '~/groups/stores/groups_store';
-import { groupsData } from './mock_data';
-
-describe('Groups Component', () => {
- let GroupsComponent;
- let store;
- let component;
- let groups;
-
- beforeEach((done) => {
- Vue.component('group-folder', groupFolderComponent);
- Vue.component('group-item', groupItemComponent);
-
- store = new GroupsStore();
- groups = store.setGroups(groupsData.groups);
-
- store.storePagination(groupsData.pagination);
-
- GroupsComponent = Vue.extend(groupsComponent);
-
- component = new GroupsComponent({
- propsData: {
- groups: store.state.groups,
- pageInfo: store.state.pageInfo,
- },
- }).$mount();
-
- Vue.nextTick(() => {
- done();
- });
- });
-
- afterEach(() => {
- component.$destroy();
- });
-
- describe('with data', () => {
- it('should render a list of groups', () => {
- expect(component.$el.classList.contains('groups-list-tree-container')).toBe(true);
- expect(component.$el.querySelector('#group-12')).toBeDefined();
- expect(component.$el.querySelector('#group-1119')).toBeDefined();
- expect(component.$el.querySelector('#group-1120')).toBeDefined();
- });
-
- it('should respect the order of groups', () => {
- const wrap = component.$el.querySelector('.groups-list-tree-container > .group-list-tree');
- expect(wrap.querySelector('.group-row:nth-child(1)').id).toBe('group-12');
- expect(wrap.querySelector('.group-row:nth-child(2)').id).toBe('group-1119');
- });
-
- it('should render group and its subgroup', () => {
- const lists = component.$el.querySelectorAll('.group-list-tree');
-
- expect(lists.length).toBe(3); // one parent and two subgroups
-
- expect(lists[0].querySelector('#group-1119').classList.contains('is-open')).toBe(true);
- expect(lists[0].querySelector('#group-1119').classList.contains('has-subgroups')).toBe(true);
-
- expect(lists[2].querySelector('#group-1120').textContent).toContain(groups.id1119.subGroups.id1120.name);
- });
-
- it('should render group identicon when group avatar is not present', () => {
- const avatar = component.$el.querySelector('#group-12 .avatar-container .avatar');
- expect(avatar.nodeName).toBe('DIV');
- expect(avatar.classList.contains('identicon')).toBeTruthy();
- expect(avatar.getAttribute('style').indexOf('background-color') > -1).toBeTruthy();
- });
-
- it('should render group avatar when group avatar is present', () => {
- const avatar = component.$el.querySelector('#group-1120 .avatar-container .avatar');
- expect(avatar.nodeName).toBe('IMG');
- expect(avatar.classList.contains('identicon')).toBeFalsy();
- });
-
- it('should remove prefix of parent group', () => {
- expect(component.$el.querySelector('#group-12 #group-1128 .title').textContent).toContain('level2 / level3 / level4');
- });
-
- it('should remove the group after leaving the group', (done) => {
- spyOn(window, 'confirm').and.returnValue(true);
-
- eventHub.$on('leaveGroup', (group, collection) => {
- store.removeGroup(group, collection);
- });
-
- component.$el.querySelector('#group-12 .leave-group').click();
-
- Vue.nextTick(() => {
- expect(component.$el.querySelector('#group-12')).toBeNull();
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/groups/mock_data.js b/spec/javascripts/groups/mock_data.js
index 5bb84b591f4..6184d671790 100644
--- a/spec/javascripts/groups/mock_data.js
+++ b/spec/javascripts/groups/mock_data.js
@@ -1,114 +1,380 @@
-const group1 = {
- id: 12,
- name: 'level1',
- path: 'level1',
- description: 'foo',
- visibility: 'public',
- avatar_url: null,
- web_url: 'http://localhost:3000/groups/level1',
- group_path: '/level1',
- full_name: 'level1',
- full_path: 'level1',
- parent_id: null,
- created_at: '2017-05-15T19:01:23.670Z',
- updated_at: '2017-05-15T19:01:23.670Z',
- number_projects_with_delimiter: '1',
- number_users_with_delimiter: '1',
- has_subgroups: true,
- permissions: {
- human_group_access: 'Master',
- },
+export const mockEndpoint = '/dashboard/groups.json';
+
+export const ITEM_TYPE = {
+ PROJECT: 'project',
+ GROUP: 'group',
};
-// This group has no direct parent, should be placed as subgroup of group1
-const group14 = {
- id: 1128,
- name: 'level4',
- path: 'level4',
- description: 'foo',
- visibility: 'public',
- avatar_url: null,
- web_url: 'http://localhost:3000/groups/level1/level2/level3/level4',
- group_path: '/level1/level2/level3/level4',
- full_name: 'level1 / level2 / level3 / level4',
- full_path: 'level1/level2/level3/level4',
- parent_id: 1127,
- created_at: '2017-05-15T19:02:01.645Z',
- updated_at: '2017-05-15T19:02:01.645Z',
- number_projects_with_delimiter: '1',
- number_users_with_delimiter: '1',
- has_subgroups: true,
- permissions: {
- human_group_access: 'Master',
- },
+export const GROUP_VISIBILITY_TYPE = {
+ public: 'Public - The group and any public projects can be viewed without any authentication.',
+ internal: 'Internal - The group and any internal projects can be viewed by any logged in user.',
+ private: 'Private - The group and its projects can only be viewed by members.',
};
-const group2 = {
- id: 1119,
- name: 'devops',
- path: 'devops',
- description: 'foo',
- visibility: 'public',
- avatar_url: null,
- web_url: 'http://localhost:3000/groups/devops',
- group_path: '/devops',
- full_name: 'devops',
- full_path: 'devops',
- parent_id: null,
- created_at: '2017-05-11T19:35:09.635Z',
- updated_at: '2017-05-11T19:35:09.635Z',
- number_projects_with_delimiter: '1',
- number_users_with_delimiter: '1',
- has_subgroups: true,
- permissions: {
- human_group_access: 'Master',
- },
+export const PROJECT_VISIBILITY_TYPE = {
+ public: 'Public - The project can be accessed without any authentication.',
+ internal: 'Internal - The project can be accessed by any logged in user.',
+ private: 'Private - Project access must be granted explicitly to each user.',
+};
+
+export const VISIBILITY_TYPE_ICON = {
+ public: 'fa-globe',
+ internal: 'fa-shield',
+ private: 'fa-lock',
};
-const group21 = {
- id: 1120,
- name: 'chef',
- path: 'chef',
- description: 'foo',
+export const mockParentGroupItem = {
+ id: 55,
+ name: 'hardware',
+ description: '',
visibility: 'public',
- avatar_url: '/uploads/-/system/group/avatar/2/GitLab.png',
- web_url: 'http://localhost:3000/groups/devops/chef',
- group_path: '/devops/chef',
- full_name: 'devops / chef',
- full_path: 'devops/chef',
- parent_id: 1119,
- created_at: '2017-05-11T19:51:04.060Z',
- updated_at: '2017-05-11T19:51:04.060Z',
- number_projects_with_delimiter: '1',
- number_users_with_delimiter: '1',
- has_subgroups: true,
- permissions: {
- human_group_access: 'Master',
- },
+ fullName: 'platform / hardware',
+ relativePath: '/platform/hardware',
+ canEdit: true,
+ type: 'group',
+ avatarUrl: null,
+ permission: 'Owner',
+ editPath: '/groups/platform/hardware/edit',
+ childrenCount: 3,
+ leavePath: '/groups/platform/hardware/group_members/leave',
+ parentId: 54,
+ memberCount: '1',
+ projectCount: 1,
+ subgroupCount: 2,
+ canLeave: false,
+ children: [],
+ isOpen: true,
+ isChildrenLoading: false,
+ isBeingRemoved: false,
};
-const groupsData = {
- groups: [group1, group14, group2, group21],
- pagination: {
- Date: 'Mon, 22 May 2017 22:31:52 GMT',
- 'X-Prev-Page': '1',
- 'X-Content-Type-Options': 'nosniff',
- 'X-Total': '31',
- 'Transfer-Encoding': 'chunked',
- 'X-Runtime': '0.611144',
- 'X-Xss-Protection': '1; mode=block',
- 'X-Request-Id': 'f5db8368-3ce5-4aa4-89d2-a125d9dead09',
- 'X-Ua-Compatible': 'IE=edge',
- 'X-Per-Page': '20',
- Link: '<http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="prev", <http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="first", <http://localhost:3000/dashboard/groups.json?page=2&per_page=20>; rel="last"',
- 'X-Next-Page': '',
- Etag: 'W/"a82f846947136271cdb7d55d19ef33d2"',
- 'X-Frame-Options': 'DENY',
- 'Content-Type': 'application/json; charset=utf-8',
- 'Cache-Control': 'max-age=0, private, must-revalidate',
- 'X-Total-Pages': '2',
- 'X-Page': '2',
+export const mockRawChildren = [
+ {
+ id: 57,
+ name: 'bsp',
+ description: '',
+ visibility: 'public',
+ full_name: 'platform / hardware / bsp',
+ relative_path: '/platform/hardware/bsp',
+ can_edit: true,
+ type: 'group',
+ avatar_url: null,
+ permission: 'Owner',
+ edit_path: '/groups/platform/hardware/bsp/edit',
+ children_count: 6,
+ leave_path: '/groups/platform/hardware/bsp/group_members/leave',
+ parent_id: 55,
+ number_users_with_delimiter: '1',
+ project_count: 4,
+ subgroup_count: 2,
+ can_leave: false,
+ children: [],
+ },
+];
+
+export const mockChildren = [
+ {
+ id: 57,
+ name: 'bsp',
+ description: '',
+ visibility: 'public',
+ fullName: 'platform / hardware / bsp',
+ relativePath: '/platform/hardware/bsp',
+ canEdit: true,
+ type: 'group',
+ avatarUrl: null,
+ permission: 'Owner',
+ editPath: '/groups/platform/hardware/bsp/edit',
+ childrenCount: 6,
+ leavePath: '/groups/platform/hardware/bsp/group_members/leave',
+ parentId: 55,
+ memberCount: '1',
+ projectCount: 4,
+ subgroupCount: 2,
+ canLeave: false,
+ children: [],
+ isOpen: true,
+ isChildrenLoading: false,
+ isBeingRemoved: false,
},
+];
+
+export const mockGroups = [
+ {
+ id: 75,
+ name: 'test-group',
+ description: '',
+ visibility: 'public',
+ full_name: 'test-group',
+ relative_path: '/test-group',
+ can_edit: true,
+ type: 'group',
+ avatar_url: null,
+ permission: 'Owner',
+ edit_path: '/groups/test-group/edit',
+ children_count: 2,
+ leave_path: '/groups/test-group/group_members/leave',
+ parent_id: null,
+ number_users_with_delimiter: '1',
+ project_count: 2,
+ subgroup_count: 0,
+ can_leave: false,
+ },
+ {
+ id: 67,
+ name: 'open-source',
+ description: '',
+ visibility: 'private',
+ full_name: 'open-source',
+ relative_path: '/open-source',
+ can_edit: true,
+ type: 'group',
+ avatar_url: null,
+ permission: 'Owner',
+ edit_path: '/groups/open-source/edit',
+ children_count: 0,
+ leave_path: '/groups/open-source/group_members/leave',
+ parent_id: null,
+ number_users_with_delimiter: '1',
+ project_count: 0,
+ subgroup_count: 0,
+ can_leave: false,
+ },
+ {
+ id: 54,
+ name: 'platform',
+ description: '',
+ visibility: 'public',
+ full_name: 'platform',
+ relative_path: '/platform',
+ can_edit: true,
+ type: 'group',
+ avatar_url: null,
+ permission: 'Owner',
+ edit_path: '/groups/platform/edit',
+ children_count: 1,
+ leave_path: '/groups/platform/group_members/leave',
+ parent_id: null,
+ number_users_with_delimiter: '1',
+ project_count: 0,
+ subgroup_count: 1,
+ can_leave: false,
+ },
+ {
+ id: 5,
+ name: 'H5bp',
+ description: 'Minus dolor consequuntur qui nam recusandae quam incidunt.',
+ visibility: 'public',
+ full_name: 'H5bp',
+ relative_path: '/h5bp',
+ can_edit: true,
+ type: 'group',
+ avatar_url: null,
+ permission: 'Owner',
+ edit_path: '/groups/h5bp/edit',
+ children_count: 1,
+ leave_path: '/groups/h5bp/group_members/leave',
+ parent_id: null,
+ number_users_with_delimiter: '5',
+ project_count: 1,
+ subgroup_count: 0,
+ can_leave: false,
+ },
+ {
+ id: 4,
+ name: 'Twitter',
+ description: 'Deserunt hic nostrum placeat veniam.',
+ visibility: 'public',
+ full_name: 'Twitter',
+ relative_path: '/twitter',
+ can_edit: true,
+ type: 'group',
+ avatar_url: null,
+ permission: 'Owner',
+ edit_path: '/groups/twitter/edit',
+ children_count: 2,
+ leave_path: '/groups/twitter/group_members/leave',
+ parent_id: null,
+ number_users_with_delimiter: '5',
+ project_count: 2,
+ subgroup_count: 0,
+ can_leave: false,
+ },
+ {
+ id: 3,
+ name: 'Documentcloud',
+ description: 'Consequatur saepe totam ea pariatur maxime.',
+ visibility: 'public',
+ full_name: 'Documentcloud',
+ relative_path: '/documentcloud',
+ can_edit: true,
+ type: 'group',
+ avatar_url: null,
+ permission: 'Owner',
+ edit_path: '/groups/documentcloud/edit',
+ children_count: 1,
+ leave_path: '/groups/documentcloud/group_members/leave',
+ parent_id: null,
+ number_users_with_delimiter: '5',
+ project_count: 1,
+ subgroup_count: 0,
+ can_leave: false,
+ },
+ {
+ id: 2,
+ name: 'Gitlab Org',
+ description: 'Debitis ea quas aperiam velit doloremque ab.',
+ visibility: 'public',
+ full_name: 'Gitlab Org',
+ relative_path: '/gitlab-org',
+ can_edit: true,
+ type: 'group',
+ avatar_url: '/uploads/-/system/group/avatar/2/GitLab.png',
+ permission: 'Owner',
+ edit_path: '/groups/gitlab-org/edit',
+ children_count: 4,
+ leave_path: '/groups/gitlab-org/group_members/leave',
+ parent_id: null,
+ number_users_with_delimiter: '5',
+ project_count: 4,
+ subgroup_count: 0,
+ can_leave: false,
+ },
+];
+
+export const mockSearchedGroups = [
+ {
+ id: 55,
+ name: 'hardware',
+ description: '',
+ visibility: 'public',
+ full_name: 'platform / hardware',
+ relative_path: '/platform/hardware',
+ can_edit: true,
+ type: 'group',
+ avatar_url: null,
+ permission: 'Owner',
+ edit_path: '/groups/platform/hardware/edit',
+ children_count: 3,
+ leave_path: '/groups/platform/hardware/group_members/leave',
+ parent_id: 54,
+ number_users_with_delimiter: '1',
+ project_count: 1,
+ subgroup_count: 2,
+ can_leave: false,
+ children: [
+ {
+ id: 57,
+ name: 'bsp',
+ description: '',
+ visibility: 'public',
+ full_name: 'platform / hardware / bsp',
+ relative_path: '/platform/hardware/bsp',
+ can_edit: true,
+ type: 'group',
+ avatar_url: null,
+ permission: 'Owner',
+ edit_path: '/groups/platform/hardware/bsp/edit',
+ children_count: 6,
+ leave_path: '/groups/platform/hardware/bsp/group_members/leave',
+ parent_id: 55,
+ number_users_with_delimiter: '1',
+ project_count: 4,
+ subgroup_count: 2,
+ can_leave: false,
+ children: [
+ {
+ id: 60,
+ name: 'kernel',
+ description: '',
+ visibility: 'public',
+ full_name: 'platform / hardware / bsp / kernel',
+ relative_path: '/platform/hardware/bsp/kernel',
+ can_edit: true,
+ type: 'group',
+ avatar_url: null,
+ permission: 'Owner',
+ edit_path: '/groups/platform/hardware/bsp/kernel/edit',
+ children_count: 1,
+ leave_path: '/groups/platform/hardware/bsp/kernel/group_members/leave',
+ parent_id: 57,
+ number_users_with_delimiter: '1',
+ project_count: 0,
+ subgroup_count: 1,
+ can_leave: false,
+ children: [
+ {
+ id: 61,
+ name: 'common',
+ description: '',
+ visibility: 'public',
+ full_name: 'platform / hardware / bsp / kernel / common',
+ relative_path: '/platform/hardware/bsp/kernel/common',
+ can_edit: true,
+ type: 'group',
+ avatar_url: null,
+ permission: 'Owner',
+ edit_path: '/groups/platform/hardware/bsp/kernel/common/edit',
+ children_count: 2,
+ leave_path: '/groups/platform/hardware/bsp/kernel/common/group_members/leave',
+ parent_id: 60,
+ number_users_with_delimiter: '1',
+ project_count: 2,
+ subgroup_count: 0,
+ can_leave: false,
+ children: [
+ {
+ id: 17,
+ name: 'v4.4',
+ description: 'Voluptatem qui ea error aperiam veritatis doloremque consequatur temporibus.',
+ visibility: 'public',
+ full_name: 'platform / hardware / bsp / kernel / common / v4.4',
+ relative_path: '/platform/hardware/bsp/kernel/common/v4.4',
+ can_edit: true,
+ type: 'project',
+ avatar_url: null,
+ permission: null,
+ edit_path: '/platform/hardware/bsp/kernel/common/v4.4/edit',
+ star_count: 0,
+ },
+ {
+ id: 16,
+ name: 'v4.1',
+ description: 'Rerum expedita voluptatem doloribus neque ducimus ut hic.',
+ visibility: 'public',
+ full_name: 'platform / hardware / bsp / kernel / common / v4.1',
+ relative_path: '/platform/hardware/bsp/kernel/common/v4.1',
+ can_edit: true,
+ type: 'project',
+ avatar_url: null,
+ permission: null,
+ edit_path: '/platform/hardware/bsp/kernel/common/v4.1/edit',
+ star_count: 0,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+];
+
+export const mockRawPageInfo = {
+ 'x-per-page': 10,
+ 'x-page': 10,
+ 'x-total': 10,
+ 'x-total-pages': 10,
+ 'x-next-page': 10,
+ 'x-prev-page': 10,
};
-export { groupsData, group1 };
+export const mockPageInfo = {
+ perPage: 10,
+ page: 10,
+ total: 10,
+ totalPages: 10,
+ nextPage: 10,
+ prevPage: 10,
+};
diff --git a/spec/javascripts/groups/service/groups_service_spec.js b/spec/javascripts/groups/service/groups_service_spec.js
new file mode 100644
index 00000000000..20bb63687f7
--- /dev/null
+++ b/spec/javascripts/groups/service/groups_service_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+import GroupsService from '~/groups/service/groups_service';
+import { mockEndpoint, mockParentGroupItem } from '../mock_data';
+
+Vue.use(VueResource);
+
+describe('GroupsService', () => {
+ let service;
+
+ beforeEach(() => {
+ service = new GroupsService(mockEndpoint);
+ });
+
+ describe('getGroups', () => {
+ it('should return promise for `GET` request on provided endpoint', () => {
+ spyOn(service.groups, 'get').and.stub();
+ const queryParams = {
+ page: 2,
+ filter: 'git',
+ sort: 'created_asc',
+ archived: true,
+ };
+
+ service.getGroups(55, 2, 'git', 'created_asc', true);
+ expect(service.groups.get).toHaveBeenCalledWith({ parent_id: 55 });
+
+ service.getGroups(null, 2, 'git', 'created_asc', true);
+ expect(service.groups.get).toHaveBeenCalledWith(queryParams);
+ });
+ });
+
+ describe('leaveGroup', () => {
+ it('should return promise for `DELETE` request on provided endpoint', () => {
+ spyOn(Vue.http, 'delete').and.stub();
+
+ service.leaveGroup(mockParentGroupItem.leavePath);
+ expect(Vue.http.delete).toHaveBeenCalledWith(mockParentGroupItem.leavePath);
+ });
+ });
+});
diff --git a/spec/javascripts/groups/store/groups_store_spec.js b/spec/javascripts/groups/store/groups_store_spec.js
new file mode 100644
index 00000000000..d74f38f476e
--- /dev/null
+++ b/spec/javascripts/groups/store/groups_store_spec.js
@@ -0,0 +1,110 @@
+import GroupsStore from '~/groups/store/groups_store';
+import {
+ mockGroups, mockSearchedGroups,
+ mockParentGroupItem, mockRawChildren,
+ mockRawPageInfo,
+} from '../mock_data';
+
+describe('ProjectsStore', () => {
+ describe('constructor', () => {
+ it('should initialize default state', () => {
+ let store;
+
+ store = new GroupsStore();
+ expect(Object.keys(store.state).length).toBe(2);
+ expect(Array.isArray(store.state.groups)).toBeTruthy();
+ expect(Object.keys(store.state.pageInfo).length).toBe(0);
+ expect(store.hideProjects).not.toBeDefined();
+
+ store = new GroupsStore(true);
+ expect(store.hideProjects).toBeTruthy();
+ });
+ });
+
+ describe('setGroups', () => {
+ it('should set groups to state', () => {
+ const store = new GroupsStore();
+ spyOn(store, 'formatGroupItem').and.callThrough();
+
+ store.setGroups(mockGroups);
+ expect(store.state.groups.length).toBe(mockGroups.length);
+ expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object));
+ expect(Object.keys(store.state.groups[0]).indexOf('fullName') > -1).toBeTruthy();
+ });
+ });
+
+ describe('setSearchedGroups', () => {
+ it('should set searched groups to state', () => {
+ const store = new GroupsStore();
+ spyOn(store, 'formatGroupItem').and.callThrough();
+
+ store.setSearchedGroups(mockSearchedGroups);
+ expect(store.state.groups.length).toBe(mockSearchedGroups.length);
+ expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object));
+ expect(Object.keys(store.state.groups[0]).indexOf('fullName') > -1).toBeTruthy();
+ expect(Object.keys(store.state.groups[0].children[0]).indexOf('fullName') > -1).toBeTruthy();
+ });
+ });
+
+ describe('setGroupChildren', () => {
+ it('should set children to group item in state', () => {
+ const store = new GroupsStore();
+ spyOn(store, 'formatGroupItem').and.callThrough();
+
+ store.setGroupChildren(mockParentGroupItem, mockRawChildren);
+ expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object));
+ expect(mockParentGroupItem.children.length).toBe(1);
+ expect(Object.keys(mockParentGroupItem.children[0]).indexOf('fullName') > -1).toBeTruthy();
+ expect(mockParentGroupItem.isOpen).toBeTruthy();
+ expect(mockParentGroupItem.isChildrenLoading).toBeFalsy();
+ });
+ });
+
+ describe('setPaginationInfo', () => {
+ it('should parse and set pagination info in state', () => {
+ const store = new GroupsStore();
+
+ store.setPaginationInfo(mockRawPageInfo);
+ expect(store.state.pageInfo.perPage).toBe(10);
+ expect(store.state.pageInfo.page).toBe(10);
+ expect(store.state.pageInfo.total).toBe(10);
+ expect(store.state.pageInfo.totalPages).toBe(10);
+ expect(store.state.pageInfo.nextPage).toBe(10);
+ expect(store.state.pageInfo.previousPage).toBe(10);
+ });
+ });
+
+ describe('formatGroupItem', () => {
+ it('should parse group item object and return updated object', () => {
+ let store;
+ let updatedGroupItem;
+
+ store = new GroupsStore();
+ updatedGroupItem = store.formatGroupItem(mockRawChildren[0]);
+ expect(Object.keys(updatedGroupItem).indexOf('fullName') > -1).toBeTruthy();
+ expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].children_count);
+ expect(updatedGroupItem.isChildrenLoading).toBe(false);
+ expect(updatedGroupItem.isBeingRemoved).toBe(false);
+
+ store = new GroupsStore(true);
+ updatedGroupItem = store.formatGroupItem(mockRawChildren[0]);
+ expect(Object.keys(updatedGroupItem).indexOf('fullName') > -1).toBeTruthy();
+ expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].subgroup_count);
+ });
+ });
+
+ describe('removeGroup', () => {
+ it('should remove children from group item in state', () => {
+ const store = new GroupsStore();
+ const rawParentGroup = Object.assign({}, mockGroups[0]);
+ const rawChildGroup = Object.assign({}, mockGroups[1]);
+
+ store.setGroups([rawParentGroup]);
+ store.setGroupChildren(store.state.groups[0], [rawChildGroup]);
+ const childItem = store.state.groups[0].children[0];
+
+ store.removeGroup(childItem, store.state.groups[0]);
+ expect(store.state.groups[0].children.length).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
index 0e01934d3a3..2443ffd48f3 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -1,53 +1,49 @@
-/* eslint-disable space-before-function-paren, no-var */
+import initTodoToggle from '~/header';
-import '~/header';
-import '~/lib/utils/text_utility';
+describe('Header', function () {
+ const todosPendingCount = '.todos-count';
+ const fixtureTemplate = 'issues/open-issue.html.raw';
-(function() {
- describe('Header', function() {
- var todosPendingCount = '.todos-count';
- var fixtureTemplate = 'issues/open-issue.html.raw';
+ function isTodosCountHidden() {
+ return $(todosPendingCount).hasClass('hidden');
+ }
- function isTodosCountHidden() {
- return $(todosPendingCount).hasClass('hidden');
- }
+ function triggerToggle(newCount) {
+ $(document).trigger('todo:toggle', newCount);
+ }
- function triggerToggle(newCount) {
- $(document).trigger('todo:toggle', newCount);
- }
+ preloadFixtures(fixtureTemplate);
+ beforeEach(() => {
+ initTodoToggle();
+ loadFixtures(fixtureTemplate);
+ });
- preloadFixtures(fixtureTemplate);
- beforeEach(function() {
- loadFixtures(fixtureTemplate);
- });
+ it('should update todos-count after receiving the todo:toggle event', () => {
+ triggerToggle('5');
+ expect($(todosPendingCount).text()).toEqual('5');
+ });
- it('should update todos-count after receiving the todo:toggle event', function() {
- triggerToggle(5);
- expect($(todosPendingCount).text()).toEqual('5');
- });
+ it('should hide todos-count when it is 0', () => {
+ triggerToggle('0');
+ expect(isTodosCountHidden()).toEqual(true);
+ });
- it('should hide todos-count when it is 0', function() {
- triggerToggle(0);
- expect(isTodosCountHidden()).toEqual(true);
+ it('should show todos-count when it is more than 0', () => {
+ triggerToggle('10');
+ expect(isTodosCountHidden()).toEqual(false);
+ });
+
+ describe('when todos-count is 1000', () => {
+ beforeEach(() => {
+ triggerToggle('1000');
});
- it('should show todos-count when it is more than 0', function() {
- triggerToggle(10);
+ it('should show todos-count', () => {
expect(isTodosCountHidden()).toEqual(false);
});
- describe('when todos-count is 1000', function() {
- beforeEach(function() {
- triggerToggle(1000);
- });
-
- it('should show todos-count', function() {
- expect(isTodosCountHidden()).toEqual(false);
- });
-
- it('should show 99+ for todos-count', function() {
- expect($(todosPendingCount).text()).toEqual('99+');
- });
+ it('should show 99+ for todos-count', () => {
+ expect($(todosPendingCount).text()).toEqual('99+');
});
});
-}).call(window);
+});
diff --git a/spec/javascripts/helpers/set_timeout_promise_helper.js b/spec/javascripts/helpers/set_timeout_promise_helper.js
new file mode 100644
index 00000000000..1478073413c
--- /dev/null
+++ b/spec/javascripts/helpers/set_timeout_promise_helper.js
@@ -0,0 +1,3 @@
+export default (time = 0) => new Promise((resolve) => {
+ setTimeout(resolve, time);
+});
diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js
index d7a2e86771c..34acdfbfba9 100644
--- a/spec/javascripts/helpers/vue_mount_component_helper.js
+++ b/spec/javascripts/helpers/vue_mount_component_helper.js
@@ -1,4 +1,8 @@
-export default (Component, props = {}) => new Component({
- propsData: props,
-}).$mount();
+export const createComponentWithStore = (Component, store, propsData = {}) => new Component({
+ store,
+ propsData,
+});
+export default (Component, props = {}, el = null) => new Component({
+ propsData: props,
+}).$mount(el);
diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js
index 3daeb91b1e2..9033eb9ce02 100644
--- a/spec/javascripts/integrations/integration_settings_form_spec.js
+++ b/spec/javascripts/integrations/integration_settings_form_spec.js
@@ -138,9 +138,9 @@ describe('IntegrationSettingsForm', () => {
deferred.resolve({ error: true, message: errorMessage, service_response: 'some error' });
const $flashContainer = $('.flash-container');
- expect($flashContainer.find('.flash-text').text()).toEqual('Test failed. some error');
+ expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error');
expect($flashContainer.find('.flash-action')).toBeDefined();
- expect($flashContainer.find('.flash-action').text()).toEqual('Save anyway');
+ expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway');
});
it('should submit form if ajax request responds without any error in test', () => {
@@ -168,7 +168,7 @@ describe('IntegrationSettingsForm', () => {
expect($flashAction).toBeDefined();
spyOn(integrationSettingsForm.$form, 'submit');
- $flashAction.trigger('click');
+ $flashAction.get(0).click();
expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
});
@@ -181,7 +181,7 @@ describe('IntegrationSettingsForm', () => {
deferred.reject();
- expect($('.flash-container .flash-text').text()).toEqual(errorMessage);
+ expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage);
});
it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => {
diff --git a/spec/javascripts/issuable_context_spec.js b/spec/javascripts/issuable_context_spec.js
deleted file mode 100644
index bcb2b7b24a0..00000000000
--- a/spec/javascripts/issuable_context_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/* global IssuableContext */
-import '~/issuable_context';
-import $ from 'jquery';
-
-describe('IssuableContext', () => {
- describe('toggleHiddenParticipants', () => {
- const event = jasmine.createSpyObj('event', ['preventDefault']);
-
- beforeEach(() => {
- spyOn($.fn, 'data').and.returnValue('data');
- spyOn($.fn, 'text').and.returnValue('data');
- });
-
- afterEach(() => {
- gl.lazyLoader = undefined;
- });
-
- it('calls loadCheck if lazyLoader is set', () => {
- gl.lazyLoader = jasmine.createSpyObj('lazyLoader', ['loadCheck']);
-
- IssuableContext.prototype.toggleHiddenParticipants(event);
-
- expect(gl.lazyLoader.loadCheck).toHaveBeenCalled();
- });
-
- it('does not throw if lazyLoader is not defined', () => {
- gl.lazyLoader = undefined;
-
- const toggle = IssuableContext.prototype.toggleHiddenParticipants.bind(null, event);
-
- expect(toggle).not.toThrow();
- });
- });
-});
diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js
index 45f55395d3a..ceee08d47c5 100644
--- a/spec/javascripts/issuable_spec.js
+++ b/spec/javascripts/issuable_spec.js
@@ -1,80 +1,44 @@
-/* global IssuableIndex */
-
-import '~/lib/utils/url_utility';
-import '~/issuable_index';
-
-(() => {
- const BASE_URL = '/user/project/issues?scope=all&state=closed';
- const DEFAULT_PARAMS = '&utf8=%E2%9C%93';
-
- function updateForm(formValues, form) {
- $.each(formValues, (id, value) => {
- $(`#${id}`, form).val(value);
- });
- }
-
- function resetForm(form) {
- $('input[name!="utf8"]', form).each((index, input) => {
- input.setAttribute('value', '');
+import IssuableIndex from '~/issuable_index';
+
+describe('Issuable', () => {
+ let Issuable;
+ describe('initBulkUpdate', () => {
+ it('should not set bulkUpdateSidebar', () => {
+ Issuable = new IssuableIndex('issue_');
+ expect(Issuable.bulkUpdateSidebar).not.toBeDefined();
});
- }
- describe('Issuable', () => {
- preloadFixtures('static/issuable_filter.html.raw');
+ it('should set bulkUpdateSidebar', () => {
+ const element = document.createElement('div');
+ element.classList.add('issues-bulk-update');
+ document.body.appendChild(element);
- beforeEach(() => {
- loadFixtures('static/issuable_filter.html.raw');
- IssuableIndex.init();
- });
-
- it('should be defined', () => {
- expect(window.IssuableIndex).toBeDefined();
+ Issuable = new IssuableIndex('issue_');
+ expect(Issuable.bulkUpdateSidebar).toBeDefined();
});
+ });
- describe('filtering', () => {
- let $filtersForm;
-
- beforeEach(() => {
- $filtersForm = $('.js-filter-form');
- loadFixtures('static/issuable_filter.html.raw');
- resetForm($filtersForm);
- });
-
- it('should contain only the default parameters', () => {
- spyOn(gl.utils, 'visitUrl');
-
- IssuableIndex.filterResults($filtersForm);
-
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
- });
-
- it('should filter for the phrase "broken"', () => {
- spyOn(gl.utils, 'visitUrl');
-
- updateForm({ search: 'broken' }, $filtersForm);
- IssuableIndex.filterResults($filtersForm);
- const params = `${DEFAULT_PARAMS}&search=broken`;
-
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
- });
-
- it('should keep query parameters after modifying filter', () => {
- spyOn(gl.utils, 'visitUrl');
+ describe('resetIncomingEmailToken', () => {
+ beforeEach(() => {
+ const element = document.createElement('a');
+ element.classList.add('incoming-email-token-reset');
+ element.setAttribute('href', 'foo');
+ document.body.appendChild(element);
- // initial filter
- updateForm({ milestone_title: 'v1.0' }, $filtersForm);
+ const input = document.createElement('input');
+ input.setAttribute('id', 'issue_email');
+ document.body.appendChild(input);
- IssuableIndex.filterResults($filtersForm);
- let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
+ Issuable = new IssuableIndex('issue_');
+ });
- // update filter
- updateForm({ label_name: 'Frontend' }, $filtersForm);
+ it('should send request to reset email token', () => {
+ spyOn(jQuery, 'ajax').and.callThrough();
+ document.querySelector('.incoming-email-token-reset').click();
- IssuableIndex.filterResults($filtersForm);
- params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
- });
+ expect(jQuery.ajax).toHaveBeenCalled();
+ expect(jQuery.ajax.calls.argsFor(0)[0].url).toEqual('foo');
});
});
-})();
+});
+
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index 583a3a74d77..b47a8bf705f 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -35,11 +35,12 @@ describe('Issuable output', () => {
canUpdate: true,
canDestroy: true,
endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
+ updateEndpoint: gl.TEST_HOST,
issuableRef: '#1',
initialTitleHtml: '',
initialTitleText: '',
- initialDescriptionHtml: '',
- initialDescriptionText: '',
+ initialDescriptionHtml: 'test',
+ initialDescriptionText: 'test',
markdownPreviewPath: '/',
markdownDocsPath: '/',
projectNamespace: '/',
@@ -223,23 +224,46 @@ describe('Issuable output', () => {
});
});
- it('closes form on error', (done) => {
- spyOn(window, 'Flash').and.callThrough();
- spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
- reject();
- }));
+ describe('error when updating', () => {
+ beforeEach(() => {
+ spyOn(window, 'Flash').and.callThrough();
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
+ reject();
+ }));
+ });
- vm.updateIssuable();
+ it('closes form on error', (done) => {
+ vm.updateIssuable();
- setTimeout(() => {
- expect(
- eventHub.$emit,
- ).toHaveBeenCalledWith('close.form');
- expect(
- window.Flash,
- ).toHaveBeenCalledWith('Error updating issue');
+ setTimeout(() => {
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('close.form');
+ expect(
+ window.Flash,
+ ).toHaveBeenCalledWith('Error updating issue');
- done();
+ done();
+ });
+ });
+
+ it('returns the correct error message for issuableType', (done) => {
+ vm.issuableType = 'merge request';
+
+ Vue.nextTick(() => {
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('close.form');
+ expect(
+ window.Flash,
+ ).toHaveBeenCalledWith('Error updating merge request');
+
+ done();
+ });
+ });
});
});
});
@@ -332,4 +356,15 @@ describe('Issuable output', () => {
.catch(done.fail);
});
});
+
+ describe('show inline edit button', () => {
+ it('should not render by default', () => {
+ expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined();
+ });
+
+ it('should render if showInlineEditButton', () => {
+ vm.showInlineEditButton = true;
+ expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined();
+ });
+ });
});
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
index 360691a3546..163e5cdd062 100644
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -1,11 +1,22 @@
import Vue from 'vue';
import descriptionComponent from '~/issue_show/components/description.vue';
+import * as taskList from '~/task_list';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Description component', () => {
let vm;
+ let DescriptionComponent;
+ const props = {
+ canUpdate: true,
+ descriptionHtml: 'test',
+ descriptionText: 'test',
+ updatedAt: new Date().toString(),
+ taskStatus: '',
+ updateUrl: gl.TEST_HOST,
+ };
beforeEach(() => {
- const Component = Vue.extend(descriptionComponent);
+ DescriptionComponent = Vue.extend(descriptionComponent);
if (!document.querySelector('.issuable-meta')) {
const metaData = document.createElement('div');
@@ -15,15 +26,11 @@ describe('Description component', () => {
document.body.appendChild(metaData);
}
- vm = new Component({
- propsData: {
- canUpdate: true,
- descriptionHtml: 'test',
- descriptionText: 'test',
- updatedAt: new Date().toString(),
- taskStatus: '',
- },
- }).$mount();
+ vm = mountComponent(DescriptionComponent, props);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
});
it('animates description changes', (done) => {
@@ -44,34 +51,46 @@ describe('Description component', () => {
});
});
- // TODO: gl.TaskList no longer exists. rewrite these tests once we have a way to rewire ES modules
-
- // it('re-inits the TaskList when description changed', (done) => {
- // spyOn(gl, 'TaskList');
- // vm.descriptionHtml = 'changed';
- //
- // setTimeout(() => {
- // expect(
- // gl.TaskList,
- // ).toHaveBeenCalled();
- //
- // done();
- // });
- // });
-
- // it('does not re-init the TaskList when canUpdate is false', (done) => {
- // spyOn(gl, 'TaskList');
- // vm.canUpdate = false;
- // vm.descriptionHtml = 'changed';
- //
- // setTimeout(() => {
- // expect(
- // gl.TaskList,
- // ).not.toHaveBeenCalled();
- //
- // done();
- // });
- // });
+ describe('TaskList', () => {
+ beforeEach(() => {
+ vm = mountComponent(DescriptionComponent, Object.assign({}, props, {
+ issuableType: 'issuableType',
+ }));
+ spyOn(taskList, 'default');
+ });
+
+ it('re-inits the TaskList when description changed', (done) => {
+ vm.descriptionHtml = 'changed';
+
+ setTimeout(() => {
+ expect(taskList.default).toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('does not re-init the TaskList when canUpdate is false', (done) => {
+ vm.canUpdate = false;
+ vm.descriptionHtml = 'changed';
+
+ setTimeout(() => {
+ expect(taskList.default).not.toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('calls with issuableType dataType', (done) => {
+ vm.descriptionHtml = 'changed';
+
+ setTimeout(() => {
+ expect(taskList.default).toHaveBeenCalledWith({
+ dataType: 'issuableType',
+ fieldName: 'description',
+ selector: '.detail-page-description',
+ });
+ done();
+ });
+ });
+ });
describe('taskStatus', () => {
it('adds full taskStatus', (done) => {
@@ -126,4 +145,8 @@ describe('Description component', () => {
});
});
});
+
+ it('sets data-update-url', () => {
+ expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(gl.TEST_HOST);
+ });
});
diff --git a/spec/javascripts/issue_show/components/edit_actions_spec.js b/spec/javascripts/issue_show/components/edit_actions_spec.js
index f6625b748b6..d779ab7bb31 100644
--- a/spec/javascripts/issue_show/components/edit_actions_spec.js
+++ b/spec/javascripts/issue_show/components/edit_actions_spec.js
@@ -61,6 +61,15 @@ describe('Edit Actions components', () => {
});
});
+ it('should not show delete button if showDeleteButton is false', (done) => {
+ vm.showDeleteButton = false;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.btn-danger')).toBeNull();
+ done();
+ });
+ });
+
describe('updateIssuable', () => {
it('sends update.issauble event when clicking save button', () => {
vm.$el.querySelector('.btn-save').click();
diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js
index a2d90a9b9f5..5370f4e1fea 100644
--- a/spec/javascripts/issue_show/components/title_spec.js
+++ b/spec/javascripts/issue_show/components/title_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import Store from '~/issue_show/stores';
import titleComponent from '~/issue_show/components/title.vue';
+import eventHub from '~/issue_show/event_hub';
describe('Title component', () => {
let vm;
@@ -25,7 +26,7 @@ describe('Title component', () => {
it('renders title HTML', () => {
expect(
- vm.$el.innerHTML.trim(),
+ vm.$el.querySelector('.title').innerHTML.trim(),
).toBe('Testing <img>');
});
@@ -47,12 +48,12 @@ describe('Title component', () => {
Vue.nextTick(() => {
expect(
- vm.$el.classList.contains('issue-realtime-pre-pulse'),
+ vm.$el.querySelector('.title').classList.contains('issue-realtime-pre-pulse'),
).toBeTruthy();
setTimeout(() => {
expect(
- vm.$el.classList.contains('issue-realtime-trigger-pulse'),
+ vm.$el.querySelector('.title').classList.contains('issue-realtime-trigger-pulse'),
).toBeTruthy();
done();
@@ -72,4 +73,36 @@ describe('Title component', () => {
done();
});
});
+
+ describe('inline edit button', () => {
+ beforeEach(() => {
+ spyOn(eventHub, '$emit');
+ });
+
+ it('should not show by default', () => {
+ expect(vm.$el.querySelector('.btn-edit')).toBeNull();
+ });
+
+ it('should not show if canUpdate is false', () => {
+ vm.showInlineEditButton = true;
+ vm.canUpdate = false;
+ expect(vm.$el.querySelector('.btn-edit')).toBeNull();
+ });
+
+ it('should show if showInlineEditButton and canUpdate', () => {
+ vm.showInlineEditButton = true;
+ vm.canUpdate = true;
+ expect(vm.$el.querySelector('.btn-edit')).toBeDefined();
+ });
+
+ it('should trigger open.form event when clicked', () => {
+ vm.showInlineEditButton = true;
+ vm.canUpdate = true;
+
+ Vue.nextTick(() => {
+ vm.$el.querySelector('.btn-edit').click();
+ expect(eventHub.$emit).toHaveBeenCalledWith('open.form');
+ });
+ });
+ });
});
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 60a452f2223..3636aac79a0 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,6 +1,5 @@
/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
import Issue from '~/issue';
-import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import '~/lib/utils/text_utility';
describe('Issue', function() {
@@ -189,37 +188,4 @@ describe('Issue', function() {
});
});
});
-
- describe('units', () => {
- describe('class constructor', () => {
- it('calls .initCloseReopenReport', () => {
- spyOn(Issue.prototype, 'initCloseReopenReport');
-
- new Issue(); // eslint-disable-line no-new
-
- expect(Issue.prototype.initCloseReopenReport).toHaveBeenCalled();
- });
- });
-
- describe('initCloseReopenReport', () => {
- it('calls .initDroplab', () => {
- const container = jasmine.createSpyObj('container', ['querySelector']);
- const dropdownTrigger = {};
- const dropdownList = {};
- const button = {};
-
- spyOn(document, 'querySelector').and.returnValue(container);
- spyOn(CloseReopenReportToggle.prototype, 'initDroplab');
- container.querySelector.and.returnValues(dropdownTrigger, dropdownList, button);
-
- Issue.prototype.initCloseReopenReport();
-
- expect(document.querySelector).toHaveBeenCalledWith('.js-issuable-close-dropdown');
- expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-toggle');
- expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-menu');
- expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-button');
- expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled();
- });
- });
- });
});
diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js
new file mode 100644
index 00000000000..5e67911d338
--- /dev/null
+++ b/spec/javascripts/job_spec.js
@@ -0,0 +1,305 @@
+import { bytesToKiB } from '~/lib/utils/number_utils';
+import '~/lib/utils/datetime_utility';
+import '~/lib/utils/url_utility';
+import Job from '~/job';
+import '~/breakpoints';
+
+describe('Job', () => {
+ const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
+
+ preloadFixtures('builds/build-with-artifacts.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('builds/build-with-artifacts.html.raw');
+ });
+
+ describe('class constructor', () => {
+ beforeEach(() => {
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
+ describe('setup', () => {
+ beforeEach(function () {
+ this.job = new Job();
+ });
+
+ it('copies build options', function () {
+ expect(this.job.pageUrl).toBe(JOB_URL);
+ expect(this.job.buildStatus).toBe('success');
+ expect(this.job.buildStage).toBe('test');
+ expect(this.job.state).toBe('');
+ });
+
+ it('only shows the jobs matching the current stage', () => {
+ expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false);
+ expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true);
+ expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
+ });
+
+ it('selects the current stage in the build dropdown menu', () => {
+ expect($('.stage-selection').text()).toBe('test');
+ });
+
+ it('updates the jobs when the build dropdown changes', () => {
+ $('.stage-item:contains("build")').click();
+
+ expect($('.stage-selection').text()).toBe('build');
+ expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true);
+ expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false);
+ expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
+ });
+
+ it('displays the remove date correctly', () => {
+ const removeDateElement = document.querySelector('.js-artifacts-remove');
+ expect(removeDateElement.innerText.trim()).toBe('1 year remaining');
+ });
+ });
+
+ describe('running build', () => {
+ it('updates the build trace on an interval', function () {
+ const deferred1 = $.Deferred();
+ const deferred2 = $.Deferred();
+ const deferred3 = $.Deferred();
+ spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
+ spyOn(gl.utils, 'visitUrl');
+
+ deferred1.resolve({
+ html: '<span>Update<span>',
+ status: 'running',
+ state: 'newstate',
+ append: true,
+ complete: false,
+ });
+
+ deferred2.resolve();
+
+ deferred3.resolve({
+ html: '<span>More</span>',
+ status: 'running',
+ state: 'finalstate',
+ append: true,
+ complete: true,
+ });
+
+ this.job = new Job();
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+ expect(this.job.state).toBe('newstate');
+
+ jasmine.clock().tick(4001);
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
+ expect(this.job.state).toBe('finalstate');
+ });
+
+ it('replaces the entire build trace', () => {
+ const deferred1 = $.Deferred();
+ const deferred2 = $.Deferred();
+ const deferred3 = $.Deferred();
+
+ spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
+
+ spyOn(gl.utils, 'visitUrl');
+
+ deferred1.resolve({
+ html: '<span>Update<span>',
+ status: 'running',
+ append: false,
+ complete: false,
+ });
+
+ deferred2.resolve();
+
+ deferred3.resolve({
+ html: '<span>Different</span>',
+ status: 'running',
+ append: false,
+ });
+
+ this.job = new Job();
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+
+ jasmine.clock().tick(4001);
+
+ expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
+ expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
+ });
+ });
+
+ describe('truncated information', () => {
+ describe('when size is less than total', () => {
+ it('shows information about truncated log', () => {
+ spyOn(gl.utils, 'visitUrl');
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ deferred.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 50,
+ total: 100,
+ });
+
+ this.job = new Job();
+
+ expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
+ });
+
+ it('shows the size in KiB', () => {
+ const size = 50;
+ spyOn(gl.utils, 'visitUrl');
+ const deferred = $.Deferred();
+
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ deferred.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size,
+ total: 100,
+ });
+
+ this.job = new Job();
+
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${bytesToKiB(size)}`);
+ });
+
+ it('shows incremented size', () => {
+ const deferred1 = $.Deferred();
+ const deferred2 = $.Deferred();
+ const deferred3 = $.Deferred();
+
+ spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
+
+ spyOn(gl.utils, 'visitUrl');
+
+ deferred1.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 50,
+ total: 100,
+ });
+
+ deferred2.resolve();
+
+ this.job = new Job();
+
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${bytesToKiB(50)}`);
+
+ jasmine.clock().tick(4001);
+
+ deferred3.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: true,
+ size: 10,
+ total: 100,
+ });
+
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${bytesToKiB(60)}`);
+ });
+
+ it('renders the raw link', () => {
+ const deferred = $.Deferred();
+ spyOn(gl.utils, 'visitUrl');
+
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ deferred.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 50,
+ total: 100,
+ });
+
+ this.job = new Job();
+
+ expect(
+ document.querySelector('.js-raw-link').textContent.trim(),
+ ).toContain('Complete Raw');
+ });
+ });
+
+ describe('when size is equal than total', () => {
+ it('does not show the trunctated information', () => {
+ const deferred = $.Deferred();
+ spyOn(gl.utils, 'visitUrl');
+
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ deferred.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 100,
+ total: 100,
+ });
+
+ this.job = new Job();
+
+ expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
+ });
+ });
+ });
+
+ describe('output trace', () => {
+ beforeEach(() => {
+ const deferred = $.Deferred();
+ spyOn(gl.utils, 'visitUrl');
+
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ deferred.resolve({
+ html: '<span>Update</span>',
+ status: 'success',
+ append: false,
+ size: 50,
+ total: 100,
+ });
+
+ this.job = new Job();
+ });
+
+ it('should render trace controls', () => {
+ const controllers = document.querySelector('.controllers');
+
+ expect(controllers.querySelector('.js-raw-link-controller')).toBeDefined();
+ expect(controllers.querySelector('.js-erase-link')).toBeDefined();
+ expect(controllers.querySelector('.js-scroll-up')).toBeDefined();
+ expect(controllers.querySelector('.js-scroll-down')).toBeDefined();
+ });
+
+ it('should render received output', () => {
+ expect(
+ document.querySelector('.js-build-output').innerHTML,
+ ).toEqual('<span>Update</span>');
+ });
+ });
+ });
+
+ describe('getBuildTrace', () => {
+ it('should request build trace with state parameter', (done) => {
+ spyOn(jQuery, 'ajax').and.callThrough();
+ // eslint-disable-next-line no-new
+ new Job();
+
+ setTimeout(() => {
+ expect(jQuery.ajax).toHaveBeenCalledWith(
+ { url: `${JOB_URL}/trace.json`, data: { state: '' } },
+ );
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js
index c7179b3e03d..4a210faa017 100644
--- a/spec/javascripts/jobs/header_spec.js
+++ b/spec/javascripts/jobs/header_spec.js
@@ -30,7 +30,6 @@ describe('Job details header', () => {
email: 'foo@bar.com',
avatar_url: 'link',
},
- retry_path: 'path',
new_issue_path: 'path',
},
isLoading: false,
@@ -49,12 +48,6 @@ describe('Job details header', () => {
).toEqual('failed Job #123 triggered 3 weeks ago by Foo');
});
- it('should render retry link', () => {
- expect(
- vm.$el.querySelector('.js-retry-button').getAttribute('href'),
- ).toEqual(props.job.retry_path);
- });
-
it('should render new issue link', () => {
expect(
vm.$el.querySelector('.js-new-issue').getAttribute('href'),
diff --git a/spec/javascripts/jobs/job_details_mediator_spec.js b/spec/javascripts/jobs/job_details_mediator_spec.js
index 1d7fa7e12fc..3069a0cd60e 100644
--- a/spec/javascripts/jobs/job_details_mediator_spec.js
+++ b/spec/javascripts/jobs/job_details_mediator_spec.js
@@ -1,39 +1,35 @@
-import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import JobMediator from '~/jobs/job_details_mediator';
import job from './mock_data';
describe('JobMediator', () => {
let mediator;
+ let mock;
beforeEach(() => {
- mediator = new JobMediator({ endpoint: 'foo' });
+ mediator = new JobMediator({ endpoint: 'jobs/40291672.json' });
+ mock = new MockAdapter(axios);
});
it('should set defaults', () => {
expect(mediator.store).toBeDefined();
expect(mediator.service).toBeDefined();
- expect(mediator.options).toEqual({ endpoint: 'foo' });
+ expect(mediator.options).toEqual({ endpoint: 'jobs/40291672.json' });
expect(mediator.state.isLoading).toEqual(false);
});
describe('request and store data', () => {
- const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(job), {
- status: 200,
- }));
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
+ mock.onGet().reply(200, job, {});
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor);
+ mock.restore();
});
it('should store received data', (done) => {
mediator.fetchJob();
-
setTimeout(() => {
expect(mediator.store.state.job).toEqual(job);
done();
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 17e4ef26b2c..43532275121 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -22,7 +22,7 @@ export default {
details_path: '/root/ci-mock/-/jobs/4757',
favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
action: {
- icon: 'icon_action_retry',
+ icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/-/jobs/4757/retry',
method: 'post',
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js
index e47adc49224..a197b35f6fb 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js
+++ b/spec/javascripts/labels_issue_sidebar_spec.js
@@ -1,14 +1,12 @@
/* eslint-disable no-new */
-/* global IssuableContext */
-/* global LabelsSelect */
+import IssuableContext from '~/issuable_context';
+import LabelsSelect from '~/labels_select';
import '~/gl_dropdown';
import 'select2';
import '~/api';
import '~/create_label';
-import '~/issuable_context';
import '~/users_select';
-import '~/labels_select';
(() => {
let saveLabelCount = 0;
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index f86f2f260c3..0a9d815f469 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -142,44 +142,33 @@ describe('common_utils', () => {
});
});
- describe('setParamInURL', () => {
+ describe('historyPushState', () => {
afterEach(() => {
- window.history.pushState({}, null, '');
+ window.history.replaceState({}, null, null);
});
- it('should return the parameter', () => {
- window.history.replaceState({}, null, '');
-
- expect(commonUtils.setParamInURL('page', 156)).toBe('?page=156');
- expect(commonUtils.setParamInURL('page', '156')).toBe('?page=156');
- });
+ it('should call pushState with the correct path', () => {
+ spyOn(window.history, 'pushState');
- it('should update the existing parameter when its a number', () => {
- window.history.pushState({}, null, '?page=15');
+ commonUtils.historyPushState('newpath?page=2');
- expect(commonUtils.setParamInURL('page', 16)).toBe('?page=16');
- expect(commonUtils.setParamInURL('page', '16')).toBe('?page=16');
- expect(commonUtils.setParamInURL('page', true)).toBe('?page=true');
+ expect(window.history.pushState).toHaveBeenCalled();
+ expect(window.history.pushState.calls.allArgs()[0][2]).toContain('newpath?page=2');
});
+ });
- it('should update the existing parameter when its a string', () => {
- window.history.pushState({}, null, '?scope=all');
-
- expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished');
- });
-
- it('should update the existing parameter when more than one parameter exists', () => {
- window.history.pushState({}, null, '?scope=all&page=15');
-
- expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15');
+ describe('parseQueryStringIntoObject', () => {
+ it('should return object with query parameters', () => {
+ expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ scope: 'all', page: '2' });
+ expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' });
+ expect(commonUtils.parseQueryStringIntoObject()).toEqual({});
});
+ });
- it('should add a new parameter to the end of the existing ones', () => {
- window.history.pushState({}, null, '?scope=all');
-
- expect(commonUtils.setParamInURL('page', 16)).toBe('?scope=all&page=16');
- expect(commonUtils.setParamInURL('page', '16')).toBe('?scope=all&page=16');
- expect(commonUtils.setParamInURL('page', true)).toBe('?scope=all&page=true');
+ describe('buildUrlWithCurrentLocation', () => {
+ it('should build an url with current location and given parameters', () => {
+ expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname);
+ expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(`${window.location.pathname}?page=2`);
});
});
@@ -467,15 +456,27 @@ describe('common_utils', () => {
commonUtils.ajaxPost(requestURL, data);
expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST');
});
+ });
- describe('gl.utils.spriteIcon', () => {
- beforeEach(() => {
- window.gon.sprite_icons = 'icons.svg';
- });
+ describe('spriteIcon', () => {
+ let beforeGon;
- it('should return the svg for a linked icon', () => {
- expect(gl.utils.spriteIcon('test')).toEqual('<svg><use xlink:href="icons.svg#test" /></svg>');
- });
+ beforeEach(() => {
+ window.gon = window.gon || {};
+ beforeGon = Object.assign({}, window.gon);
+ window.gon.sprite_icons = 'icons.svg';
+ });
+
+ afterEach(() => {
+ window.gon = beforeGon;
+ });
+
+ it('should return the svg for a linked icon', () => {
+ expect(commonUtils.spriteIcon('test')).toEqual('<svg ><use xlink:href="icons.svg#test" /></svg>');
+ });
+
+ it('should set svg className when passed', () => {
+ expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual('<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>');
});
});
});
diff --git a/spec/javascripts/lib/utils/datefix_spec.js b/spec/javascripts/lib/utils/datefix_spec.js
new file mode 100644
index 00000000000..e58ac4300ba
--- /dev/null
+++ b/spec/javascripts/lib/utils/datefix_spec.js
@@ -0,0 +1,27 @@
+import { pad, pikadayToString } from '~/lib/utils/datefix';
+
+describe('datefix', () => {
+ describe('pad', () => {
+ it('should add a 0 when length is smaller than 2', () => {
+ expect(pad(2)).toEqual('02');
+ });
+
+ it('should not add a zero when lenght matches the default', () => {
+ expect(pad(12)).toEqual('12');
+ });
+
+ it('should add a 0 when lenght is smaller than the provided', () => {
+ expect(pad(12, 3)).toEqual('012');
+ });
+ });
+
+ describe('parsePikadayDate', () => {
+ // removed because of https://gitlab.com/gitlab-org/gitlab-ce/issues/39834
+ });
+
+ describe('pikadayToString', () => {
+ it('should format a UTC date into yyyy-mm-dd format', () => {
+ expect(pikadayToString(new Date('2020-01-29'))).toEqual('2020-01-29');
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js
index 83c92deccdc..fcf27f6805f 100644
--- a/spec/javascripts/lib/utils/number_utility_spec.js
+++ b/spec/javascripts/lib/utils/number_utility_spec.js
@@ -1,4 +1,4 @@
-import { formatRelevantDigits, bytesToKiB, bytesToMiB } from '~/lib/utils/number_utils';
+import { formatRelevantDigits, bytesToKiB, bytesToMiB, bytesToGiB, numberToHumanSize } from '~/lib/utils/number_utils';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
@@ -52,4 +52,29 @@ describe('Number Utils', () => {
expect(bytesToMiB(1000000)).toEqual(0.95367431640625);
});
});
+
+ describe('bytesToGiB', () => {
+ it('calculates GiB for the given bytes', () => {
+ expect(bytesToGiB(1073741824)).toEqual(1);
+ expect(bytesToGiB(10737418240)).toEqual(10);
+ });
+ });
+
+ describe('numberToHumanSize', () => {
+ it('should return bytes', () => {
+ expect(numberToHumanSize(654)).toEqual('654 bytes');
+ });
+
+ it('should return KiB', () => {
+ expect(numberToHumanSize(1079)).toEqual('1.05 KiB');
+ });
+
+ it('should return MiB', () => {
+ expect(numberToHumanSize(10485764)).toEqual('10.00 MiB');
+ });
+
+ it('should return GiB', () => {
+ expect(numberToHumanSize(10737418240)).toEqual('10.00 GiB');
+ });
+ });
});
diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js
index 2aa7011ca51..9b8f68f1676 100644
--- a/spec/javascripts/lib/utils/poll_spec.js
+++ b/spec/javascripts/lib/utils/poll_spec.js
@@ -155,7 +155,7 @@ describe('Poll', () => {
successCallback: () => {
Polling.stop();
setTimeout(() => {
- Polling.restart();
+ Polling.restart({ data: { page: 4 } });
}, 0);
},
errorCallback: callbacks.error,
@@ -170,10 +170,10 @@ describe('Poll', () => {
Polling.stop();
expect(service.fetch.calls.count()).toEqual(2);
- expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
+ expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
expect(Polling.stop).toHaveBeenCalled();
expect(Polling.restart).toHaveBeenCalled();
-
+ expect(Polling.options.data).toEqual({ page: 4 });
done();
});
});
diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js
new file mode 100644
index 00000000000..a95a7e2a5be
--- /dev/null
+++ b/spec/javascripts/lib/utils/text_markdown_spec.js
@@ -0,0 +1,62 @@
+import textUtils from '~/lib/utils/text_markdown';
+
+describe('init markdown', () => {
+ let textArea;
+
+ beforeAll(() => {
+ textArea = document.createElement('textarea');
+ document.querySelector('body').appendChild(textArea);
+ textArea.focus();
+ });
+
+ afterAll(() => {
+ textArea.parentNode.removeChild(textArea);
+ });
+
+ describe('without selection', () => {
+ it('inserts the tag on an empty line', () => {
+ const initialValue = '';
+
+ textArea.value = initialValue;
+ textArea.selectionStart = 0;
+ textArea.selectionEnd = 0;
+
+ textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
+
+ it('inserts the tag on a new line if the current one is not empty', () => {
+ const initialValue = 'some text';
+
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+ textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}\n* `);
+ });
+
+ it('inserts the tag on the same line if the current line only contains spaces', () => {
+ const initialValue = ' ';
+
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+ textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
+
+ it('inserts the tag on the same line if the current line only contains tabs', () => {
+ const initialValue = '\t\t\t';
+
+ textArea.value = initialValue;
+ textArea.setSelectionRange(initialValue.length, initialValue.length);
+
+ textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+
+ expect(textArea.value).toEqual(`${initialValue}* `);
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index f1a975ba962..1f46c225071 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -1,109 +1,65 @@
-import '~/lib/utils/text_utility';
+import * as textUtils from '~/lib/utils/text_utility';
describe('text_utility', () => {
- describe('gl.text.getTextWidth', () => {
- it('returns zero width when no text is passed', () => {
- expect(gl.text.getTextWidth('')).toBe(0);
+ describe('addDelimiter', () => {
+ it('should add a delimiter to the given string', () => {
+ expect(textUtils.addDelimiter('1234')).toEqual('1,234');
+ expect(textUtils.addDelimiter('222222')).toEqual('222,222');
});
- it('returns zero width when no text is passed and font is passed', () => {
- expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
+ it('should not add a delimiter if string contains no numbers', () => {
+ expect(textUtils.addDelimiter('aaaa')).toEqual('aaaa');
});
+ });
- it('returns width when text is passed', () => {
- expect(gl.text.getTextWidth('foo') > 0).toBe(true);
+ describe('highCountTrim', () => {
+ it('returns 99+ for count >= 100', () => {
+ expect(textUtils.highCountTrim(105)).toBe('99+');
+ expect(textUtils.highCountTrim(100)).toBe('99+');
});
- it('returns bigger width when font is larger', () => {
- const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
- const regular = gl.text.getTextWidth('foo', '10px sans-serif');
- expect(largeFont > regular).toBe(true);
+ it('returns exact number for count < 100', () => {
+ expect(textUtils.highCountTrim(45)).toBe(45);
});
});
- describe('gl.text.pluralize', () => {
- it('returns pluralized', () => {
- expect(gl.text.pluralize('test', 2)).toBe('tests');
- });
-
- it('returns pluralized when count is 0', () => {
- expect(gl.text.pluralize('test', 0)).toBe('tests');
+ describe('capitalizeFirstCharacter', () => {
+ it('returns string with first letter capitalized', () => {
+ expect(textUtils.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab');
+ expect(textUtils.highCountTrim(105)).toBe('99+');
+ expect(textUtils.highCountTrim(100)).toBe('99+');
});
+ });
- it('does not return pluralized', () => {
- expect(gl.text.pluralize('test', 1)).toBe('test');
+ describe('humanize', () => {
+ it('should remove underscores and uppercase the first letter', () => {
+ expect(textUtils.humanize('foo_bar')).toEqual('Foo bar');
});
});
- describe('gl.text.highCountTrim', () => {
- it('returns 99+ for count >= 100', () => {
- expect(gl.text.highCountTrim(105)).toBe('99+');
- expect(gl.text.highCountTrim(100)).toBe('99+');
+ describe('pluralize', () => {
+ it('should pluralize given string', () => {
+ expect(textUtils.pluralize('test', 2)).toBe('tests');
});
- it('returns exact number for count < 100', () => {
- expect(gl.text.highCountTrim(45)).toBe(45);
+ it('should pluralize when count is 0', () => {
+ expect(textUtils.pluralize('test', 0)).toBe('tests');
});
- });
- describe('gl.text.insertText', () => {
- let textArea;
-
- beforeAll(() => {
- textArea = document.createElement('textarea');
- document.querySelector('body').appendChild(textArea);
- textArea.focus();
+ it('should not pluralize when count is 1', () => {
+ expect(textUtils.pluralize('test', 1)).toBe('test');
});
+ });
- afterAll(() => {
- textArea.parentNode.removeChild(textArea);
+ describe('dasherize', () => {
+ it('should replace underscores with dashes', () => {
+ expect(textUtils.dasherize('foo_bar_foo')).toEqual('foo-bar-foo');
});
+ });
- describe('without selection', () => {
- it('inserts the tag on an empty line', () => {
- const initialValue = '';
-
- textArea.value = initialValue;
- textArea.selectionStart = 0;
- textArea.selectionEnd = 0;
-
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
-
- expect(textArea.value).toEqual(`${initialValue}* `);
- });
-
- it('inserts the tag on a new line if the current one is not empty', () => {
- const initialValue = 'some text';
-
- textArea.value = initialValue;
- textArea.setSelectionRange(initialValue.length, initialValue.length);
-
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
-
- expect(textArea.value).toEqual(`${initialValue}\n* `);
- });
-
- it('inserts the tag on the same line if the current line only contains spaces', () => {
- const initialValue = ' ';
-
- textArea.value = initialValue;
- textArea.setSelectionRange(initialValue.length, initialValue.length);
-
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
-
- expect(textArea.value).toEqual(`${initialValue}* `);
- });
-
- it('inserts the tag on the same line if the current line only contains tabs', () => {
- const initialValue = '\t\t\t';
-
- textArea.value = initialValue;
- textArea.setSelectionRange(initialValue.length, initialValue.length);
-
- gl.text.insertText(textArea, textArea.value, '*', null, '', false);
-
- expect(textArea.value).toEqual(`${initialValue}* `);
- });
+ describe('slugify', () => {
+ it('should remove accents and convert to lower case', () => {
+ expect(textUtils.slugify('João')).toEqual('joão');
});
});
});
diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js
index 395dc560671..6054b75d0b8 100644
--- a/spec/javascripts/merge_request_notes_spec.js
+++ b/spec/javascripts/merge_request_notes_spec.js
@@ -1,6 +1,6 @@
/* global Notes */
-import 'vendor/autosize';
+import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
import '~/render_gfm';
@@ -23,12 +23,17 @@ describe('Merge request notes', () => {
loadFixtures(discussionTabFixture);
gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads';
- $('body').data('page', 'projects:merge_requests:show');
+ $('body').attr('data-page', 'projects:merge_requests:show');
window.gon.current_user_id = $('.note:last').data('author-id');
return new Notes('', []);
});
+ afterEach(() => {
+ // Undo what we did to the shared <body>
+ $('body').removeAttr('data-page');
+ });
+
describe('up arrow', () => {
it('edits last comment when triggered in main form', () => {
const upArrowEvent = $.Event('keydown');
@@ -71,12 +76,17 @@ describe('Merge request notes', () => {
<textarea class="js-note-text"></textarea>
</form>`;
setFixtures(diffsResponse.html + noteFormHtml);
- $('body').data('page', 'projects:merge_requests:show');
+ $('body').attr('data-page', 'projects:merge_requests:show');
window.gon.current_user_id = $('.note:last').data('author-id');
return new Notes('', []);
});
+ afterEach(() => {
+ // Undo what we did to the shared <body>
+ $('body').removeAttr('data-page');
+ });
+
describe('up arrow', () => {
it('edits last comment in discussion when triggered in discussion form', (done) => {
const upArrowEvent = $.Event('keydown');
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index ccdbfcba692..e441d1153ed 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -5,8 +5,7 @@ import '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
import '~/breakpoints';
import '~/lib/utils/common_utils';
-import '~/diff';
-import '~/files_comment_button';
+import Diff from '~/diff';
import '~/notes';
import 'vendor/jquery.scrollTo';
@@ -225,7 +224,7 @@ import 'vendor/jquery.scrollTo';
describe('with "Side-by-side"/parallel diff view', () => {
beforeEach(function () {
this.class.diffViewType = () => 'parallel';
- gl.Diff.prototype.diffViewType = () => 'parallel';
+ Diff.prototype.diffViewType = () => 'parallel';
});
it('maintains `container-limited` for pipelines tab', function (done) {
@@ -277,7 +276,7 @@ import 'vendor/jquery.scrollTo';
describe('loadDiff', function () {
beforeEach(() => {
loadFixtures('merge_requests/diff_comment.html.raw');
- spyOn(window.gl.utils, 'getPagePath').and.returnValue('merge_requests');
+ $('body').attr('data-page', 'projects:merge_requests:show');
window.gl.ImageFile = () => {};
window.notes = new Notes('', []);
spyOn(window.notes, 'toggleDiffNote').and.callThrough();
@@ -286,6 +285,9 @@ import 'vendor/jquery.scrollTo';
afterEach(() => {
delete window.gl.ImageFile;
delete window.notes;
+
+ // Undo what we did to the shared <body>
+ $('body').removeAttr('data-page');
});
it('requires an absolute pathname', function () {
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index 752fdfb4614..9885b8a790f 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -1,6 +1,8 @@
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue';
-import { MonitorMockInterceptor } from './mock_data';
+import axios from '~/lib/utils/axios_utils';
+import { metricsGroupsAPIResponse, mockApiEndpoint } from './mock_data';
describe('Dashboard', () => {
const fixtureName = 'environments/metrics/metrics.html.raw';
@@ -26,13 +28,17 @@ describe('Dashboard', () => {
});
describe('requests information to the server', () => {
+ let mock;
beforeEach(() => {
document.querySelector('#prometheus-graphs').setAttribute('data-has-metrics', 'true');
- Vue.http.interceptors.push(MonitorMockInterceptor);
+ mock = new MockAdapter(axios);
+ mock.onGet(mockApiEndpoint).reply(200, {
+ metricsGroupsAPIResponse,
+ });
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, MonitorMockInterceptor);
+ mock.reset();
});
it('shows up a loading state', (done) => {
diff --git a/spec/javascripts/monitoring/graph/legend_spec.js b/spec/javascripts/monitoring/graph/legend_spec.js
index 2571b7ef869..145c8db28d5 100644
--- a/spec/javascripts/monitoring/graph/legend_spec.js
+++ b/spec/javascripts/monitoring/graph/legend_spec.js
@@ -28,7 +28,7 @@ const defaultValuesComponent = {
currentDataIndex: 0,
};
-const timeSeries = createTimeSeries(convertedMetrics[0].queries[0],
+const timeSeries = createTimeSeries(convertedMetrics[0].queries,
defaultValuesComponent.graphWidth, defaultValuesComponent.graphHeight,
defaultValuesComponent.graphHeightOffset);
diff --git a/spec/javascripts/monitoring/graph_path_spec.js b/spec/javascripts/monitoring/graph_path_spec.js
index 81825a3ae87..c83bd19345f 100644
--- a/spec/javascripts/monitoring/graph_path_spec.js
+++ b/spec/javascripts/monitoring/graph_path_spec.js
@@ -13,7 +13,7 @@ const createComponent = (propsData) => {
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
-const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], 428, 272, 120);
+const timeSeries = createTimeSeries(convertedMetrics[0].queries, 428, 272, 120);
const firstTimeSeries = timeSeries[0];
describe('Monitoring Paths', () => {
@@ -32,4 +32,21 @@ describe('Monitoring Paths', () => {
expect(metricLine.getAttribute('stroke')).toBe('#1f78d1');
expect(metricLine.getAttribute('d')).toBe(firstTimeSeries.linePath);
});
+
+ describe('Computed properties', () => {
+ it('strokeDashArray', () => {
+ const component = createComponent({
+ generatedLinePath: firstTimeSeries.linePath,
+ generatedAreaPath: firstTimeSeries.areaPath,
+ lineColor: firstTimeSeries.lineColor,
+ areaColor: firstTimeSeries.areaColor,
+ });
+
+ component.lineStyle = 'dashed';
+ expect(component.strokeDashArray).toBe('3, 1');
+
+ component.lineStyle = 'dotted';
+ expect(component.strokeDashArray).toBe('1, 1');
+ });
+ });
});
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 7ceab657464..6b34855b8b2 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -2425,13 +2425,6 @@ const metricsGroupsAPIResponse = {
export default metricsGroupsAPIResponse;
-const responseMockData = {
- 'GET': {
- '/root/hello-prometheus/environments/30/additional_metrics.json': metricsGroupsAPIResponse,
- 'http://test.host/frontend-fixtures/environments-project/environments/1/additional_metrics.json': metricsGroupsAPIResponse, // TODO: MAke sure this works in the monitoring_bundle_spec
- },
-};
-
export const deploymentData = [
{
id: 111,
@@ -8320,11 +8313,3 @@ export function convertDatesMultipleSeries(multipleSeries) {
});
return convertedMultiple;
}
-
-export function MonitorMockInterceptor(request, next) {
- const body = responseMockData[request.method.toUpperCase()][request.url];
-
- next(request.respondWith(JSON.stringify(body), {
- status: 200,
- }));
-}
diff --git a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
index 7e44a9ade9e..99584c75287 100644
--- a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
+++ b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
@@ -2,7 +2,7 @@ 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], 428, 272, 120);
+const timeSeries = createTimeSeries(convertedMetrics[0].queries, 428, 272, 120);
const firstTimeSeries = timeSeries[0];
describe('Multiple time series', () => {
diff --git a/spec/javascripts/namespace_select_spec.js b/spec/javascripts/namespace_select_spec.js
new file mode 100644
index 00000000000..9d7625ca269
--- /dev/null
+++ b/spec/javascripts/namespace_select_spec.js
@@ -0,0 +1,65 @@
+import NamespaceSelect from '~/namespace_select';
+
+describe('NamespaceSelect', () => {
+ beforeEach(() => {
+ spyOn($.fn, 'glDropdown');
+ });
+
+ it('initializes glDropdown', () => {
+ const dropdown = document.createElement('div');
+
+ // eslint-disable-next-line no-new
+ new NamespaceSelect({ dropdown });
+
+ expect($.fn.glDropdown).toHaveBeenCalled();
+ });
+
+ describe('as input', () => {
+ let glDropdownOptions;
+
+ beforeEach(() => {
+ const dropdown = document.createElement('div');
+ // eslint-disable-next-line no-new
+ new NamespaceSelect({ dropdown });
+ glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0];
+ });
+
+ it('prevents click events', () => {
+ const dummyEvent = new Event('dummy');
+ spyOn(dummyEvent, 'preventDefault');
+
+ glDropdownOptions.clicked({ e: dummyEvent });
+
+ expect(dummyEvent.preventDefault).toHaveBeenCalled();
+ });
+ });
+
+ describe('as filter', () => {
+ let glDropdownOptions;
+
+ beforeEach(() => {
+ const dropdown = document.createElement('div');
+ dropdown.dataset.isFilter = 'true';
+ // eslint-disable-next-line no-new
+ new NamespaceSelect({ dropdown });
+ glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0];
+ });
+
+ it('does not prevent click events', () => {
+ const dummyEvent = new Event('dummy');
+ spyOn(dummyEvent, 'preventDefault');
+
+ glDropdownOptions.clicked({ e: dummyEvent });
+
+ expect(dummyEvent.preventDefault).not.toHaveBeenCalled();
+ });
+
+ it('sets URL of dropdown items', () => {
+ const dummyNamespace = { id: 'eal' };
+
+ const itemUrl = glDropdownOptions.url(dummyNamespace);
+
+ expect(itemUrl).toContain(`namespace_id=${dummyNamespace.id}`);
+ });
+ });
+});
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index c57f44dae17..50a5e4ff056 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,7 +1,6 @@
/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */
-/* global NewBranchForm */
-import '~/new_branch_form';
+import NewBranchForm from '~/new_branch_form';
(function() {
describe('Branch', function() {
diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js
index 3f659af5c3b..db75262b562 100644
--- a/spec/javascripts/notes/components/issue_comment_form_spec.js
+++ b/spec/javascripts/notes/components/issue_comment_form_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import autosize from 'vendor/autosize';
+import Autosize from 'autosize';
import store from '~/notes/stores';
import issueCommentForm from '~/notes/components/issue_comment_form.vue';
import { loggedOutIssueData, notesDataMock, userDataMock, issueDataMock } from '../mock_data';
@@ -55,6 +55,25 @@ describe('issue_comment_form component', () => {
expect(vm.toggleIssueState).toHaveBeenCalled();
});
+
+ it('should disable action button whilst submitting', (done) => {
+ const saveNotePromise = Promise.resolve();
+ vm.note = 'hello world';
+ spyOn(vm, 'saveNote').and.returnValue(saveNotePromise);
+ spyOn(vm, 'stopPolling');
+
+ const actionButton = vm.$el.querySelector('.js-action-button');
+
+ vm.handleSave();
+
+ Vue.nextTick()
+ .then(() => expect(actionButton.disabled).toBeTruthy())
+ .then(saveNotePromise)
+ .then(Vue.nextTick)
+ .then(() => expect(actionButton.disabled).toBeFalsy())
+ .then(done)
+ .catch(done.fail);
+ });
});
describe('textarea', () => {
@@ -97,14 +116,14 @@ describe('issue_comment_form component', () => {
});
it('should resize textarea after note discarded', (done) => {
- spyOn(autosize, 'update');
+ spyOn(Autosize, 'update');
spyOn(vm, 'discard').and.callThrough();
vm.note = 'foo';
vm.discard();
Vue.nextTick(() => {
- expect(autosize.update).toHaveBeenCalled();
+ expect(Autosize.update).toHaveBeenCalled();
done();
});
});
diff --git a/spec/javascripts/notes/components/issue_placeholder_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_note_spec.js
deleted file mode 100644
index 6e5275087f3..00000000000
--- a/spec/javascripts/notes/components/issue_placeholder_note_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-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
deleted file mode 100644
index d508a49f710..00000000000
--- a/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js
+++ /dev/null
@@ -1,24 +0,0 @@
-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
deleted file mode 100644
index c317ce32716..00000000000
--- a/spec/javascripts/notes/components/issue_system_note_spec.js
+++ /dev/null
@@ -1,53 +0,0 @@
-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_spec.js b/spec/javascripts/notes_spec.js
index 3e791a31604..677a389b88f 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,11 +1,10 @@
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
/* global Notes */
-import 'vendor/autosize';
+import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
import '~/render_gfm';
-import '~/render_math';
import '~/notes';
(function() {
@@ -39,7 +38,12 @@ import '~/notes';
loadFixtures(commentsTemplate);
gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads';
- $('body').data('page', 'projects:merge_requets:show');
+ $('body').attr('data-page', 'projects:merge_requets:show');
+ });
+
+ afterEach(() => {
+ // Undo what we did to the shared <body>
+ $('body').removeAttr('data-page');
});
describe('task lists', function() {
@@ -98,6 +102,16 @@ import '~/notes';
$('.js-comment-button').click();
expect(this.autoSizeSpy).toHaveBeenTriggered();
});
+
+ it('should not place escaped text in the comment box in case of error', function() {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+ $(textarea).text('A comment with `markup`.');
+
+ deferred.reject();
+ $('.js-comment-button').click();
+ expect($(textarea).val()).toEqual('A comment with `markup`.');
+ });
});
describe('updateNote', () => {
@@ -328,6 +342,7 @@ import '~/notes';
diff_discussion_html: false,
};
$form = jasmine.createSpyObj('$form', ['closest', 'find']);
+ $form.length = 1;
row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']);
notes = jasmine.createSpyObj('notes', [
@@ -356,13 +371,29 @@ import '~/notes';
$form.closest.and.returnValues(row, $form);
$form.find.and.returnValues(discussionContainer);
body.attr.and.returnValue('');
-
- Notes.prototype.renderDiscussionNote.call(notes, note, $form);
});
it('should call Notes.animateAppendNote', () => {
+ Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+
expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $('.main-notes-list'));
});
+
+ it('should append to row selected with line_code', () => {
+ $form.length = 0;
+ note.discussion_line_code = 'line_code';
+ note.diff_discussion_html = '<tr></tr>';
+
+ const line = document.createElement('div');
+ line.id = note.discussion_line_code;
+ document.body.appendChild(line);
+
+ $form.closest.and.returnValues($form);
+
+ Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+
+ expect(line.nextSibling.outerHTML).toEqual(note.diff_discussion_html);
+ });
});
describe('Discussion sub note', () => {
@@ -426,19 +457,17 @@ import '~/notes';
});
describe('putEditFormInPlace', () => {
- it('should call gl.GLForm with GFM parameter passed through', () => {
- spyOn(gl, 'GLForm');
-
- const $el = jasmine.createSpyObj('$form', ['find', 'closest']);
- $el.find.and.returnValue($('<div>'));
- $el.closest.and.returnValue($('<div>'));
+ it('should call GLForm with GFM parameter passed through', () => {
+ const notes = new Notes('', []);
+ const $el = $(`
+ <div>
+ <form></form>
+ </div>
+ `);
- Notes.prototype.putEditFormInPlace.call({
- getEditFormSelector: () => '',
- enableGFM: true
- }, $el);
+ notes.putEditFormInPlace($el);
- expect(gl.GLForm).toHaveBeenCalledWith(jasmine.any(Object), true);
+ expect(notes.glForm.enableGFM).toBeTruthy();
});
});
@@ -815,7 +844,7 @@ import '~/notes';
});
it('shows a flash message', () => {
- this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline);
+ this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
expect($('.flash-alert').is(':visible')).toBeTruthy();
});
@@ -828,7 +857,7 @@ import '~/notes';
});
it('hides visible flash message', () => {
- this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline);
+ this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
this.notes.clearFlash();
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index 85bd87318db..e8fcd4b1a36 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -11,7 +11,7 @@ describe('pipeline graph action component', () => {
tooltipText: 'bar',
link: 'foo',
actionMethod: 'post',
- actionIcon: 'icon_action_cancel',
+ actionIcon: 'cancel',
},
}).$mount();
diff --git a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
index 25fd18b197e..ba721bc53c6 100644
--- a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
@@ -11,7 +11,7 @@ describe('action component', () => {
tooltipText: 'bar',
link: 'foo',
actionMethod: 'post',
- actionIcon: 'icon_action_cancel',
+ actionIcon: 'cancel',
},
}).$mount();
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
index e90593e0f40..342ee6c1242 100644
--- a/spec/javascripts/pipelines/graph/job_component_spec.js
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -14,7 +14,7 @@ describe('pipeline graph job component', () => {
group: 'success',
details_path: '/root/ci-mock/builds/4256',
action: {
- icon: 'icon_action_retry',
+ icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/builds/4256/retry',
method: 'post',
diff --git a/spec/javascripts/pipelines/graph/mock_data.js b/spec/javascripts/pipelines/graph/mock_data.js
index 56c522b7f77..b9494f86d74 100644
--- a/spec/javascripts/pipelines/graph/mock_data.js
+++ b/spec/javascripts/pipelines/graph/mock_data.js
@@ -39,7 +39,7 @@ export default {
"details_path": "/root/ci-mock/builds/4153",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4153/retry",
"method": "post"
@@ -62,7 +62,7 @@ export default {
"details_path": "/root/ci-mock/builds/4153",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4153/retry",
"method": "post"
@@ -96,7 +96,7 @@ export default {
"details_path": "/root/ci-mock/builds/4166",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4166/retry",
"method": "post"
@@ -119,7 +119,7 @@ export default {
"details_path": "/root/ci-mock/builds/4166",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4166/retry",
"method": "post"
@@ -138,7 +138,7 @@ export default {
"details_path": "/root/ci-mock/builds/4159",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4159/retry",
"method": "post"
@@ -161,7 +161,7 @@ export default {
"details_path": "/root/ci-mock/builds/4159",
"favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
"action": {
- "icon": "icon_action_retry",
+ "icon": "retry",
"title": "Retry",
"path": "/root/ci-mock/builds/4159/retry",
"method": "post"
diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js
index aa4d6eedaf4..063ab53681b 100644
--- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js
+++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js
@@ -13,7 +13,7 @@ describe('stage column component', () => {
group: 'success',
details_path: '/root/ci-mock/builds/4256',
action: {
- icon: 'icon_action_retry',
+ icon: 'retry',
title: 'Retry',
path: '/root/ci-mock/builds/4256/retry',
method: 'post',
diff --git a/spec/javascripts/pipelines/navigation_tabs_spec.js b/spec/javascripts/pipelines/navigation_tabs_spec.js
deleted file mode 100644
index 53a88e6322f..00000000000
--- a/spec/javascripts/pipelines/navigation_tabs_spec.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import Vue from 'vue';
-import navigationTabs from '~/pipelines/components/navigation_tabs.vue';
-import mountComponent from '../helpers/vue_mount_component_helper';
-
-describe('navigation tabs pipeline component', () => {
- let vm;
- let Component;
- let data;
-
- beforeEach(() => {
- data = {
- scope: 'all',
- count: {
- all: 16,
- running: 1,
- pending: 10,
- finished: 0,
- },
- paths: {
- allPath: '/gitlab-org/gitlab-ce/pipelines',
- pendingPath: '/gitlab-org/gitlab-ce/pipelines?scope=pending',
- finishedPath: '/gitlab-org/gitlab-ce/pipelines?scope=finished',
- runningPath: '/gitlab-org/gitlab-ce/pipelines?scope=running',
- branchesPath: '/gitlab-org/gitlab-ce/pipelines?scope=branches',
- tagsPath: '/gitlab-org/gitlab-ce/pipelines?scope=tags',
- },
- };
-
- Component = Vue.extend(navigationTabs);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render tabs with correct paths', () => {
- vm = mountComponent(Component, data);
-
- // All
- const allTab = vm.$el.querySelector('.js-pipelines-tab-all a');
- expect(allTab.textContent.trim()).toContain('All');
- expect(allTab.getAttribute('href')).toEqual(data.paths.allPath);
-
- // Pending
- const pendingTab = vm.$el.querySelector('.js-pipelines-tab-pending a');
- expect(pendingTab.textContent.trim()).toContain('Pending');
- expect(pendingTab.getAttribute('href')).toEqual(data.paths.pendingPath);
-
- // Running
- const runningTab = vm.$el.querySelector('.js-pipelines-tab-running a');
- expect(runningTab.textContent.trim()).toContain('Running');
- expect(runningTab.getAttribute('href')).toEqual(data.paths.runningPath);
-
- // Finished
- const finishedTab = vm.$el.querySelector('.js-pipelines-tab-finished a');
- expect(finishedTab.textContent.trim()).toContain('Finished');
- expect(finishedTab.getAttribute('href')).toEqual(data.paths.finishedPath);
-
- // Branches
- const branchesTab = vm.$el.querySelector('.js-pipelines-tab-branches a');
- expect(branchesTab.textContent.trim()).toContain('Branches');
-
- // Tags
- const tagsTab = vm.$el.querySelector('.js-pipelines-tab-tags a');
- expect(tagsTab.textContent.trim()).toContain('Tags');
- });
-
- describe('scope', () => {
- it('should render scope provided as active tab', () => {
- vm = mountComponent(Component, data);
- expect(vm.$el.querySelector('.js-pipelines-tab-all').className).toContain('active');
- });
- });
-
- describe('badges', () => {
- it('should render provided number', () => {
- vm = mountComponent(Component, data);
- // All
- expect(
- vm.$el.querySelector('.js-totalbuilds-count').textContent.trim(),
- ).toContain(data.count.all);
-
- // Pending
- expect(
- vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim(),
- ).toContain(data.count.pending);
-
- // Running
- expect(
- vm.$el.querySelector('.js-pipelines-tab-running .badge').textContent.trim(),
- ).toContain(data.count.running);
-
- // Finished
- expect(
- vm.$el.querySelector('.js-pipelines-tab-finished .badge').textContent.trim(),
- ).toContain(data.count.finished);
- });
-
- it('should not render badge when number is undefined', () => {
- vm = mountComponent(Component, {
- scope: 'all',
- paths: {},
- count: {},
- });
-
- // All
- expect(
- vm.$el.querySelector('.js-totalbuilds-count'),
- ).toEqual(null);
-
- // Pending
- expect(
- vm.$el.querySelector('.js-pipelines-tab-pending .badge'),
- ).toEqual(null);
-
- // Running
- expect(
- vm.$el.querySelector('.js-pipelines-tab-running .badge'),
- ).toEqual(null);
-
- // Finished
- expect(
- vm.$el.querySelector('.js-pipelines-tab-finished .badge'),
- ).toEqual(null);
- });
- });
-});
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
index c30abb2edb0..367b42cefb0 100644
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ b/spec/javascripts/pipelines/pipelines_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import pipelinesComp from '~/pipelines/components/pipelines.vue';
import Store from '~/pipelines/stores/pipelines_store';
+import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
@@ -9,26 +10,33 @@ describe('Pipelines', () => {
preloadFixtures(jsonFixtureName);
let PipelinesComponent;
- let pipeline;
+ let pipelines;
+ let component;
beforeEach(() => {
loadFixtures('static/pipelines.html.raw');
- const pipelines = getJSONFixture(jsonFixtureName).pipelines;
- pipeline = pipelines.find(p => p.id === 1);
+ pipelines = getJSONFixture(jsonFixtureName);
PipelinesComponent = Vue.extend(pipelinesComp);
});
+ afterEach(() => {
+ component.$destroy();
+ });
+
describe('successfull request', () => {
describe('with pipelines', () => {
const pipelinesInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(pipeline), {
+ next(request.respondWith(JSON.stringify(pipelines), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(pipelinesInterceptor);
+ component = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ });
});
afterEach(() => {
@@ -38,18 +46,71 @@ describe('Pipelines', () => {
});
it('should render table', (done) => {
- const component = new PipelinesComponent({
- propsData: {
- store: new Store(),
- },
- }).$mount();
-
setTimeout(() => {
expect(component.$el.querySelector('.table-holder')).toBeDefined();
- expect(component.$el.querySelector('.realtime-loading')).toBe(null);
+ expect(
+ component.$el.querySelectorAll('.gl-responsive-table-row').length,
+ ).toEqual(pipelines.pipelines.length + 1);
+ done();
+ });
+ });
+
+ it('should render navigation tabs', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(),
+ ).toContain('Pending');
+ expect(
+ component.$el.querySelector('.js-pipelines-tab-all').textContent.trim(),
+ ).toContain('All');
+ expect(
+ component.$el.querySelector('.js-pipelines-tab-running').textContent.trim(),
+ ).toContain('Running');
+ expect(
+ component.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(),
+ ).toContain('Finished');
+ expect(
+ component.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(),
+ ).toContain('Branches');
+ expect(
+ component.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(),
+ ).toContain('Tags');
+ done();
+ });
+ });
+
+ it('should make an API request when using tabs', (done) => {
+ setTimeout(() => {
+ spyOn(component, 'updateContent');
+ component.$el.querySelector('.js-pipelines-tab-finished').click();
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
done();
});
});
+
+ describe('with pagination', () => {
+ it('should make an API request when using pagination', (done) => {
+ setTimeout(() => {
+ spyOn(component, 'updateContent');
+ // Mock pagination
+ component.store.state.pageInfo = {
+ page: 1,
+ total: 10,
+ perPage: 2,
+ nextPage: 2,
+ totalPages: 5,
+ };
+
+ Vue.nextTick(() => {
+ component.$el.querySelector('.js-next-button a').click();
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' });
+
+ done();
+ });
+ });
+ });
+ });
});
describe('without pipelines', () => {
@@ -70,15 +131,14 @@ describe('Pipelines', () => {
});
it('should render empty state', (done) => {
- const component = new PipelinesComponent({
+ component = new PipelinesComponent({
propsData: {
store: new Store(),
},
}).$mount();
setTimeout(() => {
- expect(component.$el.querySelector('.empty-state')).toBeDefined();
- expect(component.$el.querySelector('.realtime-loading')).toBe(null);
+ expect(component.$el.querySelector('.empty-state')).not.toBe(null);
done();
});
});
@@ -103,7 +163,7 @@ describe('Pipelines', () => {
});
it('should render error state', (done) => {
- const component = new PipelinesComponent({
+ component = new PipelinesComponent({
propsData: {
store: new Store(),
},
@@ -111,9 +171,54 @@ describe('Pipelines', () => {
setTimeout(() => {
expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
- expect(component.$el.querySelector('.realtime-loading')).toBe(null);
done();
});
});
});
+
+ describe('methods', () => {
+ beforeEach(() => {
+ spyOn(history, 'pushState').and.stub();
+ });
+
+ describe('updateContent', () => {
+ it('should set given parameters', () => {
+ component = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ });
+ component.updateContent({ scope: 'finished', page: '4' });
+
+ expect(component.page).toEqual('4');
+ expect(component.scope).toEqual('finished');
+ expect(component.requestData.scope).toEqual('finished');
+ expect(component.requestData.page).toEqual('4');
+ });
+ });
+
+ describe('onChangeTab', () => {
+ it('should set page to 1', () => {
+ component = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ });
+ spyOn(component, 'updateContent');
+
+ component.onChangeTab('running');
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' });
+ });
+ });
+
+ describe('onChangePage', () => {
+ it('should update page and keep scope', () => {
+ component = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ });
+ spyOn(component, 'updateContent');
+
+ component.onChangePage(4);
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' });
+ });
+ });
+ });
});
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index d7456a48bc1..a9126d2f4e9 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -10,6 +10,7 @@ describe('Pipelines Table Row', () => {
propsData: {
pipeline,
autoDevopsHelpPath: 'foo',
+ viewType: 'root',
},
}).$mount();
};
diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js
index 4f5eb42eb35..ca2f9163313 100644
--- a/spec/javascripts/pipelines/pipelines_table_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_spec.js
@@ -23,6 +23,7 @@ describe('Pipelines Table', () => {
propsData: {
pipelines: [],
autoDevopsHelpPath: 'foo',
+ viewType: 'root',
},
}).$mount();
});
@@ -49,6 +50,7 @@ describe('Pipelines Table', () => {
propsData: {
pipelines: [],
autoDevopsHelpPath: 'foo',
+ viewType: 'root',
},
}).$mount();
expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(0);
@@ -61,6 +63,7 @@ describe('Pipelines Table', () => {
propsData: {
pipelines: [pipeline],
autoDevopsHelpPath: 'foo',
+ viewType: 'root',
},
}).$mount();
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
index 171629fcd6b..edef150dd1e 100644
--- a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
+++ b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
@@ -50,6 +50,18 @@ describe('ProjectsListItemComponent', () => {
expect(vm.highlightedProjectName).toBe(mockProject.name);
});
});
+
+ describe('truncatedNamespace', () => {
+ it('should truncate project name from namespace string', () => {
+ vm.namespace = 'platform / nokia-3310';
+ expect(vm.truncatedNamespace).toBe('platform');
+ });
+
+ it('should truncate namespace string from the middle if it includes more than two groups in path', () => {
+ vm.namespace = 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310';
+ expect(vm.truncatedNamespace).toBe('platform / ... / Mobile Chipset');
+ });
+ });
});
describe('template', () => {
diff --git a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
index 2b3a821dbd9..b24567ffc0c 100644
--- a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
@@ -109,12 +109,16 @@ describe('PrometheusMetrics', () => {
it('should show loader animation while response is being loaded and hide it when request is complete', (done) => {
const deferred = $.Deferred();
- spyOn($, 'getJSON').and.returnValue(deferred.promise());
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
prometheusMetrics.loadActiveMetrics();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
- expect($.getJSON).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
+ expect($.ajax).toHaveBeenCalledWith({
+ url: prometheusMetrics.activeMetricsEndpoint,
+ dataType: 'json',
+ global: false,
+ });
deferred.resolve({ data: metrics, success: true });
@@ -126,7 +130,7 @@ describe('PrometheusMetrics', () => {
it('should show empty state if response failed to load', (done) => {
const deferred = $.Deferred();
- spyOn($, 'getJSON').and.returnValue(deferred.promise());
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
spyOn(prometheusMetrics, 'populateActiveMetrics');
prometheusMetrics.loadActiveMetrics();
@@ -142,7 +146,7 @@ describe('PrometheusMetrics', () => {
it('should populate metrics list once response is loaded', (done) => {
const deferred = $.Deferred();
- spyOn($, 'getJSON').and.returnValue(deferred.promise());
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
spyOn(prometheusMetrics, 'populateActiveMetrics');
prometheusMetrics.loadActiveMetrics();
diff --git a/spec/javascripts/registry/mock_data.js b/spec/javascripts/registry/mock_data.js
index 18600d00bff..6bffb47be55 100644
--- a/spec/javascripts/registry/mock_data.js
+++ b/spec/javascripts/registry/mock_data.js
@@ -26,7 +26,7 @@ export const registryServerResponse = [
name: 'centos7',
short_revision: 'b118ab5b0',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
- size: 679,
+ total_size: 679,
layers: 19,
location: 'location',
created_at: 1505828744434,
@@ -36,7 +36,7 @@ export const registryServerResponse = [
name: 'centos6',
short_revision: 'b118ab5b0',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
- size: 679,
+ total_size: 679,
layers: 19,
location: 'location',
created_at: 1505828744434,
@@ -70,7 +70,7 @@ export const parsedRegistryServerResponse = [
tag: registryServerResponse[0].name,
revision: registryServerResponse[0].revision,
shortRevision: registryServerResponse[0].short_revision,
- size: registryServerResponse[0].size,
+ size: registryServerResponse[0].total_size,
layers: registryServerResponse[0].layers,
location: registryServerResponse[0].location,
createdAt: registryServerResponse[0].created_at,
@@ -81,7 +81,7 @@ export const parsedRegistryServerResponse = [
tag: registryServerResponse[1].name,
revision: registryServerResponse[1].revision,
shortRevision: registryServerResponse[1].short_revision,
- size: registryServerResponse[1].size,
+ size: registryServerResponse[1].total_size,
layers: registryServerResponse[1].layers,
location: registryServerResponse[1].location,
createdAt: registryServerResponse[1].created_at,
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js
new file mode 100644
index 00000000000..f750061a6a1
--- /dev/null
+++ b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import listCollapsed from '~/repo/components/commit_sidebar/list_collapsed.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list collapsed', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(listCollapsed);
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.openFiles.push(file(), file());
+ vm.$store.state.openFiles[0].tempFile = true;
+ vm.$store.state.openFiles.forEach((f) => {
+ Object.assign(f, {
+ changed: true,
+ });
+ });
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders added & modified files count', () => {
+ expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1');
+ });
+});
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js
new file mode 100644
index 00000000000..18c9b46fcd9
--- /dev/null
+++ b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js
@@ -0,0 +1,53 @@
+import Vue from 'vue';
+import listItem from '~/repo/components/commit_sidebar/list_item.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list item', () => {
+ let vm;
+ let f;
+
+ beforeEach(() => {
+ const Component = Vue.extend(listItem);
+
+ f = file();
+
+ vm = mountComponent(Component, {
+ file: f,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders file path', () => {
+ expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path);
+ });
+
+ describe('computed', () => {
+ describe('iconName', () => {
+ it('returns modified when not a tempFile', () => {
+ expect(vm.iconName).toBe('file-modified');
+ });
+
+ it('returns addition when not a tempFile', () => {
+ f.tempFile = true;
+
+ expect(vm.iconName).toBe('file-addition');
+ });
+ });
+
+ describe('iconClass', () => {
+ it('returns modified when not a tempFile', () => {
+ expect(vm.iconClass).toContain('multi-file-modified');
+ });
+
+ it('returns addition when not a tempFile', () => {
+ f.tempFile = true;
+
+ expect(vm.iconClass).toContain('multi-file-addition');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_spec.js
new file mode 100644
index 00000000000..df7e3c5de21
--- /dev/null
+++ b/spec/javascripts/repo/components/commit_sidebar/list_spec.js
@@ -0,0 +1,72 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import commitSidebarList from '~/repo/components/commit_sidebar/list.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(commitSidebarList);
+
+ vm = createComponentWithStore(Component, store, {
+ title: 'Staged',
+ fileList: [],
+ collapsed: false,
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('empty file list', () => {
+ it('renders no changes text', () => {
+ expect(vm.$el.querySelector('.help-block').textContent.trim()).toBe('No changes');
+ });
+ });
+
+ describe('with a list of files', () => {
+ beforeEach((done) => {
+ const f = file('file name');
+ f.changed = true;
+ vm.fileList.push(f);
+
+ Vue.nextTick(done);
+ });
+
+ it('renders list', () => {
+ expect(vm.$el.querySelectorAll('li').length).toBe(1);
+ });
+ });
+
+ describe('collapsed', () => {
+ beforeEach((done) => {
+ vm.collapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('adds collapsed class', () => {
+ expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
+ });
+
+ it('hides list', () => {
+ expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
+ expect(vm.$el.querySelector('.help-block')).toBeNull();
+ });
+
+ it('hides collapse button', () => {
+ expect(vm.$el.querySelector('.multi-file-commit-panel-collapse-btn')).toBeNull();
+ });
+ });
+
+ it('clicking toggle collapse button emits toggle event', () => {
+ spyOn(vm, '$emit');
+
+ vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('toggleCollapsed');
+ });
+});
diff --git a/spec/javascripts/repo/components/new_branch_form_spec.js b/spec/javascripts/repo/components/new_branch_form_spec.js
new file mode 100644
index 00000000000..9a705a1f0ed
--- /dev/null
+++ b/spec/javascripts/repo/components/new_branch_form_spec.js
@@ -0,0 +1,114 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import newBranchForm from '~/repo/components/new_branch_form.vue';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore } from '../helpers';
+
+describe('Multi-file editor new branch form', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(newBranchForm);
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.currentBranch = 'master';
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('template', () => {
+ it('renders submit as disabled', () => {
+ expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBe('disabled');
+ });
+
+ it('enables the submit button when branch is not empty', (done) => {
+ vm.branchName = 'testing';
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBeNull();
+
+ done();
+ });
+ });
+
+ it('displays current branch creating from', (done) => {
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('p').textContent.replace(/\s+/g, ' ').trim()).toBe('Create from: master');
+
+ done();
+ });
+ });
+ });
+
+ describe('submitNewBranch', () => {
+ beforeEach(() => {
+ spyOn(vm, 'createNewBranch').and.returnValue(Promise.resolve());
+ });
+
+ it('sets to loading', () => {
+ vm.submitNewBranch();
+
+ expect(vm.loading).toBeTruthy();
+ });
+
+ it('hides current flash element', (done) => {
+ vm.$refs.flashContainer.innerHTML = '<div class="flash-alert"></div>';
+
+ vm.submitNewBranch();
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.flash-alert')).toBeNull();
+
+ done();
+ });
+ });
+
+ it('calls createdNewBranch with branchName', () => {
+ vm.branchName = 'testing';
+
+ vm.submitNewBranch();
+
+ expect(vm.createNewBranch).toHaveBeenCalledWith('testing');
+ });
+ });
+
+ describe('submitNewBranch with error', () => {
+ beforeEach(() => {
+ spyOn(vm, 'createNewBranch').and.returnValue(Promise.reject({
+ json: () => Promise.resolve({
+ message: 'error message',
+ }),
+ }));
+ });
+
+ it('sets loading to false', (done) => {
+ vm.loading = true;
+
+ vm.submitNewBranch();
+
+ setTimeout(() => {
+ expect(vm.loading).toBeFalsy();
+
+ done();
+ });
+ });
+
+ it('creates flash element', (done) => {
+ vm.submitNewBranch();
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.flash-alert')).not.toBeNull();
+ expect(vm.$el.querySelector('.flash-alert').textContent.trim()).toBe('error message');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js
new file mode 100644
index 00000000000..93b10fc1fee
--- /dev/null
+++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js
@@ -0,0 +1,71 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import newDropdown from '~/repo/components/new_dropdown/index.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
+
+describe('new dropdown component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const component = Vue.extend(newDropdown);
+
+ vm = createComponentWithStore(component, store);
+
+ vm.$store.state.path = '';
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders new file and new directory links', () => {
+ expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
+ expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('New directory');
+ });
+
+ describe('createNewItem', () => {
+ it('sets modalType to blob when new file is clicked', () => {
+ vm.$el.querySelectorAll('a')[0].click();
+
+ expect(vm.modalType).toBe('blob');
+ });
+
+ it('sets modalType to tree when new directory is clicked', () => {
+ vm.$el.querySelectorAll('a')[1].click();
+
+ expect(vm.modalType).toBe('tree');
+ });
+
+ it('opens modal when link is clicked', (done) => {
+ vm.$el.querySelectorAll('a')[0].click();
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.modal')).not.toBeNull();
+
+ done();
+ });
+ });
+ });
+
+ describe('toggleModalOpen', () => {
+ it('closes modal after toggling', (done) => {
+ vm.toggleModalOpen();
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.modal')).not.toBeNull();
+ })
+ .then(vm.toggleModalOpen)
+ .then(() => {
+ expect(vm.$el.querySelector('.modal')).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
new file mode 100644
index 00000000000..1ff7590ec79
--- /dev/null
+++ b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
@@ -0,0 +1,198 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import modal from '~/repo/components/new_dropdown/modal.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../../helpers';
+
+describe('new file modal component', () => {
+ const Component = Vue.extend(modal);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ ['tree', 'blob'].forEach((type) => {
+ describe(type, () => {
+ beforeEach(() => {
+ vm = createComponentWithStore(Component, store, {
+ type,
+ path: '',
+ }).$mount();
+
+ vm.entryName = 'testing';
+ });
+
+ it(`sets modal title as ${type}`, () => {
+ const title = type === 'tree' ? 'directory' : 'file';
+
+ expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`);
+ });
+
+ it(`sets button label as ${type}`, () => {
+ const title = type === 'tree' ? 'directory' : 'file';
+
+ expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`);
+ });
+
+ it(`sets form label as ${type}`, () => {
+ const title = type === 'tree' ? 'Directory' : 'File';
+
+ expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(`${title} name`);
+ });
+
+ describe('createEntryInStore', () => {
+ it('calls createTempEntry', () => {
+ spyOn(vm, 'createTempEntry');
+
+ vm.createEntryInStore();
+
+ expect(vm.createTempEntry).toHaveBeenCalledWith({
+ name: 'testing',
+ type,
+ });
+ });
+
+ it('sets editMode to true', (done) => {
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.editMode).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('toggles blob view', (done) => {
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.currentBlobView).toBe('repo-editor');
+
+ done();
+ });
+ });
+
+ it('opens newly created file', (done) => {
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.openFiles.length).toBe(1);
+ expect(vm.$store.state.openFiles[0].name).toBe(type === 'blob' ? 'testing' : '.gitkeep');
+
+ done();
+ });
+ });
+
+ it(`creates ${type} in the current stores path`, (done) => {
+ vm.$store.state.path = 'app';
+
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.tree[0].path).toBe('app/testing');
+ expect(vm.$store.state.tree[0].name).toBe('testing');
+
+ if (type === 'tree') {
+ expect(vm.$store.state.tree[0].tree.length).toBe(1);
+ }
+
+ done();
+ });
+ });
+
+ if (type === 'blob') {
+ it('creates new file', (done) => {
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('testing');
+ expect(vm.$store.state.tree[0].type).toBe('blob');
+ expect(vm.$store.state.tree[0].tempFile).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('does not create temp file when file already exists', (done) => {
+ vm.$store.state.tree.push(file('testing', '1', type));
+
+ vm.createEntryInStore();
+
+ setTimeout(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('testing');
+ expect(vm.$store.state.tree[0].type).toBe('blob');
+ expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
+
+ done();
+ });
+ });
+ } else {
+ it('creates new tree', () => {
+ vm.createEntryInStore();
+
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('testing');
+ expect(vm.$store.state.tree[0].type).toBe('tree');
+ expect(vm.$store.state.tree[0].tempFile).toBeTruthy();
+ expect(vm.$store.state.tree[0].tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].tree[0].name).toBe('.gitkeep');
+ });
+
+ it('creates multiple trees when entryName has slashes', () => {
+ vm.entryName = 'app/test';
+ vm.createEntryInStore();
+
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('app');
+ expect(vm.$store.state.tree[0].tree[0].name).toBe('test');
+ expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep');
+ });
+
+ it('creates tree in existing tree', () => {
+ vm.$store.state.tree.push(file('app', '1', 'tree'));
+
+ vm.entryName = 'app/test';
+ vm.createEntryInStore();
+
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('app');
+ expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
+ expect(vm.$store.state.tree[0].tree[0].tempFile).toBeTruthy();
+ expect(vm.$store.state.tree[0].tree[0].name).toBe('test');
+ expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep');
+ });
+
+ it('does not create new tree when already exists', () => {
+ vm.$store.state.tree.push(file('app', '1', 'tree'));
+
+ vm.entryName = 'app';
+ vm.createEntryInStore();
+
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe('app');
+ expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
+ expect(vm.$store.state.tree[0].tree.length).toBe(0);
+ });
+ }
+ });
+ });
+ });
+
+ it('focuses field on mount', () => {
+ document.body.innerHTML += '<div class="js-test"></div>';
+
+ vm = createComponentWithStore(Component, store, {
+ type: 'tree',
+ path: '',
+ }).$mount('.js-test');
+
+ expect(document.activeElement).toBe(vm.$refs.fieldName);
+
+ vm.$el.remove();
+ });
+});
diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
new file mode 100644
index 00000000000..bf7893029b1
--- /dev/null
+++ b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
@@ -0,0 +1,103 @@
+import Vue from 'vue';
+import upload from '~/repo/components/new_dropdown/upload.vue';
+import store from '~/repo/stores';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
+
+describe('new dropdown upload', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(upload);
+
+ vm = createComponentWithStore(Component, store, {
+ path: '',
+ });
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('readFile', () => {
+ beforeEach(() => {
+ spyOn(FileReader.prototype, 'readAsText');
+ spyOn(FileReader.prototype, 'readAsDataURL');
+ });
+
+ it('calls readAsText for text files', () => {
+ const file = {
+ type: 'text/html',
+ };
+
+ vm.readFile(file);
+
+ expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(file);
+ });
+
+ it('calls readAsDataURL for non-text files', () => {
+ const file = {
+ type: 'images/png',
+ };
+
+ vm.readFile(file);
+
+ expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file);
+ });
+ });
+
+ describe('createFile', () => {
+ const target = {
+ result: 'content',
+ };
+ const binaryTarget = {
+ result: 'base64,base64content',
+ };
+ const file = {
+ name: 'file',
+ };
+
+ it('creates new file', (done) => {
+ vm.createFile(target, file, true);
+
+ vm.$nextTick(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe(file.name);
+ expect(vm.$store.state.tree[0].content).toBe(target.result);
+
+ done();
+ });
+ });
+
+ it('creates new file in path', (done) => {
+ vm.$store.state.path = 'testing';
+ vm.createFile(target, file, true);
+
+ vm.$nextTick(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe(file.name);
+ expect(vm.$store.state.tree[0].content).toBe(target.result);
+ expect(vm.$store.state.tree[0].path).toBe(`testing/${file.name}`);
+
+ done();
+ });
+ });
+
+ it('splits content on base64 if binary', (done) => {
+ vm.createFile(binaryTarget, file, false);
+
+ vm.$nextTick(() => {
+ expect(vm.$store.state.tree.length).toBe(1);
+ expect(vm.$store.state.tree[0].name).toBe(file.name);
+ expect(vm.$store.state.tree[0].content).toBe(binaryTarget.result.split('base64,')[1]);
+ expect(vm.$store.state.tree[0].base64).toBe(true);
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js
index e604dcc152d..1c794123095 100644
--- a/spec/javascripts/repo/components/repo_commit_section_spec.js
+++ b/spec/javascripts/repo/components/repo_commit_section_spec.js
@@ -1,159 +1,114 @@
import Vue from 'vue';
+import store from '~/repo/stores';
+import service from '~/repo/services';
import repoCommitSection from '~/repo/components/repo_commit_section.vue';
-import RepoStore from '~/repo/stores/repo_store';
-import RepoService from '~/repo/services/repo_service';
+import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper';
+import { file, resetStore } from '../helpers';
describe('RepoCommitSection', () => {
- const branch = 'master';
- const projectUrl = 'projectUrl';
- const changedFiles = [{
- id: 0,
- changed: true,
- url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`,
- path: 'dir/file0.ext',
- newContent: 'a',
- }, {
- id: 1,
- changed: true,
- url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`,
- path: 'dir/file1.ext',
- newContent: 'b',
- }];
- const openedFiles = changedFiles.concat([{
- id: 2,
- url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`,
- path: 'dir/file2.ext',
- changed: false,
- }]);
-
- RepoStore.projectUrl = projectUrl;
-
- function createComponent(el) {
+ let vm;
+
+ function createComponent() {
const RepoCommitSection = Vue.extend(repoCommitSection);
- return new RepoCommitSection().$mount(el);
+ const comp = new RepoCommitSection({
+ store,
+ }).$mount();
+
+ comp.$store.state.currentBranch = 'master';
+ comp.$store.state.openFiles = [file(), file()];
+ comp.$store.state.openFiles.forEach(f => Object.assign(f, {
+ changed: true,
+ content: 'testing',
+ }));
+
+ return comp.$mount();
}
+ beforeEach((done) => {
+ vm = createComponent();
+
+ vm.collapsed = false;
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
it('renders a commit section', () => {
- RepoStore.isCommitable = true;
- RepoStore.currentBranch = branch;
- RepoStore.targetBranch = branch;
- RepoStore.openedFiles = openedFiles;
-
- const vm = createComponent();
- const changedFileElements = [...vm.$el.querySelectorAll('.changed-files > li')];
- const commitMessage = vm.$el.querySelector('#commit-message');
- const submitCommit = vm.$refs.submitCommit;
- const targetBranch = vm.$el.querySelector('.target-branch');
-
- expect(vm.$el.querySelector(':scope > form')).toBeTruthy();
- expect(vm.$el.querySelector('.staged-files').textContent.trim()).toEqual('Staged files (2)');
+ const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')];
+ const submitCommit = vm.$el.querySelector('form .btn');
+
+ expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
+ expect(vm.$el.querySelector('.multi-file-commit-panel-section header').textContent.trim()).toEqual('Staged');
expect(changedFileElements.length).toEqual(2);
changedFileElements.forEach((changedFile, i) => {
- expect(changedFile.textContent.trim()).toEqual(changedFiles[i].path);
+ expect(changedFile.textContent.trim()).toEqual(vm.$store.getters.changedFiles[i].path);
});
- expect(commitMessage.tagName).toEqual('TEXTAREA');
- expect(commitMessage.name).toEqual('commit-message');
- expect(submitCommit.type).toEqual('submit');
expect(submitCommit.disabled).toBeTruthy();
- expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeFalsy();
- expect(vm.$el.querySelector('.commit-summary').textContent.trim()).toEqual('Commit 2 files');
- expect(targetBranch.querySelector(':scope > label').textContent.trim()).toEqual('Target branch');
- expect(targetBranch.querySelector('.help-block').textContent.trim()).toEqual(branch);
+ expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull();
});
- it('does not render if not isCommitable', () => {
- RepoStore.isCommitable = false;
- RepoStore.openedFiles = [{
- id: 0,
- changed: true,
- }];
+ describe('when submitting', () => {
+ let changedFiles;
- const vm = createComponent();
+ beforeEach(() => {
+ vm.commitMessage = 'testing';
+ changedFiles = JSON.parse(JSON.stringify(vm.$store.getters.changedFiles));
- expect(vm.$el.innerHTML).toBeFalsy();
- });
+ spyOn(service, 'commit').and.returnValue(Promise.resolve({
+ short_id: '1',
+ stats: {},
+ }));
+ });
- it('does not render if no changedFiles', () => {
- RepoStore.isCommitable = true;
- RepoStore.openedFiles = [];
+ it('allows you to submit', () => {
+ expect(vm.$el.querySelector('form .btn').disabled).toBeTruthy();
+ });
- const vm = createComponent();
+ it('submits commit', (done) => {
+ vm.makeCommit();
+
+ // Wait for the branch check to finish
+ getSetTimeoutPromise()
+ .then(() => Vue.nextTick())
+ .then(() => {
+ const args = service.commit.calls.allArgs()[0];
+ const { commit_message, actions, branch: payloadBranch } = args[1];
+
+ expect(commit_message).toBe('testing');
+ expect(actions.length).toEqual(2);
+ expect(payloadBranch).toEqual('master');
+ expect(actions[0].action).toEqual('update');
+ expect(actions[1].action).toEqual('update');
+ expect(actions[0].content).toEqual(changedFiles[0].content);
+ expect(actions[1].content).toEqual(changedFiles[1].content);
+ expect(actions[0].file_path).toEqual(changedFiles[0].path);
+ expect(actions[1].file_path).toEqual(changedFiles[1].path);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
- expect(vm.$el.innerHTML).toBeFalsy();
- });
+ it('redirects to MR creation page if start new MR checkbox checked', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ vm.startNewMR = true;
- it('shows commit submit and summary if commitMessage and spinner if submitCommitsLoading', (done) => {
- const projectId = 'projectId';
- const commitMessage = 'commitMessage';
- RepoStore.isCommitable = true;
- RepoStore.currentBranch = branch;
- RepoStore.targetBranch = branch;
- RepoStore.openedFiles = openedFiles;
- RepoStore.projectId = projectId;
-
- // We need to append to body to get form `submit` events working
- // Otherwise we run into, "Form submission canceled because the form is not connected"
- // See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
- const el = document.createElement('div');
- document.body.appendChild(el);
-
- const vm = createComponent(el);
- const commitMessageEl = vm.$el.querySelector('#commit-message');
- const submitCommit = vm.$refs.submitCommit;
-
- vm.commitMessage = commitMessage;
-
- Vue.nextTick(() => {
- expect(commitMessageEl.value).toBe(commitMessage);
- expect(submitCommit.disabled).toBeFalsy();
-
- spyOn(vm, 'makeCommit').and.callThrough();
- spyOn(RepoService, 'commitFiles').and.callFake(() => Promise.resolve());
-
- submitCommit.click();
-
- Vue.nextTick(() => {
- expect(vm.makeCommit).toHaveBeenCalled();
- expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeTruthy();
-
- const args = RepoService.commitFiles.calls.allArgs()[0];
- const { commit_message, actions, branch: payloadBranch } = args[0];
-
- expect(commit_message).toBe(commitMessage);
- expect(actions.length).toEqual(2);
- expect(payloadBranch).toEqual(branch);
- expect(actions[0].action).toEqual('update');
- expect(actions[1].action).toEqual('update');
- expect(actions[0].content).toEqual(openedFiles[0].newContent);
- expect(actions[1].content).toEqual(openedFiles[1].newContent);
- expect(actions[0].file_path).toEqual(openedFiles[0].path);
- expect(actions[1].file_path).toEqual(openedFiles[1].path);
-
- done();
- });
- });
- });
+ vm.makeCommit();
- describe('methods', () => {
- describe('resetCommitState', () => {
- it('should reset store vars and scroll to top', () => {
- const vm = {
- submitCommitsLoading: true,
- changedFiles: new Array(10),
- commitMessage: 'commitMessage',
- editMode: true,
- };
-
- repoCommitSection.methods.resetCommitState.call(vm);
-
- expect(vm.submitCommitsLoading).toEqual(false);
- expect(vm.changedFiles).toEqual([]);
- expect(vm.commitMessage).toEqual('');
- expect(vm.editMode).toEqual(false);
- });
+ getSetTimeoutPromise()
+ .then(() => Vue.nextTick())
+ .then(() => {
+ expect(gl.utils.visitUrl).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
});
});
});
diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js
index 411514009dc..44018464b35 100644
--- a/spec/javascripts/repo/components/repo_edit_button_spec.js
+++ b/spec/javascripts/repo/components/repo_edit_button_spec.js
@@ -1,49 +1,83 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoEditButton from '~/repo/components/repo_edit_button.vue';
-import RepoStore from '~/repo/stores/repo_store';
+import { file, resetStore } from '../helpers';
describe('RepoEditButton', () => {
- function createComponent() {
+ let vm;
+
+ beforeEach(() => {
+ const f = file();
const RepoEditButton = Vue.extend(repoEditButton);
- return new RepoEditButton().$mount();
- }
+ vm = new RepoEditButton({
+ store,
+ });
- it('renders an edit button that toggles the view state', (done) => {
- RepoStore.isCommitable = true;
- RepoStore.changedFiles = [];
- RepoStore.binary = false;
- RepoStore.openedFiles = [{}, {}];
+ f.active = true;
+ vm.$store.dispatch('setInitialData', {
+ canCommit: true,
+ onTopOfBranch: true,
+ });
+ vm.$store.state.openFiles.push(f);
+ });
- const vm = createComponent();
+ afterEach(() => {
+ vm.$destroy();
- expect(vm.$el.tagName).toEqual('BUTTON');
- expect(vm.$el.textContent).toMatch('Edit');
+ resetStore(vm.$store);
+ });
- spyOn(vm, 'editCancelClicked').and.callThrough();
+ it('renders an edit button', () => {
+ vm.$mount();
- vm.$el.click();
+ expect(vm.$el.querySelector('.btn')).not.toBeNull();
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit');
+ });
+
+ it('renders edit button with cancel text', () => {
+ vm.$store.state.editMode = true;
+
+ vm.$mount();
+
+ expect(vm.$el.querySelector('.btn')).not.toBeNull();
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
+ });
+
+ it('toggles edit mode on click', (done) => {
+ vm.$mount();
+
+ vm.$el.querySelector('.btn').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
- Vue.nextTick(() => {
- expect(vm.editCancelClicked).toHaveBeenCalled();
- expect(vm.$el.textContent).toMatch('Cancel edit');
done();
});
});
- it('does not render if not isCommitable', () => {
- RepoStore.isCommitable = false;
+ describe('discardPopupOpen', () => {
+ beforeEach(() => {
+ vm.$store.state.discardPopupOpen = true;
+ vm.$store.state.editMode = true;
+ vm.$store.state.openFiles[0].changed = true;
- const vm = createComponent();
+ vm.$mount();
+ });
- expect(vm.$el.innerHTML).toBeUndefined();
- });
+ it('renders popup', () => {
+ expect(vm.$el.querySelector('.modal')).not.toBeNull();
+ });
+
+ it('removes all changed files', (done) => {
+ vm.$el.querySelector('.btn-warning').click();
- describe('methods', () => {
- describe('editCancelClicked', () => {
- it('sets dialog to open when there are changedFiles');
+ vm.$nextTick(() => {
+ expect(vm.$store.getters.changedFiles.length).toBe(0);
+ expect(vm.$el.querySelector('.modal')).toBeNull();
- it('toggles editMode and calls toggleBlobView');
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js
index 85d55d171f9..81158cad639 100644
--- a/spec/javascripts/repo/components/repo_editor_spec.js
+++ b/spec/javascripts/repo/components/repo_editor_spec.js
@@ -1,49 +1,60 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoEditor from '~/repo/components/repo_editor.vue';
+import monacoLoader from '~/repo/monaco_loader';
+import { file, resetStore } from '../helpers';
describe('RepoEditor', () => {
- beforeEach(() => {
+ let vm;
+
+ beforeEach((done) => {
+ const f = file();
const RepoEditor = Vue.extend(repoEditor);
- this.vm = new RepoEditor().$mount();
+ vm = new RepoEditor({
+ store,
+ });
+
+ f.active = true;
+ f.tempFile = true;
+ vm.$store.state.openFiles.push(f);
+ vm.$store.getters.activeFile.html = 'testing';
+ vm.monaco = true;
+
+ vm.$mount();
+
+ monacoLoader(['vs/editor/editor.main'], () => {
+ setTimeout(done, 0);
+ });
});
- it('renders an ide container', (done) => {
- this.vm.openedFiles = ['idiidid'];
- this.vm.binary = false;
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+ it('renders an ide container', (done) => {
Vue.nextTick(() => {
- expect(this.vm.shouldHideEditor).toBe(false);
- expect(this.vm.$el.id).toEqual('ide');
- expect(this.vm.$el.tagName).toBe('DIV');
+ expect(vm.shouldHideEditor).toBeFalsy();
+
done();
});
});
- describe('when there are no open files', () => {
- it('does not render the ide', (done) => {
- this.vm.openedFiles = [];
+ describe('when open file is binary and not raw', () => {
+ beforeEach((done) => {
+ vm.$store.getters.activeFile.binary = true;
+
+ Vue.nextTick(done);
+ });
- Vue.nextTick(() => {
- expect(this.vm.shouldHideEditor).toBe(true);
- expect(this.vm.$el.tagName).not.toBeDefined();
- done();
- });
+ it('does not render the IDE', () => {
+ expect(vm.shouldHideEditor).toBeTruthy();
});
- });
- describe('when open file is binary and not raw', () => {
- it('does not render the IDE', (done) => {
- this.vm.binary = true;
- this.vm.activeFile = {
- raw: false,
- };
-
- Vue.nextTick(() => {
- expect(this.vm.shouldHideEditor).toBe(true);
- expect(this.vm.$el.tagName).not.toBeDefined();
- done();
- });
+ it('shows activeFile html', () => {
+ expect(vm.$el.textContent).toContain('testing');
});
});
});
diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js
index dfab51710c3..d6e255e4810 100644
--- a/spec/javascripts/repo/components/repo_file_buttons_spec.js
+++ b/spec/javascripts/repo/components/repo_file_buttons_spec.js
@@ -1,75 +1,49 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoFileButtons from '~/repo/components/repo_file_buttons.vue';
-import RepoStore from '~/repo/stores/repo_store';
+import { file, resetStore } from '../helpers';
describe('RepoFileButtons', () => {
+ const activeFile = file();
+ let vm;
+
function createComponent() {
const RepoFileButtons = Vue.extend(repoFileButtons);
- return new RepoFileButtons().$mount();
- }
+ activeFile.rawPath = 'test';
+ activeFile.blamePath = 'test';
+ activeFile.commitsPath = 'test';
+ activeFile.active = true;
+ store.state.openFiles.push(activeFile);
- it('renders Raw, Blame, History, Permalink and Preview toggle', () => {
- const activeFile = {
- extension: 'md',
- url: 'url',
- raw_path: 'raw_path',
- blame_path: 'blame_path',
- commits_path: 'commits_path',
- permalink: 'permalink',
- };
- const activeFileLabel = 'activeFileLabel';
- RepoStore.openedFiles = new Array(1);
- RepoStore.activeFile = activeFile;
- RepoStore.activeFileLabel = activeFileLabel;
- RepoStore.editMode = true;
- RepoStore.binary = false;
+ return new RepoFileButtons({
+ store,
+ }).$mount();
+ }
- const vm = createComponent();
- const raw = vm.$el.querySelector('.raw');
- const blame = vm.$el.querySelector('.blame');
- const history = vm.$el.querySelector('.history');
+ afterEach(() => {
+ vm.$destroy();
- expect(vm.$el.id).toEqual('repo-file-buttons');
- expect(raw.href).toMatch(`/${activeFile.raw_path}`);
- expect(raw.textContent.trim()).toEqual('Raw');
- expect(blame.href).toMatch(`/${activeFile.blame_path}`);
- expect(blame.textContent.trim()).toEqual('Blame');
- expect(history.href).toMatch(`/${activeFile.commits_path}`);
- expect(history.textContent.trim()).toEqual('History');
- expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink');
- expect(vm.$el.querySelector('.preview').textContent.trim()).toEqual(activeFileLabel);
+ resetStore(vm.$store);
});
- it('triggers rawPreviewToggle on preview click', () => {
- const activeFile = {
- extension: 'md',
- url: 'url',
- };
- RepoStore.openedFiles = new Array(1);
- RepoStore.activeFile = activeFile;
- RepoStore.editMode = true;
-
- const vm = createComponent();
- const preview = vm.$el.querySelector('.preview');
-
- spyOn(vm, 'rawPreviewToggle');
-
- preview.click();
-
- expect(vm.rawPreviewToggle).toHaveBeenCalled();
- });
+ it('renders Raw, Blame, History, Permalink and Preview toggle', (done) => {
+ vm = createComponent();
- it('does not render preview toggle if not canPreview', () => {
- const activeFile = {
- extension: 'abcd',
- url: 'url',
- };
- RepoStore.openedFiles = new Array(1);
- RepoStore.activeFile = activeFile;
+ vm.$nextTick(() => {
+ const raw = vm.$el.querySelector('.raw');
+ const blame = vm.$el.querySelector('.blame');
+ const history = vm.$el.querySelector('.history');
- const vm = createComponent();
+ expect(raw.href).toMatch(`/${activeFile.rawPath}`);
+ expect(raw.textContent.trim()).toEqual('Raw');
+ expect(blame.href).toMatch(`/${activeFile.blamePath}`);
+ expect(blame.textContent.trim()).toEqual('Blame');
+ expect(history.href).toMatch(`/${activeFile.commitsPath}`);
+ expect(history.textContent.trim()).toEqual('History');
+ expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink');
- expect(vm.$el.querySelector('.preview')).toBeFalsy();
+ done();
+ });
});
});
diff --git a/spec/javascripts/repo/components/repo_file_options_spec.js b/spec/javascripts/repo/components/repo_file_options_spec.js
deleted file mode 100644
index 9759b4bf12d..00000000000
--- a/spec/javascripts/repo/components/repo_file_options_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import Vue from 'vue';
-import repoFileOptions from '~/repo/components/repo_file_options.vue';
-
-describe('RepoFileOptions', () => {
- const projectName = 'projectName';
-
- function createComponent(propsData) {
- const RepoFileOptions = Vue.extend(repoFileOptions);
-
- return new RepoFileOptions({
- propsData,
- }).$mount();
- }
-
- it('renders the title and new file/folder buttons if isMini is true', () => {
- const vm = createComponent({
- isMini: true,
- projectName,
- });
-
- expect(vm.$el.classList.contains('repo-file-options')).toBeTruthy();
- expect(vm.$el.querySelector('.title').textContent).toEqual(projectName);
- });
-
- it('does not render if isMini is false', () => {
- const vm = createComponent({
- isMini: false,
- projectName,
- });
-
- expect(vm.$el.innerHTML).toBeFalsy();
- });
-});
diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js
index 620b604f404..bf9181fb09c 100644
--- a/spec/javascripts/repo/components/repo_file_spec.js
+++ b/spec/javascripts/repo/components/repo_file_spec.js
@@ -1,40 +1,31 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoFile from '~/repo/components/repo_file.vue';
-import RepoStore from '~/repo/stores/repo_store';
+import { file, resetStore } from '../helpers';
describe('RepoFile', () => {
const updated = 'updated';
- const file = {
- icon: 'icon',
- url: 'url',
- name: 'name',
- lastCommitMessage: 'message',
- lastCommitUpdate: Date.now(),
- level: 10,
- };
- const activeFile = {
- pageTitle: 'pageTitle',
- url: 'url',
- };
- const otherFile = {
- html: '<p class="file-content">html</p>',
- pageTitle: 'otherpageTitle',
- };
+ let vm;
function createComponent(propsData) {
const RepoFile = Vue.extend(repoFile);
return new RepoFile({
+ store,
propsData,
}).$mount();
}
- it('renders link, icon, name and last commit details', () => {
+ afterEach(() => {
+ resetStore(vm.$store);
+ });
+
+ it('renders link, icon and name', () => {
const RepoFile = Vue.extend(repoFile);
- const vm = new RepoFile({
+ vm = new RepoFile({
+ store,
propsData: {
- file,
- activeFile,
+ file: file(),
},
});
spyOn(vm, 'timeFormated').and.returnValue(updated);
@@ -43,108 +34,81 @@ describe('RepoFile', () => {
const name = vm.$el.querySelector('.repo-file-name');
const fileIcon = vm.$el.querySelector('.file-icon');
- expect(vm.$el.classList.contains('active')).toBeTruthy();
- expect(vm.$el.querySelector(`.${file.icon}`).style.marginLeft).toEqual('100px');
- expect(name.title).toEqual(file.url);
- expect(name.href).toMatch(`/${file.url}`);
- expect(name.textContent.trim()).toEqual(file.name);
- expect(vm.$el.querySelector('.commit-message').textContent.trim()).toBe(file.lastCommitMessage);
- expect(vm.$el.querySelector('.commit-update').textContent.trim()).toBe(updated);
- expect(fileIcon.classList.contains(file.icon)).toBeTruthy();
- expect(fileIcon.style.marginLeft).toEqual(`${file.level * 10}px`);
+ expect(vm.$el.querySelector(`.${vm.file.icon}`).style.marginLeft).toEqual('0px');
+ expect(name.href).toMatch(`/${vm.file.url}`);
+ expect(name.textContent.trim()).toEqual(vm.file.name);
+ expect(fileIcon.classList.contains(vm.file.icon)).toBeTruthy();
+ expect(fileIcon.style.marginLeft).toEqual(`${vm.file.level * 10}px`);
+ expect(vm.$el.querySelectorAll('.animation-container').length).toBe(2);
});
it('does render if hasFiles is true and is loading tree', () => {
- const vm = createComponent({
- file,
- activeFile,
- loading: {
- tree: true,
- },
- hasFiles: true,
+ vm = createComponent({
+ file: file(),
});
- expect(vm.$el.innerHTML).toBeTruthy();
expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy();
});
- it('sets the document title correctly', () => {
- RepoStore.setActiveFiles(otherFile);
-
- expect(document.title.trim()).toEqual(otherFile.pageTitle);
- });
-
it('renders a spinner if the file is loading', () => {
- file.loading = true;
- const vm = createComponent({
- file,
- activeFile,
- loading: {
- tree: true,
- },
- hasFiles: true,
+ const f = file();
+ f.loading = true;
+ vm = createComponent({
+ file: f,
});
- expect(vm.$el.innerHTML).toBeTruthy();
- expect(vm.$el.querySelector('.fa-spin.fa-spinner').style.marginLeft).toEqual(`${file.level * 10}px`);
+ expect(vm.$el.querySelector('.fa-spin.fa-spinner')).not.toBeNull();
+ expect(vm.$el.querySelector('.fa-spin.fa-spinner').style.marginLeft).toEqual(`${vm.file.level * 16}px`);
});
- it('does not render if loading tree', () => {
- const vm = createComponent({
- file,
- activeFile,
- loading: {
- tree: true,
- },
+ it('does not render commit message and datetime if mini', (done) => {
+ vm = createComponent({
+ file: file(),
});
+ vm.$store.state.openFiles.push(vm.file);
- expect(vm.$el.innerHTML).toBeFalsy();
- });
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.commit-message')).toBeFalsy();
+ expect(vm.$el.querySelector('.commit-update')).toBeFalsy();
- it('does not render commit message and datetime if mini', () => {
- const vm = createComponent({
- file,
- activeFile,
- isMini: true,
+ done();
});
-
- expect(vm.$el.querySelector('.commit-message')).toBeFalsy();
- expect(vm.$el.querySelector('.commit-update')).toBeFalsy();
});
- it('does not set active class if file is active file', () => {
- const vm = createComponent({
- file,
- activeFile: {},
+ it('fires clickedTreeRow when the link is clicked', () => {
+ vm = createComponent({
+ file: file(),
});
- expect(vm.$el.classList.contains('active')).toBeFalsy();
- });
+ spyOn(vm, 'clickedTreeRow');
- it('fires linkClicked when the link is clicked', () => {
- const vm = createComponent({
- file,
- activeFile,
- });
+ vm.$el.click();
- spyOn(vm, 'linkClicked');
+ expect(vm.clickedTreeRow).toHaveBeenCalledWith(vm.file);
+ });
- vm.$el.querySelector('.repo-file-name').click();
+ describe('submodule', () => {
+ let f;
- expect(vm.linkClicked).toHaveBeenCalledWith(file);
- });
+ beforeEach(() => {
+ f = file('submodule name', '123456789');
+ f.type = 'submodule';
- describe('methods', () => {
- describe('linkClicked', () => {
- const vm = jasmine.createSpyObj('vm', ['$emit']);
+ vm = createComponent({
+ file: f,
+ });
+ });
- it('$emits linkclicked with file obj', () => {
- const theFile = {};
+ afterEach(() => {
+ vm.$destroy();
+ });
- repoFile.methods.linkClicked.call(vm, theFile);
+ it('renders submodule short ID', () => {
+ expect(vm.$el.querySelector('.commit-sha').textContent.trim()).toBe('12345678');
+ });
- expect(vm.$emit).toHaveBeenCalledWith('linkclicked', theFile);
- });
+ it('renders ID next to submodule name', () => {
+ expect(vm.$el.querySelector('td').textContent.replace(/\s+/g, ' ')).toContain('submodule name @ 12345678');
});
});
});
diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js
index a030314d749..031f2a9c0b2 100644
--- a/spec/javascripts/repo/components/repo_loading_file_spec.js
+++ b/spec/javascripts/repo/components/repo_loading_file_spec.js
@@ -1,12 +1,16 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoLoadingFile from '~/repo/components/repo_loading_file.vue';
+import { resetStore } from '../helpers';
describe('RepoLoadingFile', () => {
- function createComponent(propsData) {
+ let vm;
+
+ function createComponent() {
const RepoLoadingFile = Vue.extend(repoLoadingFile);
return new RepoLoadingFile({
- propsData,
+ store,
}).$mount();
}
@@ -28,52 +32,31 @@ describe('RepoLoadingFile', () => {
});
}
- it('renders 3 columns of animated LoC', () => {
- const vm = createComponent({
- loading: {
- tree: true,
- },
- hasFiles: false,
- });
- const columns = [...vm.$el.querySelectorAll('td')];
+ afterEach(() => {
+ vm.$destroy();
- expect(columns.length).toEqual(3);
- assertColumns(columns);
+ resetStore(vm.$store);
});
- it('renders 1 column of animated LoC if isMini', () => {
- const vm = createComponent({
- loading: {
- tree: true,
- },
- hasFiles: false,
- isMini: true,
- });
+ it('renders 3 columns of animated LoC', () => {
+ vm = createComponent();
const columns = [...vm.$el.querySelectorAll('td')];
- expect(columns.length).toEqual(1);
+ expect(columns.length).toEqual(3);
assertColumns(columns);
});
- it('does not render if tree is not loading', () => {
- const vm = createComponent({
- loading: {
- tree: false,
- },
- hasFiles: false,
- });
+ it('renders 1 column of animated LoC if isMini', (done) => {
+ vm = createComponent();
+ vm.$store.state.openFiles.push('test');
- expect(vm.$el.innerHTML).toBeFalsy();
- });
+ vm.$nextTick(() => {
+ const columns = [...vm.$el.querySelectorAll('td')];
- it('does not render if hasFiles is true', () => {
- const vm = createComponent({
- loading: {
- tree: true,
- },
- hasFiles: true,
- });
+ expect(columns.length).toEqual(1);
+ assertColumns(columns);
- expect(vm.$el.innerHTML).toBeFalsy();
+ done();
+ });
});
});
diff --git a/spec/javascripts/repo/components/repo_prev_directory_spec.js b/spec/javascripts/repo/components/repo_prev_directory_spec.js
index 34dde545e6a..7f82ae36a64 100644
--- a/spec/javascripts/repo/components/repo_prev_directory_spec.js
+++ b/spec/javascripts/repo/components/repo_prev_directory_spec.js
@@ -1,43 +1,45 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoPrevDirectory from '~/repo/components/repo_prev_directory.vue';
+import { resetStore } from '../helpers';
describe('RepoPrevDirectory', () => {
- function createComponent(propsData) {
+ let vm;
+ const parentLink = 'parent';
+ function createComponent() {
const RepoPrevDirectory = Vue.extend(repoPrevDirectory);
- return new RepoPrevDirectory({
- propsData,
- }).$mount();
- }
-
- it('renders a prev dir link', () => {
- const prevUrl = 'prevUrl';
- const vm = createComponent({
- prevUrl,
+ const comp = new RepoPrevDirectory({
+ store,
});
- const link = vm.$el.querySelector('a');
- spyOn(vm, 'linkClicked');
+ comp.$store.state.parentTreeUrl = parentLink;
+
+ return comp.$mount();
+ }
- expect(link.href).toMatch(`/${prevUrl}`);
- expect(link.textContent).toEqual('..');
+ beforeEach(() => {
+ vm = createComponent();
+ });
- link.click();
+ afterEach(() => {
+ vm.$destroy();
- expect(vm.linkClicked).toHaveBeenCalledWith(prevUrl);
+ resetStore(vm.$store);
});
- describe('methods', () => {
- describe('linkClicked', () => {
- const vm = jasmine.createSpyObj('vm', ['$emit']);
+ it('renders a prev dir link', () => {
+ const link = vm.$el.querySelector('a');
+
+ expect(link.href).toMatch(`/${parentLink}`);
+ expect(link.textContent).toEqual('...');
+ });
- it('$emits linkclicked with file obj', () => {
- const file = {};
+ it('clicking row triggers getTreeData', () => {
+ spyOn(vm, 'getTreeData');
- repoPrevDirectory.methods.linkClicked.call(vm, file);
+ vm.$el.querySelector('td').click();
- expect(vm.$emit).toHaveBeenCalledWith('linkclicked', file);
- });
- });
+ expect(vm.getTreeData).toHaveBeenCalledWith({ endpoint: parentLink });
});
});
diff --git a/spec/javascripts/repo/components/repo_preview_spec.js b/spec/javascripts/repo/components/repo_preview_spec.js
index 4920cf02083..8d1a87494cf 100644
--- a/spec/javascripts/repo/components/repo_preview_spec.js
+++ b/spec/javascripts/repo/components/repo_preview_spec.js
@@ -1,23 +1,37 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoPreview from '~/repo/components/repo_preview.vue';
-import RepoStore from '~/repo/stores/repo_store';
+import { file, resetStore } from '../helpers';
describe('RepoPreview', () => {
+ let vm;
+
function createComponent() {
+ const f = file();
const RepoPreview = Vue.extend(repoPreview);
- return new RepoPreview().$mount();
+ const comp = new RepoPreview({
+ store,
+ });
+
+ f.active = true;
+ f.html = 'test';
+
+ comp.$store.state.openFiles.push(f);
+
+ return comp.$mount();
}
- it('renders a div with the activeFile html', () => {
- const activeFile = {
- html: '<p class="file-content">html</p>',
- };
- RepoStore.activeFile = activeFile;
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
- const vm = createComponent();
+ it('renders a div with the activeFile html', () => {
+ vm = createComponent();
expect(vm.$el.tagName).toEqual('DIV');
- expect(vm.$el.innerHTML).toContain(activeFile.html);
+ expect(vm.$el.innerHTML).toContain('test');
});
});
diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js
index 35d2b37ac2a..df7cf8aabbb 100644
--- a/spec/javascripts/repo/components/repo_sidebar_spec.js
+++ b/spec/javascripts/repo/components/repo_sidebar_spec.js
@@ -1,169 +1,62 @@
import Vue from 'vue';
-import Helper from '~/repo/helpers/repo_helper';
-import RepoService from '~/repo/services/repo_service';
-import RepoStore from '~/repo/stores/repo_store';
+import store from '~/repo/stores';
import repoSidebar from '~/repo/components/repo_sidebar.vue';
+import { file, resetStore } from '../helpers';
describe('RepoSidebar', () => {
let vm;
- function createComponent() {
+ beforeEach(() => {
const RepoSidebar = Vue.extend(repoSidebar);
- return new RepoSidebar().$mount();
- }
+ vm = new RepoSidebar({
+ store,
+ });
+
+ vm.$store.state.isRoot = true;
+ vm.$store.state.tree.push(file());
+
+ vm.$mount();
+ });
afterEach(() => {
vm.$destroy();
+
+ resetStore(vm.$store);
});
it('renders a sidebar', () => {
- RepoStore.files = [{
- id: 0,
- }];
- RepoStore.openedFiles = [];
- RepoStore.isRoot = false;
-
- vm = createComponent();
const thead = vm.$el.querySelector('thead');
const tbody = vm.$el.querySelector('tbody');
- expect(vm.$el.id).toEqual('sidebar');
expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy();
- expect(thead.querySelector('.name').textContent).toEqual('Name');
- expect(thead.querySelector('.last-commit').textContent).toEqual('Last commit');
- expect(thead.querySelector('.last-update').textContent).toEqual('Last update');
+ expect(thead.querySelector('.name').textContent.trim()).toEqual('Name');
+ expect(thead.querySelector('.last-commit').textContent.trim()).toEqual('Last commit');
+ expect(thead.querySelector('.last-update').textContent.trim()).toEqual('Last update');
expect(tbody.querySelector('.repo-file-options')).toBeFalsy();
expect(tbody.querySelector('.prev-directory')).toBeFalsy();
expect(tbody.querySelector('.loading-file')).toBeFalsy();
expect(tbody.querySelector('.file')).toBeTruthy();
});
- it('does not render a thead, renders repo-file-options and sets sidebar-mini class if isMini', () => {
- RepoStore.openedFiles = [{
- id: 0,
- }];
- vm = createComponent();
-
- expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy();
- expect(vm.$el.querySelector('thead')).toBeFalsy();
- expect(vm.$el.querySelector('tbody .repo-file-options')).toBeTruthy();
- });
-
- it('renders 5 loading files if tree is loading and not hasFiles', () => {
- RepoStore.loading = {
- tree: true,
- };
- RepoStore.files = [];
- vm = createComponent();
-
- expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5);
- });
-
- it('renders a prev directory if isRoot', () => {
- RepoStore.files = [{
- id: 0,
- }];
- RepoStore.isRoot = true;
- vm = createComponent();
-
- expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
- });
-
- describe('methods', () => {
- describe('fileClicked', () => {
- it('should fetch data for new file', () => {
- spyOn(Helper, 'getContent').and.callThrough();
- const file1 = {
- id: 0,
- url: '',
- };
- RepoStore.files = [file1];
- RepoStore.isRoot = true;
- vm = createComponent();
+ it('renders 5 loading files if tree is loading', (done) => {
+ vm.$store.state.tree = [];
+ vm.$store.state.loading = true;
- vm.fileClicked(file1);
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5);
- expect(Helper.getContent).toHaveBeenCalledWith(file1);
- });
-
- it('should not fetch data for already opened files', () => {
- const file = {
- id: 42,
- url: 'foo',
- };
-
- spyOn(Helper, 'getFileFromPath').and.returnValue(file);
- spyOn(RepoStore, 'setActiveFiles');
- vm = createComponent();
- vm.fileClicked(file);
-
- expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(file);
- });
-
- it('should hide files in directory if already open', () => {
- spyOn(RepoStore, 'removeChildFilesOfTree').and.callThrough();
- const file1 = {
- id: 0,
- type: 'tree',
- url: '',
- opened: true,
- };
- RepoStore.files = [file1];
- RepoStore.isRoot = true;
- vm = createComponent();
-
- vm.fileClicked(file1);
-
- expect(RepoStore.removeChildFilesOfTree).toHaveBeenCalledWith(file1);
- });
+ done();
});
+ });
- describe('goToPreviousDirectoryClicked', () => {
- it('should hide files in directory if already open', () => {
- const prevUrl = 'foo/bar';
- vm = createComponent();
-
- vm.goToPreviousDirectoryClicked(prevUrl);
-
- expect(RepoService.url).toEqual(prevUrl);
- });
- });
-
- describe('back button', () => {
- const file1 = {
- id: 1,
- url: 'file1',
- };
- const file2 = {
- id: 2,
- url: 'file2',
- };
- RepoStore.files = [file1, file2];
- RepoStore.openedFiles = [file1, file2];
- RepoStore.isRoot = true;
-
- vm = createComponent();
- vm.fileClicked(file1);
-
- it('render previous file when using back button', () => {
- spyOn(Helper, 'getContent').and.callThrough();
-
- vm.fileClicked(file2);
- expect(Helper.getContent).toHaveBeenCalledWith(file2);
- Helper.getContent.calls.reset();
-
- history.pushState({
- key: Math.random(),
- }, '', file1.url);
- const popEvent = document.createEvent('Event');
- popEvent.initEvent('popstate', true, true);
- window.dispatchEvent(popEvent);
+ it('renders a prev directory if is not root', (done) => {
+ vm.$store.state.isRoot = false;
- expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(file1.url);
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
- window.history.pushState({}, null, '/');
- });
+ done();
});
});
});
diff --git a/spec/javascripts/repo/components/repo_spec.js b/spec/javascripts/repo/components/repo_spec.js
new file mode 100644
index 00000000000..b32d2c13af8
--- /dev/null
+++ b/spec/javascripts/repo/components/repo_spec.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import repo from '~/repo/components/repo.vue';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../helpers';
+
+describe('repo component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(repo);
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('does not render panel right when no files open', () => {
+ expect(vm.$el.querySelector('.panel-right')).toBeNull();
+ });
+
+ it('renders panel right when files are open', (done) => {
+ vm.$store.state.tree.push(file());
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.panel-right')).toBeNull();
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js
index d2a790ad73a..7d2174196c9 100644
--- a/spec/javascripts/repo/components/repo_tab_spec.js
+++ b/spec/javascripts/repo/components/repo_tab_spec.js
@@ -1,69 +1,107 @@
import Vue from 'vue';
+import store from '~/repo/stores';
import repoTab from '~/repo/components/repo_tab.vue';
+import { file, resetStore } from '../helpers';
describe('RepoTab', () => {
+ let vm;
+
function createComponent(propsData) {
const RepoTab = Vue.extend(repoTab);
return new RepoTab({
+ store,
propsData,
}).$mount();
}
+ afterEach(() => {
+ resetStore(vm.$store);
+ });
+
it('renders a close link and a name link', () => {
- const tab = {
- url: 'url',
- name: 'name',
- };
- const vm = createComponent({
- tab,
+ vm = createComponent({
+ tab: file(),
});
- const close = vm.$el.querySelector('.close');
- const name = vm.$el.querySelector(`a[title="${tab.url}"]`);
-
- spyOn(vm, 'closeTab');
- spyOn(vm, 'tabClicked');
+ vm.$store.state.openFiles.push(vm.tab);
+ const close = vm.$el.querySelector('.multi-file-tab-close');
+ const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`);
expect(close.querySelector('.fa-times')).toBeTruthy();
- expect(name.textContent.trim()).toEqual(tab.name);
+ expect(name.textContent.trim()).toEqual(vm.tab.name);
+ });
+
+ it('calls setFileActive when clicking tab', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ spyOn(vm, 'setFileActive');
- close.click();
- name.click();
+ vm.$el.click();
- expect(vm.closeTab).toHaveBeenCalledWith(tab);
- expect(vm.tabClicked).toHaveBeenCalledWith(tab);
+ expect(vm.setFileActive).toHaveBeenCalledWith(vm.tab);
+ });
+
+ it('calls closeFile when clicking close button', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ spyOn(vm, 'closeFile');
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ expect(vm.closeFile).toHaveBeenCalledWith({ file: vm.tab });
});
it('renders an fa-circle icon if tab is changed', () => {
- const tab = {
- url: 'url',
- name: 'name',
- changed: true,
- };
- const vm = createComponent({
+ const tab = file();
+ tab.changed = true;
+ vm = createComponent({
tab,
});
- expect(vm.$el.querySelector('.close .fa-circle')).toBeTruthy();
+ expect(vm.$el.querySelector('.multi-file-tab-close .fa-circle')).not.toBeNull();
});
describe('methods', () => {
describe('closeTab', () => {
- const vm = jasmine.createSpyObj('vm', ['$emit']);
+ it('does not close tab if is changed', (done) => {
+ const tab = file();
+ tab.changed = true;
+ tab.opened = true;
+ vm = createComponent({
+ tab,
+ });
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.dispatch('setFileActive', tab);
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
- it('returns undefined and does not $emit if file is changed', () => {
- const file = { changed: true };
- const returnVal = repoTab.methods.closeTab.call(vm, file);
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeTruthy();
- expect(returnVal).toBeUndefined();
- expect(vm.$emit).not.toHaveBeenCalled();
+ done();
+ });
});
- it('$emits tabclosed event with file obj', () => {
- const file = { changed: false };
- repoTab.methods.closeTab.call(vm, file);
+ it('closes tab when clicking close btn', (done) => {
+ const tab = file('lose');
+ tab.opened = true;
+ vm = createComponent({
+ tab,
+ });
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.dispatch('setFileActive', tab);
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeFalsy();
- expect(vm.$emit).toHaveBeenCalledWith('tabclosed', file);
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js
index a02b54efafc..1fb2242c051 100644
--- a/spec/javascripts/repo/components/repo_tabs_spec.js
+++ b/spec/javascripts/repo/components/repo_tabs_spec.js
@@ -1,45 +1,37 @@
import Vue from 'vue';
-import RepoStore from '~/repo/stores/repo_store';
+import store from '~/repo/stores';
import repoTabs from '~/repo/components/repo_tabs.vue';
+import { file, resetStore } from '../helpers';
describe('RepoTabs', () => {
- const openedFiles = [{
- id: 0,
- active: true,
- }, {
- id: 1,
- }];
+ const openedFiles = [file(), file()];
+ let vm;
function createComponent() {
const RepoTabs = Vue.extend(repoTabs);
- return new RepoTabs().$mount();
+ return new RepoTabs({
+ store,
+ }).$mount();
}
- it('renders a list of tabs', () => {
- RepoStore.openedFiles = openedFiles;
-
- const vm = createComponent();
- const tabs = [...vm.$el.querySelectorAll(':scope > li')];
-
- expect(vm.$el.id).toEqual('tabs');
- expect(tabs.length).toEqual(3);
- expect(tabs[0].classList.contains('active')).toBeTruthy();
- expect(tabs[1].classList.contains('active')).toBeFalsy();
- expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy();
+ afterEach(() => {
+ resetStore(vm.$store);
});
- describe('methods', () => {
- describe('tabClosed', () => {
- it('calls removeFromOpenedFiles with file obj', () => {
- const file = {};
+ it('renders a list of tabs', (done) => {
+ vm = createComponent();
+ openedFiles[0].active = true;
+ vm.$store.state.openFiles = openedFiles;
- spyOn(RepoStore, 'removeFromOpenedFiles');
+ vm.$nextTick(() => {
+ const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')];
- repoTabs.methods.tabClosed(file);
+ expect(tabs.length).toEqual(2);
+ expect(tabs[0].classList.contains('active')).toBeTruthy();
+ expect(tabs[1].classList.contains('active')).toBeFalsy();
- expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(file);
- });
+ done();
});
});
});
diff --git a/spec/javascripts/repo/helpers.js b/spec/javascripts/repo/helpers.js
new file mode 100644
index 00000000000..820a44992b4
--- /dev/null
+++ b/spec/javascripts/repo/helpers.js
@@ -0,0 +1,15 @@
+import { decorateData } from '~/repo/stores/utils';
+import state from '~/repo/stores/state';
+
+export const resetStore = (store) => {
+ store.replaceState(state());
+};
+
+export const file = (name = 'name', id = name, type = '') => decorateData({
+ id,
+ type,
+ icon: 'icon',
+ url: 'url',
+ name,
+ path: name,
+});
diff --git a/spec/javascripts/repo/lib/common/disposable_spec.js b/spec/javascripts/repo/lib/common/disposable_spec.js
new file mode 100644
index 00000000000..62c3913bf4d
--- /dev/null
+++ b/spec/javascripts/repo/lib/common/disposable_spec.js
@@ -0,0 +1,44 @@
+import Disposable from '~/repo/lib/common/disposable';
+
+describe('Multi-file editor library disposable class', () => {
+ let instance;
+ let disposableClass;
+
+ beforeEach(() => {
+ instance = new Disposable();
+
+ disposableClass = {
+ dispose: jasmine.createSpy('dispose'),
+ };
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ describe('add', () => {
+ it('adds disposable classes', () => {
+ instance.add(disposableClass);
+
+ expect(instance.disposers.size).toBe(1);
+ });
+ });
+
+ describe('dispose', () => {
+ beforeEach(() => {
+ instance.add(disposableClass);
+ });
+
+ it('calls dispose on all cached disposers', () => {
+ instance.dispose();
+
+ expect(disposableClass.dispose).toHaveBeenCalled();
+ });
+
+ it('clears cached disposers', () => {
+ instance.dispose();
+
+ expect(instance.disposers.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/common/model_manager_spec.js b/spec/javascripts/repo/lib/common/model_manager_spec.js
new file mode 100644
index 00000000000..8c134f178c0
--- /dev/null
+++ b/spec/javascripts/repo/lib/common/model_manager_spec.js
@@ -0,0 +1,81 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import ModelManager from '~/repo/lib/common/model_manager';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model manager', () => {
+ let instance;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ instance = new ModelManager(monaco);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ describe('addModel', () => {
+ it('caches model', () => {
+ instance.addModel(file());
+
+ expect(instance.models.size).toBe(1);
+ });
+
+ it('caches model by file path', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.models.keys().next().value).toBe('path-name');
+ });
+
+ it('adds model into disposable', () => {
+ spyOn(instance.disposable, 'add').and.callThrough();
+
+ instance.addModel(file());
+
+ expect(instance.disposable.add).toHaveBeenCalled();
+ });
+
+ it('returns cached model', () => {
+ spyOn(instance.models, 'get').and.callThrough();
+
+ instance.addModel(file());
+ instance.addModel(file());
+
+ expect(instance.models.get).toHaveBeenCalled();
+ });
+ });
+
+ describe('hasCachedModel', () => {
+ it('returns false when no models exist', () => {
+ expect(instance.hasCachedModel('path')).toBeFalsy();
+ });
+
+ it('returns true when model exists', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.hasCachedModel('path-name')).toBeTruthy();
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached models', () => {
+ instance.addModel(file());
+
+ instance.dispose();
+
+ expect(instance.models.size).toBe(0);
+ });
+
+ it('calls disposable dispose', () => {
+ spyOn(instance.disposable, 'dispose').and.callThrough();
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/common/model_spec.js b/spec/javascripts/repo/lib/common/model_spec.js
new file mode 100644
index 00000000000..d41ade237ca
--- /dev/null
+++ b/spec/javascripts/repo/lib/common/model_spec.js
@@ -0,0 +1,84 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import Model from '~/repo/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model', () => {
+ let model;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ model = new Model(monaco, file('path'));
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ model.dispose();
+ });
+
+ it('creates original model & new model', () => {
+ expect(model.originalModel).not.toBeNull();
+ expect(model.model).not.toBeNull();
+ });
+
+ describe('path', () => {
+ it('returns file path', () => {
+ expect(model.path).toBe('path');
+ });
+ });
+
+ describe('getModel', () => {
+ it('returns model', () => {
+ expect(model.getModel()).toBe(model.model);
+ });
+ });
+
+ describe('getOriginalModel', () => {
+ it('returns original model', () => {
+ expect(model.getOriginalModel()).toBe(model.originalModel);
+ });
+ });
+
+ describe('onChange', () => {
+ it('caches event by path', () => {
+ model.onChange(() => {});
+
+ expect(model.events.size).toBe(1);
+ expect(model.events.keys().next().value).toBe('path');
+ });
+
+ it('calls callback on change', (done) => {
+ const spy = jasmine.createSpy();
+ model.onChange(spy);
+
+ model.getModel().setValue('123');
+
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalledWith(model.getModel(), jasmine.anything());
+ done();
+ });
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ spyOn(model.disposable, 'dispose').and.callThrough();
+
+ model.dispose();
+
+ expect(model.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('clears events', () => {
+ model.onChange(() => {});
+
+ expect(model.events.size).toBe(1);
+
+ model.dispose();
+
+ expect(model.events.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/decorations/controller_spec.js b/spec/javascripts/repo/lib/decorations/controller_spec.js
new file mode 100644
index 00000000000..2e32e8fa0bd
--- /dev/null
+++ b/spec/javascripts/repo/lib/decorations/controller_spec.js
@@ -0,0 +1,120 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import editor from '~/repo/lib/editor';
+import DecorationsController from '~/repo/lib/decorations/controller';
+import Model from '~/repo/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library decorations controller', () => {
+ let editorInstance;
+ let controller;
+ let model;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ editorInstance = editor.create(monaco);
+ editorInstance.createInstance(document.createElement('div'));
+
+ controller = new DecorationsController(editorInstance);
+ model = new Model(monaco, file('path'));
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ model.dispose();
+ editorInstance.dispose();
+ controller.dispose();
+ });
+
+ describe('getAllDecorationsForModel', () => {
+ it('returns empty array when no decorations exist for model', () => {
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations).toEqual([]);
+ });
+
+ it('returns decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations[0]).toEqual({ decoration: 'decorationValue' });
+ });
+ });
+
+ describe('addDecorations', () => {
+ it('caches decorations in a new map', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('does not create new cache model', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.decorations.size).toBe(1);
+ expect(controller.decorations.keys().next().value).toBe('path');
+ });
+
+ it('calls decorate method', () => {
+ spyOn(controller, 'decorate');
+
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.decorate).toHaveBeenCalled();
+ });
+ });
+
+ describe('decorate', () => {
+ it('sets decorations on editor instance', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations');
+
+ controller.decorate(model);
+
+ expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []);
+ });
+
+ it('caches decorations', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.keys().next().value).toBe('path');
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached decorations', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ controller.dispose();
+
+ expect(controller.decorations.size).toBe(0);
+ });
+
+ it('clears cached editorDecorations', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ controller.dispose();
+
+ expect(controller.editorDecorations.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/diff/controller_spec.js b/spec/javascripts/repo/lib/diff/controller_spec.js
new file mode 100644
index 00000000000..ed62e28d3a3
--- /dev/null
+++ b/spec/javascripts/repo/lib/diff/controller_spec.js
@@ -0,0 +1,176 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import editor from '~/repo/lib/editor';
+import ModelManager from '~/repo/lib/common/model_manager';
+import DecorationsController from '~/repo/lib/decorations/controller';
+import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/repo/lib/diff/controller';
+import { computeDiff } from '~/repo/lib/diff/diff';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library dirty diff controller', () => {
+ let editorInstance;
+ let controller;
+ let modelManager;
+ let decorationsController;
+ let model;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ editorInstance = editor.create(monaco);
+ editorInstance.createInstance(document.createElement('div'));
+
+ modelManager = new ModelManager(monaco);
+ decorationsController = new DecorationsController(editorInstance);
+
+ model = modelManager.addModel(file());
+
+ controller = new DirtyDiffController(modelManager, decorationsController);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ controller.dispose();
+ model.dispose();
+ decorationsController.dispose();
+ editorInstance.dispose();
+ });
+
+ describe('getDiffChangeType', () => {
+ ['added', 'removed', 'modified'].forEach((type) => {
+ it(`returns ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(getDiffChangeType(change)).toBe(type);
+ });
+ });
+ });
+
+ describe('getDecorator', () => {
+ ['added', 'removed', 'modified'].forEach((type) => {
+ it(`returns with linesDecorationsClassName for ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(
+ getDecorator(change).options.linesDecorationsClassName,
+ ).toBe(`dirty-diff dirty-diff-${type}`);
+ });
+
+ it('returns with line numbers', () => {
+ const change = {
+ lineNumber: 1,
+ endLineNumber: 2,
+ [type]: true,
+ };
+
+ const range = getDecorator(change).range;
+
+ expect(range.startLineNumber).toBe(1);
+ expect(range.endLineNumber).toBe(2);
+ expect(range.startColumn).toBe(1);
+ expect(range.endColumn).toBe(1);
+ });
+ });
+ });
+
+ describe('attachModel', () => {
+ it('adds change event callback', () => {
+ spyOn(model, 'onChange');
+
+ controller.attachModel(model);
+
+ expect(model.onChange).toHaveBeenCalled();
+ });
+
+ it('calls throttledComputeDiff on change', () => {
+ spyOn(controller, 'throttledComputeDiff');
+
+ controller.attachModel(model);
+
+ model.getModel().setValue('123');
+
+ expect(controller.throttledComputeDiff).toHaveBeenCalled();
+ });
+ });
+
+ describe('computeDiff', () => {
+ it('posts to worker', () => {
+ spyOn(controller.dirtyDiffWorker, 'postMessage');
+
+ controller.computeDiff(model);
+
+ expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({
+ path: model.path,
+ originalContent: '',
+ newContent: '',
+ });
+ });
+ });
+
+ describe('reDecorate', () => {
+ it('calls decorations controller decorate', () => {
+ spyOn(controller.decorationsController, 'decorate');
+
+ controller.reDecorate(model);
+
+ expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model);
+ });
+ });
+
+ describe('decorate', () => {
+ it('adds decorations into decorations controller', () => {
+ spyOn(controller.decorationsController, 'addDecorations');
+
+ controller.decorate({ data: { changes: [], path: 'path' } });
+
+ expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith('path', 'dirtyDiff', jasmine.anything());
+ });
+
+ it('adds decorations into editor', () => {
+ const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations');
+
+ controller.decorate({ data: { changes: computeDiff('123', '1234'), path: 'path' } });
+
+ expect(spy).toHaveBeenCalledWith([], [{
+ range: new monaco.Range(
+ 1, 1, 1, 1,
+ ),
+ options: {
+ isWholeLine: true,
+ linesDecorationsClassName: 'dirty-diff dirty-diff-modified',
+ },
+ }]);
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ spyOn(controller.disposable, 'dispose').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('terminates worker', () => {
+ spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled();
+ });
+
+ it('removes worker event listener', () => {
+ spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith('message', jasmine.anything());
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/diff/diff_spec.js b/spec/javascripts/repo/lib/diff/diff_spec.js
new file mode 100644
index 00000000000..3269ec5d2c9
--- /dev/null
+++ b/spec/javascripts/repo/lib/diff/diff_spec.js
@@ -0,0 +1,80 @@
+import { computeDiff } from '~/repo/lib/diff/diff';
+
+describe('Multi-file editor library diff calculator', () => {
+ describe('computeDiff', () => {
+ it('returns empty array if no changes', () => {
+ const diff = computeDiff('123', '123');
+
+ expect(diff).toEqual([]);
+ });
+
+ describe('modified', () => {
+ it('', () => {
+ const diff = computeDiff('123', '1234')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(2);
+ });
+ });
+
+ describe('added', () => {
+ it('', () => {
+ const diff = computeDiff('123', '123\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(3);
+ });
+ });
+
+ describe('removed', () => {
+ it('', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeTruthy();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n123')[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeTruthy();
+ expect(diff.lineNumber).toBe(2);
+ });
+ });
+
+ it('includes line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.lineNumber).toBe(1);
+ });
+
+ it('includes end line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.endLineNumber).toBe(1);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/editor_options_spec.js b/spec/javascripts/repo/lib/editor_options_spec.js
new file mode 100644
index 00000000000..b4887d063ed
--- /dev/null
+++ b/spec/javascripts/repo/lib/editor_options_spec.js
@@ -0,0 +1,7 @@
+import editorOptions from '~/repo/lib/editor_options';
+
+describe('Multi-file editor library editor options', () => {
+ it('returns an array', () => {
+ expect(editorOptions).toEqual(jasmine.any(Array));
+ });
+});
diff --git a/spec/javascripts/repo/lib/editor_spec.js b/spec/javascripts/repo/lib/editor_spec.js
new file mode 100644
index 00000000000..cd32832a232
--- /dev/null
+++ b/spec/javascripts/repo/lib/editor_spec.js
@@ -0,0 +1,128 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import editor from '~/repo/lib/editor';
+import { file } from '../helpers';
+
+describe('Multi-file editor library', () => {
+ let instance;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ instance = editor.create(monaco);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ it('creates instance of editor', () => {
+ expect(editor.editorInstance).not.toBeNull();
+ });
+
+ describe('createInstance', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ });
+
+ it('creates editor instance', () => {
+ spyOn(instance.monaco.editor, 'create').and.callThrough();
+
+ instance.createInstance(el);
+
+ expect(instance.monaco.editor.create).toHaveBeenCalled();
+ });
+
+ it('creates dirty diff controller', () => {
+ instance.createInstance(el);
+
+ expect(instance.dirtyDiffController).not.toBeNull();
+ });
+ });
+
+ describe('createModel', () => {
+ it('calls model manager addModel', () => {
+ spyOn(instance.modelManager, 'addModel');
+
+ instance.createModel('FILE');
+
+ expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE');
+ });
+ });
+
+ describe('attachModel', () => {
+ let model;
+
+ beforeEach(() => {
+ instance.createInstance(document.createElement('div'));
+
+ model = instance.createModel(file());
+ });
+
+ it('sets the current model on the instance', () => {
+ instance.attachModel(model);
+
+ expect(instance.currentModel).toBe(model);
+ });
+
+ it('attaches the model to the current instance', () => {
+ spyOn(instance.instance, 'setModel');
+
+ instance.attachModel(model);
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel());
+ });
+
+ it('attaches the model to the dirty diff controller', () => {
+ spyOn(instance.dirtyDiffController, 'attachModel');
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model);
+ });
+
+ it('re-decorates with the dirty diff controller', () => {
+ spyOn(instance.dirtyDiffController, 'reDecorate');
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model);
+ });
+ });
+
+ describe('clearEditor', () => {
+ it('resets the editor model', () => {
+ instance.createInstance(document.createElement('div'));
+
+ spyOn(instance.instance, 'setModel');
+
+ instance.clearEditor();
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(null);
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposble dispose method', () => {
+ spyOn(instance.disposable, 'dispose').and.callThrough();
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('resets instance', () => {
+ instance.createInstance(document.createElement('div'));
+
+ expect(instance.instance).not.toBeNull();
+
+ instance.dispose();
+
+ expect(instance.instance).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/repo/services/repo_service_spec.js b/spec/javascripts/repo/services/repo_service_spec.js
deleted file mode 100644
index 6f530770525..00000000000
--- a/spec/javascripts/repo/services/repo_service_spec.js
+++ /dev/null
@@ -1,171 +0,0 @@
-import axios from 'axios';
-import RepoService from '~/repo/services/repo_service';
-import RepoStore from '~/repo/stores/repo_store';
-import Api from '~/api';
-
-describe('RepoService', () => {
- it('has default json format param', () => {
- expect(RepoService.options.params.format).toBe('json');
- });
-
- describe('buildParams', () => {
- let newParams;
- const url = 'url';
-
- beforeEach(() => {
- newParams = {};
-
- spyOn(Object, 'assign').and.returnValue(newParams);
- });
-
- it('clones params', () => {
- const params = RepoService.buildParams(url);
-
- expect(Object.assign).toHaveBeenCalledWith({}, RepoService.options.params);
-
- expect(params).toBe(newParams);
- });
-
- it('sets and returns viewer params to richif urlIsRichBlob is true', () => {
- spyOn(RepoService, 'urlIsRichBlob').and.returnValue(true);
-
- const params = RepoService.buildParams(url);
-
- expect(params.viewer).toEqual('rich');
- });
-
- it('returns params urlIsRichBlob is false', () => {
- spyOn(RepoService, 'urlIsRichBlob').and.returnValue(false);
-
- const params = RepoService.buildParams(url);
-
- expect(params.viewer).toBeUndefined();
- });
-
- it('calls urlIsRichBlob with the objects url prop if no url arg is provided', () => {
- spyOn(RepoService, 'urlIsRichBlob');
- RepoService.url = url;
-
- RepoService.buildParams();
-
- expect(RepoService.urlIsRichBlob).toHaveBeenCalledWith(url);
- });
- });
-
- describe('urlIsRichBlob', () => {
- it('returns true for md extension', () => {
- const isRichBlob = RepoService.urlIsRichBlob('url.md');
-
- expect(isRichBlob).toBeTruthy();
- });
-
- it('returns false for js extension', () => {
- const isRichBlob = RepoService.urlIsRichBlob('url.js');
-
- expect(isRichBlob).toBeFalsy();
- });
- });
-
- describe('getContent', () => {
- const params = {};
- const url = 'url';
- const requestPromise = Promise.resolve();
-
- beforeEach(() => {
- spyOn(RepoService, 'buildParams').and.returnValue(params);
- spyOn(axios, 'get').and.returnValue(requestPromise);
- });
-
- it('calls buildParams and axios.get', () => {
- const request = RepoService.getContent(url);
-
- expect(RepoService.buildParams).toHaveBeenCalledWith(url);
- expect(axios.get).toHaveBeenCalledWith(url, {
- params,
- });
- expect(request).toBe(requestPromise);
- });
-
- it('uses object url prop if no url arg is provided', () => {
- RepoService.url = url;
-
- RepoService.getContent();
-
- expect(axios.get).toHaveBeenCalledWith(url, {
- params,
- });
- });
- });
-
- describe('getBase64Content', () => {
- const url = 'url';
- const response = { data: 'data' };
-
- beforeEach(() => {
- spyOn(RepoService, 'bufferToBase64');
- spyOn(axios, 'get').and.returnValue(Promise.resolve(response));
- });
-
- it('calls axios.get and bufferToBase64 on completion', (done) => {
- const request = RepoService.getBase64Content(url);
-
- expect(axios.get).toHaveBeenCalledWith(url, {
- responseType: 'arraybuffer',
- });
- expect(request).toEqual(jasmine.any(Promise));
-
- request.then(() => {
- expect(RepoService.bufferToBase64).toHaveBeenCalledWith(response.data);
- done();
- }).catch(done.fail);
- });
- });
-
- describe('commitFiles', () => {
- it('calls commitMultiple and .then commitFlash', (done) => {
- const projectId = 'projectId';
- const payload = {};
- RepoStore.projectId = projectId;
-
- spyOn(Api, 'commitMultiple').and.returnValue(Promise.resolve());
- spyOn(RepoService, 'commitFlash');
-
- const apiPromise = RepoService.commitFiles(payload);
-
- expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, payload);
-
- apiPromise.then(() => {
- expect(RepoService.commitFlash).toHaveBeenCalled();
- done();
- }).catch(done.fail);
- });
- });
-
- describe('commitFlash', () => {
- it('calls Flash with data.message', () => {
- const data = {
- message: 'message',
- };
- spyOn(window, 'Flash');
-
- RepoService.commitFlash(data);
-
- expect(window.Flash).toHaveBeenCalledWith(data.message);
- });
-
- it('calls Flash with success string if short_id and stats', () => {
- const data = {
- short_id: 'short_id',
- stats: {
- additions: '4',
- deletions: '5',
- },
- };
- spyOn(window, 'Flash');
-
- RepoService.commitFlash(data);
-
- expect(window.Flash).toHaveBeenCalledWith(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
- });
- });
-});
diff --git a/spec/javascripts/repo/stores/actions/branch_spec.js b/spec/javascripts/repo/stores/actions/branch_spec.js
new file mode 100644
index 00000000000..af9d6835a67
--- /dev/null
+++ b/spec/javascripts/repo/stores/actions/branch_spec.js
@@ -0,0 +1,38 @@
+import store from '~/repo/stores';
+import service from '~/repo/services';
+import { resetStore } from '../../helpers';
+
+describe('Multi-file store branch actions', () => {
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('createNewBranch', () => {
+ beforeEach(() => {
+ spyOn(service, 'createBranch').and.returnValue(Promise.resolve({
+ json: () => ({
+ name: 'testing',
+ }),
+ }));
+ spyOn(history, 'pushState');
+
+ store.state.project.id = 2;
+ store.state.currentBranch = 'testing';
+ });
+
+ it('creates new branch', (done) => {
+ store.dispatch('createNewBranch', 'master')
+ .then(() => {
+ expect(store.state.currentBranch).toBe('testing');
+ expect(service.createBranch).toHaveBeenCalledWith(2, {
+ branch: 'master',
+ ref: 'testing',
+ });
+ expect(history.pushState).toHaveBeenCalled();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js
new file mode 100644
index 00000000000..099c0556e71
--- /dev/null
+++ b/spec/javascripts/repo/stores/actions/file_spec.js
@@ -0,0 +1,417 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import service from '~/repo/services';
+import { file, resetStore } from '../../helpers';
+
+describe('Multi-file store file actions', () => {
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('closeFile', () => {
+ let localFile;
+ let getLastCommitDataSpy;
+ let oldGetLastCommitData;
+
+ beforeEach(() => {
+ getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
+ oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
+ store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
+
+ localFile = file();
+ localFile.active = true;
+ localFile.opened = true;
+ localFile.parentTreeUrl = 'parentTreeUrl';
+
+ store.state.openFiles.push(localFile);
+
+ spyOn(history, 'pushState');
+ });
+
+ afterEach(() => {
+ store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
+ });
+
+ it('closes open files', (done) => {
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(localFile.opened).toBeFalsy();
+ expect(localFile.active).toBeFalsy();
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not close file if has changed', (done) => {
+ localFile.changed = true;
+
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(localFile.opened).toBeTruthy();
+ expect(localFile.active).toBeTruthy();
+ expect(store.state.openFiles.length).toBe(1);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not close file if temp file', (done) => {
+ localFile.tempFile = true;
+
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(localFile.opened).toBeTruthy();
+ expect(localFile.active).toBeTruthy();
+ expect(store.state.openFiles.length).toBe(1);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('force closes a changed file', (done) => {
+ localFile.changed = true;
+
+ store.dispatch('closeFile', { file: localFile, force: true })
+ .then(() => {
+ expect(localFile.opened).toBeFalsy();
+ expect(localFile.active).toBeFalsy();
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls pushState when no open files are left', (done) => {
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'parentTreeUrl');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets next file as active', (done) => {
+ const f = file();
+ store.state.openFiles.push(f);
+
+ expect(f.active).toBeFalsy();
+
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(f.active).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls getLastCommitData', (done) => {
+ store.dispatch('closeFile', { file: localFile })
+ .then(() => {
+ expect(getLastCommitDataSpy).toHaveBeenCalled();
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('setFileActive', () => {
+ let scrollToTabSpy;
+ let oldScrollToTab;
+
+ beforeEach(() => {
+ scrollToTabSpy = jasmine.createSpy('scrollToTab');
+ oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
+ store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
+ });
+
+ afterEach(() => {
+ store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
+ });
+
+ it('calls scrollToTab', (done) => {
+ store.dispatch('setFileActive', file())
+ .then(() => {
+ expect(scrollToTabSpy).toHaveBeenCalled();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets the file active', (done) => {
+ const localFile = file();
+
+ store.dispatch('setFileActive', localFile)
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('returns early if file is already active', (done) => {
+ const localFile = file();
+ localFile.active = true;
+
+ store.dispatch('setFileActive', localFile)
+ .then(() => {
+ expect(scrollToTabSpy).not.toHaveBeenCalled();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets current active file to not active', (done) => {
+ const localFile = file();
+ localFile.active = true;
+ store.state.openFiles.push(localFile);
+
+ store.dispatch('setFileActive', file())
+ .then(() => {
+ expect(localFile.active).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('resets location.hash for line highlighting', (done) => {
+ location.hash = 'test';
+
+ store.dispatch('setFileActive', file())
+ .then(() => {
+ expect(location.hash).not.toBe('test');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('getFileData', () => {
+ let localFile = file();
+
+ beforeEach(() => {
+ spyOn(service, 'getFileData').and.returnValue(Promise.resolve({
+ headers: {
+ 'page-title': 'testing getFileData',
+ },
+ json: () => Promise.resolve({
+ blame_path: 'blame_path',
+ commits_path: 'commits_path',
+ permalink: 'permalink',
+ raw_path: 'raw_path',
+ binary: false,
+ html: '123',
+ render_error: '',
+ }),
+ }));
+
+ localFile = file();
+ localFile.url = 'getFileDataURL';
+ });
+
+ it('calls the service', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(() => {
+ expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets the file data', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(localFile.blamePath).toBe('blame_path');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets document title', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(() => {
+ expect(document.title).toBe('testing getFileData');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets the file as active', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('adds the file to open files', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(localFile.name);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('toggles the file loading', (done) => {
+ store.dispatch('getFileData', localFile)
+ .then(() => {
+ expect(localFile.loading).toBeTruthy();
+
+ return Vue.nextTick();
+ })
+ .then(() => {
+ expect(localFile.loading).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('getRawFileData', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw'));
+
+ tmpFile = file();
+ });
+
+ it('calls getRawFileData service method', (done) => {
+ store.dispatch('getRawFileData', tmpFile)
+ .then(() => {
+ expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('updates file raw data', (done) => {
+ store.dispatch('getRawFileData', tmpFile)
+ .then(() => {
+ expect(tmpFile.raw).toBe('raw');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('changeFileContent', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ tmpFile = file();
+ });
+
+ it('updates file content', (done) => {
+ store.dispatch('changeFileContent', {
+ file: tmpFile,
+ content: 'content',
+ })
+ .then(() => {
+ expect(tmpFile.content).toBe('content');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('createTempFile', () => {
+ beforeEach(() => {
+ document.body.innerHTML += '<div class="flash-container"></div>';
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ it('creates temp file', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then((f) => {
+ expect(f.tempFile).toBeTruthy();
+ expect(store.state.tree.length).toBe(1);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('adds tmp file to open files', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then((f) => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(f.name);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets tmp file as active', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then((f) => {
+ expect(f.active).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('enters edit mode if file is not base64', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then(() => {
+ expect(store.state.editMode).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not enter edit mode if file is base64', (done) => {
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ base64: true,
+ }).then(() => {
+ expect(store.state.editMode).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('creates flash message is file already exists', (done) => {
+ store.state.tree.push(file('test', '1', 'blob'));
+
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then(() => {
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('increases level of file', (done) => {
+ store.state.level = 1;
+
+ store.dispatch('createTempFile', {
+ tree: store.state,
+ name: 'test',
+ }).then((f) => {
+ expect(f.level).toBe(2);
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js
new file mode 100644
index 00000000000..393a797c6a3
--- /dev/null
+++ b/spec/javascripts/repo/stores/actions/tree_spec.js
@@ -0,0 +1,469 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import service from '~/repo/services';
+import { file, resetStore } from '../../helpers';
+
+describe('Multi-file store tree actions', () => {
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('getTreeData', () => {
+ beforeEach(() => {
+ spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
+ headers: {
+ 'page-title': 'test',
+ },
+ json: () => Promise.resolve({
+ last_commit_path: 'last_commit_path',
+ parent_tree_url: 'parent_tree_url',
+ path: '/',
+ trees: [{ name: 'tree' }],
+ blobs: [{ name: 'blob' }],
+ submodules: [{ name: 'submodule' }],
+ }),
+ }));
+ spyOn(history, 'pushState');
+
+ Object.assign(store.state.endpoints, {
+ rootEndpoint: 'rootEndpoint',
+ });
+ });
+
+ it('calls service getTreeData', (done) => {
+ store.dispatch('getTreeData')
+ .then(() => {
+ expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('adds data into tree', (done) => {
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.tree.length).toBe(3);
+ expect(store.state.tree[0].type).toBe('tree');
+ expect(store.state.tree[1].type).toBe('submodule');
+ expect(store.state.tree[2].type).toBe('blob');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets parent tree URL', (done) => {
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.parentTreeUrl).toBe('parent_tree_url');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets last commit path', (done) => {
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.lastCommitPath).toBe('last_commit_path');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets root if not currently at root', (done) => {
+ store.state.isInitialRoot = false;
+
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.isInitialRoot).toBeTruthy();
+ expect(store.state.isRoot).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets page title', (done) => {
+ store.dispatch('getTreeData')
+ .then(() => {
+ expect(document.title).toBe('test');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('toggles loading', (done) => {
+ store.dispatch('getTreeData')
+ .then(() => {
+ expect(store.state.loading).toBeTruthy();
+
+ return Vue.nextTick();
+ })
+ .then(() => {
+ expect(store.state.loading).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls pushState with endpoint', (done) => {
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'rootEndpoint');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls getLastCommitData if prevLastCommitPath is not null', (done) => {
+ const getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
+ const oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
+ store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
+ store.state.prevLastCommitPath = 'test';
+
+ store.dispatch('getTreeData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(getLastCommitDataSpy).toHaveBeenCalledWith(store.state);
+
+ store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('toggleTreeOpen', () => {
+ let oldGetTreeData;
+ let getTreeDataSpy;
+ let tree;
+
+ beforeEach(() => {
+ getTreeDataSpy = jasmine.createSpy('getTreeData');
+
+ oldGetTreeData = store._actions.getTreeData; // eslint-disable-line
+ store._actions.getTreeData = [getTreeDataSpy]; // eslint-disable-line
+
+ tree = {
+ opened: false,
+ tree: [],
+ };
+ });
+
+ afterEach(() => {
+ store._actions.getTreeData = oldGetTreeData; // eslint-disable-line
+ });
+
+ it('toggles the tree open', (done) => {
+ store.dispatch('toggleTreeOpen', {
+ endpoint: 'test',
+ tree,
+ }).then(() => {
+ expect(tree.opened).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls getTreeData if tree is closed', (done) => {
+ store.dispatch('toggleTreeOpen', {
+ endpoint: 'test',
+ tree,
+ }).then(() => {
+ expect(getTreeDataSpy).toHaveBeenCalledWith({
+ endpoint: 'test',
+ tree,
+ });
+ expect(store.state.previousUrl).toBe('test');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('resets entries tree', (done) => {
+ Object.assign(tree, {
+ opened: true,
+ tree: ['a'],
+ });
+
+ store.dispatch('toggleTreeOpen', {
+ endpoint: 'test',
+ tree,
+ }).then(() => {
+ expect(tree.tree.length).toBe(0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('pushes new state', (done) => {
+ spyOn(history, 'pushState');
+ Object.assign(tree, {
+ opened: true,
+ parentTreeUrl: 'testing',
+ });
+
+ store.dispatch('toggleTreeOpen', {
+ endpoint: 'test',
+ tree,
+ }).then(() => {
+ expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'testing');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('clickedTreeRow', () => {
+ describe('tree', () => {
+ let toggleTreeOpenSpy;
+ let oldToggleTreeOpen;
+
+ beforeEach(() => {
+ toggleTreeOpenSpy = jasmine.createSpy('toggleTreeOpen');
+
+ oldToggleTreeOpen = store._actions.toggleTreeOpen; // eslint-disable-line
+ store._actions.toggleTreeOpen = [toggleTreeOpenSpy]; // eslint-disable-line
+ });
+
+ afterEach(() => {
+ store._actions.toggleTreeOpen = oldToggleTreeOpen; // eslint-disable-line
+ });
+
+ it('opens tree', (done) => {
+ const tree = {
+ url: 'a',
+ type: 'tree',
+ };
+
+ store.dispatch('clickedTreeRow', tree)
+ .then(() => {
+ expect(toggleTreeOpenSpy).toHaveBeenCalledWith({
+ endpoint: tree.url,
+ tree,
+ });
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('submodule', () => {
+ let row;
+
+ beforeEach(() => {
+ spyOn(gl.utils, 'visitUrl');
+
+ row = {
+ url: 'submoduleurl',
+ type: 'submodule',
+ loading: false,
+ };
+ });
+
+ it('toggles loading for row', (done) => {
+ store.dispatch('clickedTreeRow', row)
+ .then(() => {
+ expect(row.loading).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('opens submodule URL', (done) => {
+ store.dispatch('clickedTreeRow', row)
+ .then(() => {
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('submoduleurl');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('blob', () => {
+ let row;
+
+ beforeEach(() => {
+ row = {
+ type: 'blob',
+ opened: false,
+ };
+ });
+
+ it('calls getFileData', (done) => {
+ const getFileDataSpy = jasmine.createSpy('getFileData');
+ const oldGetFileData = store._actions.getFileData; // eslint-disable-line
+ store._actions.getFileData = [getFileDataSpy]; // eslint-disable-line
+
+ store.dispatch('clickedTreeRow', row)
+ .then(() => {
+ expect(getFileDataSpy).toHaveBeenCalledWith(row);
+
+ store._actions.getFileData = oldGetFileData; // eslint-disable-line
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('calls setFileActive when file is opened', (done) => {
+ const setFileActiveSpy = jasmine.createSpy('setFileActive');
+ const oldSetFileActive = store._actions.setFileActive; // eslint-disable-line
+ store._actions.setFileActive = [setFileActiveSpy]; // eslint-disable-line
+
+ row.opened = true;
+
+ store.dispatch('clickedTreeRow', row)
+ .then(() => {
+ expect(setFileActiveSpy).toHaveBeenCalledWith(row);
+
+ store._actions.setFileActive = oldSetFileActive; // eslint-disable-line
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+ });
+
+ describe('createTempTree', () => {
+ it('creates temp tree', (done) => {
+ store.dispatch('createTempTree', 'test')
+ .then(() => {
+ expect(store.state.tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].name).toBe('test');
+ expect(store.state.tree[0].type).toBe('tree');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('creates .gitkeep file in temp tree', (done) => {
+ store.dispatch('createTempTree', 'test')
+ .then(() => {
+ expect(store.state.tree[0].tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].tree[0].name).toBe('.gitkeep');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('creates new folder inside another tree', (done) => {
+ const tree = {
+ type: 'tree',
+ name: 'testing',
+ tree: [],
+ };
+
+ store.state.tree.push(tree);
+
+ store.dispatch('createTempTree', 'testing/test')
+ .then(() => {
+ expect(store.state.tree[0].name).toBe('testing');
+ expect(store.state.tree[0].tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].tree[0].name).toBe('test');
+ expect(store.state.tree[0].tree[0].type).toBe('tree');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not create new tree if already exists', (done) => {
+ const tree = {
+ type: 'tree',
+ name: 'testing',
+ tree: [],
+ };
+
+ store.state.tree.push(tree);
+
+ store.dispatch('createTempTree', 'testing/test')
+ .then(() => {
+ expect(store.state.tree[0].name).toBe('testing');
+ expect(store.state.tree[0].tempFile).toBeUndefined();
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('getLastCommitData', () => {
+ beforeEach(() => {
+ spyOn(service, 'getTreeLastCommit').and.returnValue(Promise.resolve({
+ headers: {
+ 'more-logs-url': null,
+ },
+ json: () => Promise.resolve([{
+ type: 'tree',
+ file_name: 'testing',
+ commit: {
+ message: 'commit message',
+ authored_date: '123',
+ },
+ }]),
+ }));
+
+ store.state.tree.push(file('testing', '1', 'tree'));
+ store.state.lastCommitPath = 'lastcommitpath';
+ });
+
+ it('calls service with lastCommitPath', (done) => {
+ store.dispatch('getLastCommitData')
+ .then(() => {
+ expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('updates trees last commit data', (done) => {
+ store.dispatch('getLastCommitData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.tree[0].lastCommit.message).toBe('commit message');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('does not update entry if not found', (done) => {
+ store.state.tree[0].name = 'a';
+
+ store.dispatch('getLastCommitData')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.tree[0].lastCommit.message).not.toBe('commit message');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('updateDirectoryData', () => {
+ it('adds data into tree', (done) => {
+ const tree = {
+ tree: [],
+ };
+ const data = {
+ trees: [{ name: 'tree' }],
+ submodules: [{ name: 'submodule' }],
+ blobs: [{ name: 'blob' }],
+ };
+
+ store.dispatch('updateDirectoryData', {
+ data,
+ tree,
+ }).then(() => {
+ expect(tree.tree[0].name).toBe('tree');
+ expect(tree.tree[0].type).toBe('tree');
+ expect(tree.tree[1].name).toBe('submodule');
+ expect(tree.tree[1].type).toBe('submodule');
+ expect(tree.tree[2].name).toBe('blob');
+ expect(tree.tree[2].type).toBe('blob');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js
new file mode 100644
index 00000000000..f2a7a698912
--- /dev/null
+++ b/spec/javascripts/repo/stores/actions_spec.js
@@ -0,0 +1,419 @@
+import Vue from 'vue';
+import store from '~/repo/stores';
+import service from '~/repo/services';
+import { resetStore, file } from '../helpers';
+
+describe('Multi-file store actions', () => {
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('redirectToUrl', () => {
+ it('calls visitUrl', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+
+ store.dispatch('redirectToUrl', 'test')
+ .then(() => {
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('test');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('setInitialData', () => {
+ it('commits initial data', (done) => {
+ store.dispatch('setInitialData', { canCommit: true })
+ .then(() => {
+ expect(store.state.canCommit).toBeTruthy();
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('closeDiscardPopup', () => {
+ it('closes the discard popup', (done) => {
+ store.dispatch('closeDiscardPopup', false)
+ .then(() => {
+ expect(store.state.discardPopupOpen).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('discardAllChanges', () => {
+ beforeEach(() => {
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].changed = true;
+ });
+ });
+
+ describe('closeAllFiles', () => {
+ beforeEach(() => {
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].opened = true;
+ });
+
+ it('closes all open files', (done) => {
+ store.dispatch('closeAllFiles')
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('toggleEditMode', () => {
+ it('toggles edit mode', (done) => {
+ store.state.editMode = true;
+
+ store.dispatch('toggleEditMode')
+ .then(() => {
+ expect(store.state.editMode).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('sets preview mode', (done) => {
+ store.state.currentBlobView = 'repo-editor';
+ store.state.editMode = true;
+
+ store.dispatch('toggleEditMode')
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.currentBlobView).toBe('repo-preview');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('opens discard popup if there are changed files', (done) => {
+ store.state.editMode = true;
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].changed = true;
+
+ store.dispatch('toggleEditMode')
+ .then(() => {
+ expect(store.state.discardPopupOpen).toBeTruthy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('can force closed if there are changed files', (done) => {
+ store.state.editMode = true;
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].changed = true;
+
+ store.dispatch('toggleEditMode', true)
+ .then(() => {
+ expect(store.state.discardPopupOpen).toBeFalsy();
+ expect(store.state.editMode).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('discards file changes', (done) => {
+ const f = file();
+ store.state.editMode = true;
+ store.state.tree.push(f);
+ store.state.openFiles.push(f);
+ f.changed = true;
+
+ store.dispatch('toggleEditMode', true)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(f.changed).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('toggleBlobView', () => {
+ it('sets edit mode view if in edit mode', (done) => {
+ store.state.editMode = true;
+
+ store.dispatch('toggleBlobView')
+ .then(() => {
+ expect(store.state.currentBlobView).toBe('repo-editor');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets preview mode view if not in edit mode', (done) => {
+ store.dispatch('toggleBlobView')
+ .then(() => {
+ expect(store.state.currentBlobView).toBe('repo-preview');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('checkCommitStatus', () => {
+ beforeEach(() => {
+ store.state.project.id = 2;
+ store.state.currentBranch = 'master';
+ store.state.currentRef = '1';
+ });
+
+ it('calls service', (done) => {
+ spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
+ commit: { id: '123' },
+ }));
+
+ store.dispatch('checkCommitStatus')
+ .then(() => {
+ expect(service.getBranchData).toHaveBeenCalledWith(2, 'master');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns true if current ref does not equal returned ID', (done) => {
+ spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
+ commit: { id: '123' },
+ }));
+
+ store.dispatch('checkCommitStatus')
+ .then((val) => {
+ expect(val).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns false if current ref equals returned ID', (done) => {
+ spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
+ commit: { id: '1' },
+ }));
+
+ store.dispatch('checkCommitStatus')
+ .then((val) => {
+ expect(val).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('commitChanges', () => {
+ let payload;
+
+ beforeEach(() => {
+ spyOn(window, 'scrollTo');
+
+ document.body.innerHTML += '<div class="flash-container"></div>';
+
+ store.state.project.id = 123;
+ payload = {
+ branch: 'master',
+ };
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'commit').and.returnValue(Promise.resolve({
+ id: '123456',
+ short_id: '123',
+ message: 'test message',
+ committed_date: 'date',
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ }));
+ });
+
+ it('calls service', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(service.commit).toHaveBeenCalledWith(123, payload);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('shows flash notice', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ const alert = document.querySelector('.flash-container');
+
+ expect(alert.querySelector('.flash-notice')).not.toBeNull();
+ expect(alert.textContent.trim()).toBe(
+ 'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.',
+ );
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('adds commit data to changed files', (done) => {
+ const changedFile = file();
+ const f = file();
+ changedFile.changed = true;
+
+ store.state.openFiles.push(changedFile, f);
+
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(changedFile.lastCommit.message).toBe('test message');
+ expect(f.lastCommit.message).not.toBe('test message');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('toggles edit mode', (done) => {
+ store.state.editMode = true;
+
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(store.state.editMode).toBeFalsy();
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('closes all files', (done) => {
+ store.state.openFiles.push(file());
+ store.state.openFiles[0].opened = true;
+
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('scrolls to top of page', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('updates commit ref', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ expect(store.state.currentRef).toBe('123456');
+
+ done();
+ }).catch(done.fail);
+ });
+
+ it('redirects to new merge request page', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+
+ store.state.endpoints.newMergeRequestUrl = 'newMergeRequestUrl?branch=';
+
+ store.dispatch('commitChanges', { payload, newMr: true })
+ .then(() => {
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master');
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('failed', () => {
+ beforeEach(() => {
+ spyOn(service, 'commit').and.returnValue(Promise.resolve({
+ message: 'failed message',
+ }));
+ });
+
+ it('shows failed message', (done) => {
+ store.dispatch('commitChanges', { payload, newMr: false })
+ .then(() => {
+ const alert = document.querySelector('.flash-container');
+
+ expect(alert.textContent.trim()).toBe(
+ 'failed message',
+ );
+
+ done();
+ }).catch(done.fail);
+ });
+ });
+ });
+
+ describe('createTempEntry', () => {
+ it('creates a temp tree', (done) => {
+ store.dispatch('createTempEntry', {
+ name: 'test',
+ type: 'tree',
+ })
+ .then(() => {
+ expect(store.state.tree.length).toBe(1);
+ expect(store.state.tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].type).toBe('tree');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('creates temp file', (done) => {
+ store.dispatch('createTempEntry', {
+ name: 'test',
+ type: 'blob',
+ })
+ .then(() => {
+ expect(store.state.tree.length).toBe(1);
+ expect(store.state.tree[0].tempFile).toBeTruthy();
+ expect(store.state.tree[0].type).toBe('blob');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('popHistoryState', () => {
+
+ });
+
+ describe('scrollToTab', () => {
+ it('focuses the current active element', (done) => {
+ document.body.innerHTML += '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>';
+ const el = document.querySelector('.repo-tab');
+ spyOn(el, 'focus');
+
+ store.dispatch('scrollToTab')
+ .then(() => {
+ setTimeout(() => {
+ expect(el.focus).toHaveBeenCalled();
+
+ document.getElementById('tabs').remove();
+
+ done();
+ });
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/getters_spec.js b/spec/javascripts/repo/stores/getters_spec.js
new file mode 100644
index 00000000000..952b8ec3a59
--- /dev/null
+++ b/spec/javascripts/repo/stores/getters_spec.js
@@ -0,0 +1,146 @@
+import * as getters from '~/repo/stores/getters';
+import state from '~/repo/stores/state';
+import { file } from '../helpers';
+
+describe('Multi-file store getters', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('treeList', () => {
+ it('returns flat tree list', () => {
+ localState.tree.push(file('1'));
+ localState.tree[0].tree.push(file('2'));
+ localState.tree[0].tree[0].tree.push(file('3'));
+
+ const treeList = getters.treeList(localState);
+
+ expect(treeList.length).toBe(3);
+ expect(treeList[1].name).toBe(localState.tree[0].tree[0].name);
+ expect(treeList[2].name).toBe(localState.tree[0].tree[0].tree[0].name);
+ });
+ });
+
+ describe('changedFiles', () => {
+ it('returns a list of changed opened files', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('changed'));
+ localState.openFiles[1].changed = true;
+
+ const changedFiles = getters.changedFiles(localState);
+
+ expect(changedFiles.length).toBe(1);
+ expect(changedFiles[0].name).toBe('changed');
+ });
+ });
+
+ describe('activeFile', () => {
+ it('returns the current active file', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('active'));
+ localState.openFiles[1].active = true;
+
+ expect(getters.activeFile(localState).name).toBe('active');
+ });
+
+ it('returns undefined if no active files are found', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('active'));
+
+ expect(getters.activeFile(localState)).toBeUndefined();
+ });
+ });
+
+ describe('activeFileExtension', () => {
+ it('returns the file extension for the current active file', () => {
+ localState.openFiles.push(file('active'));
+ localState.openFiles[0].active = true;
+ localState.openFiles[0].path = 'test.js';
+
+ expect(getters.activeFileExtension(localState)).toBe('.js');
+
+ localState.openFiles[0].path = 'test.es6.js';
+
+ expect(getters.activeFileExtension(localState)).toBe('.js');
+ });
+ });
+
+ describe('isCollapsed', () => {
+ it('returns true if state has open files', () => {
+ localState.openFiles.push(file());
+
+ expect(getters.isCollapsed(localState)).toBeTruthy();
+ });
+
+ it('returns false if state has no open files', () => {
+ expect(getters.isCollapsed(localState)).toBeFalsy();
+ });
+ });
+
+ describe('canEditFile', () => {
+ beforeEach(() => {
+ localState.onTopOfBranch = true;
+ localState.canCommit = true;
+
+ localState.openFiles.push(file());
+ localState.openFiles[0].active = true;
+ });
+
+ it('returns true if user can commit and has open files', () => {
+ expect(getters.canEditFile(localState)).toBeTruthy();
+ });
+
+ it('returns false if user can commit and has no open files', () => {
+ localState.openFiles = [];
+
+ expect(getters.canEditFile(localState)).toBeFalsy();
+ });
+
+ it('returns false if user can commit and active file is binary', () => {
+ localState.openFiles[0].binary = true;
+
+ expect(getters.canEditFile(localState)).toBeFalsy();
+ });
+
+ it('returns false if user cant commit', () => {
+ localState.canCommit = false;
+
+ expect(getters.canEditFile(localState)).toBeFalsy();
+ });
+
+ it('returns false if user can commit but on a branch', () => {
+ localState.onTopOfBranch = false;
+
+ expect(getters.canEditFile(localState)).toBeFalsy();
+ });
+ });
+
+ describe('modifiedFiles', () => {
+ it('returns a list of modified files', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('changed'));
+ localState.openFiles[1].changed = true;
+
+ const modifiedFiles = getters.modifiedFiles(localState);
+
+ expect(modifiedFiles.length).toBe(1);
+ expect(modifiedFiles[0].name).toBe('changed');
+ });
+ });
+
+ describe('addedFiles', () => {
+ it('returns a list of added files', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('added'));
+ localState.openFiles[1].changed = true;
+ localState.openFiles[1].tempFile = true;
+
+ const modifiedFiles = getters.addedFiles(localState);
+
+ expect(modifiedFiles.length).toBe(1);
+ expect(modifiedFiles[0].name).toBe('added');
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/mutations/branch_spec.js b/spec/javascripts/repo/stores/mutations/branch_spec.js
new file mode 100644
index 00000000000..3c06794d5e3
--- /dev/null
+++ b/spec/javascripts/repo/stores/mutations/branch_spec.js
@@ -0,0 +1,18 @@
+import mutations from '~/repo/stores/mutations/branch';
+import state from '~/repo/stores/state';
+
+describe('Multi-file store branch mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('SET_CURRENT_BRANCH', () => {
+ it('sets currentBranch', () => {
+ mutations.SET_CURRENT_BRANCH(localState, 'master');
+
+ expect(localState.currentBranch).toBe('master');
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/mutations/file_spec.js b/spec/javascripts/repo/stores/mutations/file_spec.js
new file mode 100644
index 00000000000..2f2835dde1f
--- /dev/null
+++ b/spec/javascripts/repo/stores/mutations/file_spec.js
@@ -0,0 +1,131 @@
+import mutations from '~/repo/stores/mutations/file';
+import state from '~/repo/stores/state';
+import { file } from '../../helpers';
+
+describe('Multi-file store file mutations', () => {
+ let localState;
+ let localFile;
+
+ beforeEach(() => {
+ localState = state();
+ localFile = file();
+ });
+
+ describe('SET_FILE_ACTIVE', () => {
+ it('sets the file active', () => {
+ mutations.SET_FILE_ACTIVE(localState, {
+ file: localFile,
+ active: true,
+ });
+
+ expect(localFile.active).toBeTruthy();
+ });
+ });
+
+ describe('TOGGLE_FILE_OPEN', () => {
+ beforeEach(() => {
+ mutations.TOGGLE_FILE_OPEN(localState, localFile);
+ });
+
+ it('adds into opened files', () => {
+ expect(localFile.opened).toBeTruthy();
+ expect(localState.openFiles.length).toBe(1);
+ });
+
+ it('removes from opened files', () => {
+ mutations.TOGGLE_FILE_OPEN(localState, localFile);
+
+ expect(localFile.opened).toBeFalsy();
+ expect(localState.openFiles.length).toBe(0);
+ });
+ });
+
+ describe('SET_FILE_DATA', () => {
+ it('sets extra file data', () => {
+ mutations.SET_FILE_DATA(localState, {
+ data: {
+ blame_path: 'blame',
+ commits_path: 'commits',
+ permalink: 'permalink',
+ raw_path: 'raw',
+ binary: true,
+ html: 'html',
+ render_error: 'render_error',
+ },
+ file: localFile,
+ });
+
+ expect(localFile.blamePath).toBe('blame');
+ expect(localFile.commitsPath).toBe('commits');
+ expect(localFile.permalink).toBe('permalink');
+ expect(localFile.rawPath).toBe('raw');
+ expect(localFile.binary).toBeTruthy();
+ expect(localFile.html).toBe('html');
+ expect(localFile.renderError).toBe('render_error');
+ });
+ });
+
+ describe('SET_FILE_RAW_DATA', () => {
+ it('sets raw data', () => {
+ mutations.SET_FILE_RAW_DATA(localState, {
+ file: localFile,
+ raw: 'testing',
+ });
+
+ expect(localFile.raw).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_FILE_CONTENT', () => {
+ beforeEach(() => {
+ localFile.raw = 'test';
+ });
+
+ it('sets content', () => {
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ file: localFile,
+ content: 'test',
+ });
+
+ expect(localFile.content).toBe('test');
+ });
+
+ it('sets changed if content does not match raw', () => {
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ file: localFile,
+ content: 'testing',
+ });
+
+ expect(localFile.content).toBe('testing');
+ expect(localFile.changed).toBeTruthy();
+ });
+ });
+
+ describe('DISCARD_FILE_CHANGES', () => {
+ beforeEach(() => {
+ localFile.content = 'test';
+ localFile.changed = true;
+ });
+
+ it('resets content and changed', () => {
+ mutations.DISCARD_FILE_CHANGES(localState, localFile);
+
+ expect(localFile.content).toBe('');
+ expect(localFile.changed).toBeFalsy();
+ });
+ });
+
+ describe('CREATE_TMP_FILE', () => {
+ it('adds file into parent tree', () => {
+ const f = file();
+
+ mutations.CREATE_TMP_FILE(localState, {
+ file: f,
+ parent: localFile,
+ });
+
+ expect(localFile.tree.length).toBe(1);
+ expect(localFile.tree[0].name).toBe(f.name);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js
new file mode 100644
index 00000000000..1c76cfed9c8
--- /dev/null
+++ b/spec/javascripts/repo/stores/mutations/tree_spec.js
@@ -0,0 +1,71 @@
+import mutations from '~/repo/stores/mutations/tree';
+import state from '~/repo/stores/state';
+import { file } from '../../helpers';
+
+describe('Multi-file store tree mutations', () => {
+ let localState;
+ let localTree;
+
+ beforeEach(() => {
+ localState = state();
+ localTree = file();
+ });
+
+ describe('TOGGLE_TREE_OPEN', () => {
+ it('toggles tree open', () => {
+ mutations.TOGGLE_TREE_OPEN(localState, localTree);
+
+ expect(localTree.opened).toBeTruthy();
+
+ mutations.TOGGLE_TREE_OPEN(localState, localTree);
+
+ expect(localTree.opened).toBeFalsy();
+ });
+ });
+
+ describe('SET_DIRECTORY_DATA', () => {
+ const data = [{
+ name: 'tree',
+ },
+ {
+ name: 'submodule',
+ },
+ {
+ name: 'blob',
+ }];
+
+ it('adds directory data', () => {
+ mutations.SET_DIRECTORY_DATA(localState, {
+ data,
+ tree: localState,
+ });
+
+ expect(localState.tree.length).toBe(3);
+ expect(localState.tree[0].name).toBe('tree');
+ expect(localState.tree[1].name).toBe('submodule');
+ expect(localState.tree[2].name).toBe('blob');
+ });
+ });
+
+ describe('SET_PARENT_TREE_URL', () => {
+ it('sets the parent tree url', () => {
+ mutations.SET_PARENT_TREE_URL(localState, 'test');
+
+ expect(localState.parentTreeUrl).toBe('test');
+ });
+ });
+
+ describe('CREATE_TMP_TREE', () => {
+ it('adds tree into parent tree', () => {
+ const tmpEntry = file();
+
+ mutations.CREATE_TMP_TREE(localState, {
+ tmpEntry,
+ parent: localTree,
+ });
+
+ expect(localTree.tree.length).toBe(1);
+ expect(localTree.tree[0].name).toBe(tmpEntry.name);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/mutations_spec.js b/spec/javascripts/repo/stores/mutations_spec.js
new file mode 100644
index 00000000000..d1c9885e01d
--- /dev/null
+++ b/spec/javascripts/repo/stores/mutations_spec.js
@@ -0,0 +1,117 @@
+import mutations from '~/repo/stores/mutations';
+import state from '~/repo/stores/state';
+import { file } from '../helpers';
+
+describe('Multi-file store mutations', () => {
+ let localState;
+ let entry;
+
+ beforeEach(() => {
+ localState = state();
+ entry = file();
+ });
+
+ describe('SET_INITIAL_DATA', () => {
+ it('sets all initial data', () => {
+ mutations.SET_INITIAL_DATA(localState, {
+ test: 'test',
+ });
+
+ expect(localState.test).toBe('test');
+ });
+ });
+
+ describe('SET_PREVIEW_MODE', () => {
+ it('sets currentBlobView to repo-preview', () => {
+ mutations.SET_PREVIEW_MODE(localState);
+
+ expect(localState.currentBlobView).toBe('repo-preview');
+
+ localState.currentBlobView = 'testing';
+
+ mutations.SET_PREVIEW_MODE(localState);
+
+ expect(localState.currentBlobView).toBe('repo-preview');
+ });
+ });
+
+ describe('SET_EDIT_MODE', () => {
+ it('sets currentBlobView to repo-editor', () => {
+ mutations.SET_EDIT_MODE(localState);
+
+ expect(localState.currentBlobView).toBe('repo-editor');
+
+ localState.currentBlobView = 'testing';
+
+ mutations.SET_EDIT_MODE(localState);
+
+ expect(localState.currentBlobView).toBe('repo-editor');
+ });
+ });
+
+ describe('TOGGLE_LOADING', () => {
+ it('toggles loading of entry', () => {
+ mutations.TOGGLE_LOADING(localState, entry);
+
+ expect(entry.loading).toBeTruthy();
+
+ mutations.TOGGLE_LOADING(localState, entry);
+
+ expect(entry.loading).toBeFalsy();
+ });
+ });
+
+ describe('TOGGLE_EDIT_MODE', () => {
+ it('toggles editMode', () => {
+ mutations.TOGGLE_EDIT_MODE(localState);
+
+ expect(localState.editMode).toBeTruthy();
+
+ mutations.TOGGLE_EDIT_MODE(localState);
+
+ expect(localState.editMode).toBeFalsy();
+ });
+ });
+
+ describe('TOGGLE_DISCARD_POPUP', () => {
+ it('sets discardPopupOpen', () => {
+ mutations.TOGGLE_DISCARD_POPUP(localState, true);
+
+ expect(localState.discardPopupOpen).toBeTruthy();
+
+ mutations.TOGGLE_DISCARD_POPUP(localState, false);
+
+ expect(localState.discardPopupOpen).toBeFalsy();
+ });
+ });
+
+ describe('SET_COMMIT_REF', () => {
+ it('sets currentRef', () => {
+ mutations.SET_COMMIT_REF(localState, '123');
+
+ expect(localState.currentRef).toBe('123');
+ });
+ });
+
+ describe('SET_ROOT', () => {
+ it('sets isRoot & initialRoot', () => {
+ mutations.SET_ROOT(localState, true);
+
+ expect(localState.isRoot).toBeTruthy();
+ expect(localState.isInitialRoot).toBeTruthy();
+
+ mutations.SET_ROOT(localState, false);
+
+ expect(localState.isRoot).toBeFalsy();
+ expect(localState.isInitialRoot).toBeFalsy();
+ });
+ });
+
+ describe('SET_PREVIOUS_URL', () => {
+ it('sets previousUrl', () => {
+ mutations.SET_PREVIOUS_URL(localState, 'testing');
+
+ expect(localState.previousUrl).toBe('testing');
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/utils_spec.js b/spec/javascripts/repo/stores/utils_spec.js
new file mode 100644
index 00000000000..37287c587d7
--- /dev/null
+++ b/spec/javascripts/repo/stores/utils_spec.js
@@ -0,0 +1,102 @@
+import * as utils from '~/repo/stores/utils';
+
+describe('Multi-file store utils', () => {
+ describe('setPageTitle', () => {
+ it('sets the document page title', () => {
+ utils.setPageTitle('test');
+
+ expect(document.title).toBe('test');
+ });
+ });
+
+ describe('pushState', () => {
+ it('calls history.pushState', () => {
+ spyOn(history, 'pushState');
+
+ utils.pushState('test');
+
+ expect(history.pushState).toHaveBeenCalledWith({ url: 'test' }, '', 'test');
+ });
+ });
+
+ describe('createTemp', () => {
+ it('creates temp tree', () => {
+ const tmp = utils.createTemp({
+ name: 'test',
+ path: 'test',
+ type: 'tree',
+ level: 0,
+ changed: false,
+ content: '',
+ base64: '',
+ });
+
+ expect(tmp.tempFile).toBeTruthy();
+ expect(tmp.icon).toBe('fa-folder');
+ });
+
+ it('creates temp file', () => {
+ const tmp = utils.createTemp({
+ name: 'test',
+ path: 'test',
+ type: 'blob',
+ level: 0,
+ changed: false,
+ content: '',
+ base64: '',
+ });
+
+ expect(tmp.tempFile).toBeTruthy();
+ expect(tmp.icon).toBe('fa-file-text-o');
+ });
+ });
+
+ describe('findIndexOfFile', () => {
+ let state;
+
+ beforeEach(() => {
+ state = [{
+ path: '1',
+ }, {
+ path: '2',
+ }];
+ });
+
+ it('finds in the index of an entry by path', () => {
+ const index = utils.findIndexOfFile(state, {
+ path: '2',
+ });
+
+ expect(index).toBe(1);
+ });
+ });
+
+ describe('findEntry', () => {
+ let state;
+
+ beforeEach(() => {
+ state = {
+ tree: [{
+ type: 'tree',
+ name: 'test',
+ }, {
+ type: 'blob',
+ name: 'file',
+ }],
+ };
+ });
+
+ it('returns an entry found by name', () => {
+ const foundEntry = utils.findEntry(state, 'tree', 'test');
+
+ expect(foundEntry.type).toBe('tree');
+ expect(foundEntry.name).toBe('test');
+ });
+
+ it('returns undefined when no entry found', () => {
+ const foundEntry = utils.findEntry(state, 'blob', 'test');
+
+ expect(foundEntry).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index a53f58b5d0d..a2394857b82 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -3,10 +3,9 @@
import '~/gl_dropdown';
import '~/search_autocomplete';
import '~/lib/utils/common_utils';
-import 'vendor/fuzzaldrin-plus';
(function() {
- var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
+ var assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
var userName = 'root';
widget = null;
@@ -29,29 +28,39 @@ import 'vendor/fuzzaldrin-plus';
groupName = 'Gitlab Org';
+ const removeBodyAttributes = function() {
+ const $body = $('body');
+
+ $body.removeAttr('data-page');
+ $body.removeAttr('data-project');
+ $body.removeAttr('data-group');
+ };
+
// Add required attributes to body before starting the test.
// section would be dashboard|group|project
- addBodyAttributes = function(section) {
- var $body;
+ const addBodyAttributes = function(section) {
if (section == null) {
section = 'dashboard';
}
- $body = $('body');
- $body.removeAttr('data-page');
- $body.removeAttr('data-project');
- $body.removeAttr('data-group');
+
+ const $body = $('body');
+ removeBodyAttributes();
switch (section) {
case 'dashboard':
- return $body.data('page', 'root:index');
+ return $body.attr('data-page', 'root:index');
case 'group':
- $body.data('page', 'groups:show');
+ $body.attr('data-page', 'groups:show');
return $body.data('group', 'gitlab-org');
case 'project':
- $body.data('page', 'projects:show');
+ $body.attr('data-page', 'projects:show');
return $body.data('project', 'gitlab-ce');
}
};
+ const disableProjectIssues = function() {
+ document.querySelector('.js-search-project-options').setAttribute('data-issues-disabled', true);
+ };
+
// Mock `gl` object in window for dashboard specific page. App code will need it.
mockDashboardOptions = function() {
window.gl || (window.gl = {});
@@ -86,18 +95,20 @@ import 'vendor/fuzzaldrin-plus';
assertLinks = function(list, issuesPath, mrsPath) {
var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink;
- issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName;
- issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName;
+ if (issuesPath) {
+ issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName;
+ issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName;
+ a1 = "a[href='" + issuesAssignedToMeLink + "']";
+ a2 = "a[href='" + issuesIHaveCreatedLink + "']";
+ expect(list.find(a1).length).toBe(1);
+ expect(list.find(a1).text()).toBe('Issues assigned to me');
+ expect(list.find(a2).length).toBe(1);
+ expect(list.find(a2).text()).toBe("Issues I've created");
+ }
mrsAssignedToMeLink = mrsPath + "/?assignee_username=" + userName;
mrsIHaveCreatedLink = mrsPath + "/?author_username=" + userName;
- a1 = "a[href='" + issuesAssignedToMeLink + "']";
- a2 = "a[href='" + issuesIHaveCreatedLink + "']";
a3 = "a[href='" + mrsAssignedToMeLink + "']";
a4 = "a[href='" + mrsIHaveCreatedLink + "']";
- expect(list.find(a1).length).toBe(1);
- expect(list.find(a1).text()).toBe('Issues assigned to me');
- expect(list.find(a2).length).toBe(1);
- expect(list.find(a2).text()).toBe("Issues I've created");
expect(list.find(a3).length).toBe(1);
expect(list.find(a3).text()).toBe('Merge requests assigned to me');
expect(list.find(a4).length).toBe(1);
@@ -108,7 +119,7 @@ import 'vendor/fuzzaldrin-plus';
preloadFixtures('static/search_autocomplete.html.raw');
beforeEach(function() {
loadFixtures('static/search_autocomplete.html.raw');
- widget = new gl.SearchAutocomplete;
+
// Prevent turbolinks from triggering within gl_dropdown
spyOn(window.gl.utils, 'visitUrl').and.returnValue(true);
@@ -120,6 +131,8 @@ import 'vendor/fuzzaldrin-plus';
});
afterEach(function() {
+ // Undo what we did to the shared <body>
+ removeBodyAttributes();
window.gon = {};
});
it('should show Dashboard specific dropdown menu', function() {
@@ -146,6 +159,14 @@ import 'vendor/fuzzaldrin-plus';
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, projectIssuesPath, projectMRsPath);
});
+ it('should show only Project mergeRequest dropdown menu items when project issues are disabled', function() {
+ addBodyAttributes('project');
+ disableProjectIssues();
+ mockProjectOptions();
+ widget.searchInput.triggerHandler('focus');
+ const list = widget.wrap.find('.dropdown-menu').find('ul');
+ assertLinks(list, null, projectMRsPath);
+ });
it('should not show category related menu if there is text in the input', function() {
var link, list;
addBodyAttributes('project');
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index a912e150e9b..5d6a885d4cc 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,7 +1,7 @@
-/* global ShortcutsIssuable */
+import initCopyAsGFM from '~/behaviors/copy_as_gfm';
+import ShortcutsIssuable from '~/shortcuts_issuable';
-import '~/copy_as_gfm';
-import '~/shortcuts_issuable';
+initCopyAsGFM();
describe('ShortcutsIssuable', () => {
const fixtureName = 'merge_requests/diff_comment.html.raw';
diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js
index 53e4c68beb3..a2a609edef9 100644
--- a/spec/javascripts/shortcuts_spec.js
+++ b/spec/javascripts/shortcuts_spec.js
@@ -1,4 +1,5 @@
-/* global Shortcuts */
+import Shortcuts from '~/shortcuts';
+
describe('Shortcuts', () => {
const fixtureName = 'merge_requests/diff_comment.html.raw';
const createEvent = (type, target) => $.Event(type, {
@@ -8,19 +9,17 @@ describe('Shortcuts', () => {
preloadFixtures(fixtureName);
describe('toggleMarkdownPreview', () => {
- let sc;
-
beforeEach(() => {
loadFixtures(fixtureName);
spyOnEvent('.js-new-note-form .js-md-preview-button', 'focus');
spyOnEvent('.edit-note .js-md-preview-button', 'focus');
- sc = new Shortcuts();
+ new Shortcuts(); // eslint-disable-line no-new
});
it('focuses preview button in form', () => {
- sc.toggleMarkdownPreview(
+ Shortcuts.toggleMarkdownPreview(
createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text'),
));
@@ -31,7 +30,7 @@ describe('Shortcuts', () => {
document.querySelector('.js-note-edit').click();
setTimeout(() => {
- sc.toggleMarkdownPreview(
+ Shortcuts.toggleMarkdownPreview(
createEvent('KeyboardEvent', document.querySelector('.edit-note .js-note-text'),
));
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index e2b6bcabc98..0682b463043 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -109,12 +109,14 @@ const sidebarMockData = {
labels: [],
web_url: '/root/some-project/issues/5',
},
+ '/gitlab-org/gitlab-shell/issues/5/toggle_subscription': {},
},
};
export default {
mediator: {
endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
+ toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
editable: true,
diff --git a/spec/javascripts/sidebar/participants_spec.js b/spec/javascripts/sidebar/participants_spec.js
new file mode 100644
index 00000000000..30cc549c7c0
--- /dev/null
+++ b/spec/javascripts/sidebar/participants_spec.js
@@ -0,0 +1,174 @@
+import Vue from 'vue';
+import participants from '~/sidebar/components/participants/participants.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+const PARTICIPANT = {
+ id: 1,
+ state: 'active',
+ username: 'marcene',
+ name: 'Allie Will',
+ web_url: 'foo.com',
+ avatar_url: 'gravatar.com/avatar/xxx',
+};
+
+const PARTICIPANT_LIST = [
+ PARTICIPANT,
+ { ...PARTICIPANT, id: 2 },
+ { ...PARTICIPANT, id: 3 },
+];
+
+describe('Participants', function () {
+ let vm;
+ let Participants;
+
+ beforeEach(() => {
+ Participants = Vue.extend(participants);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('collapsed sidebar state', () => {
+ it('shows loading spinner when loading', () => {
+ vm = mountComponent(Participants, {
+ loading: true,
+ });
+
+ expect(vm.$el.querySelector('.js-participants-collapsed-loading-icon')).toBeDefined();
+ });
+
+ it('shows participant count when given', () => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ });
+ const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
+
+ expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
+ });
+
+ it('shows full participant count when there are hidden participants', () => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 1,
+ });
+ const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
+
+ expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
+ });
+ });
+
+ describe('expanded sidebar state', () => {
+ it('shows loading spinner when loading', () => {
+ vm = mountComponent(Participants, {
+ loading: true,
+ });
+
+ expect(vm.$el.querySelector('.js-participants-expanded-loading-icon')).toBeDefined();
+ });
+
+ it('when only showing visible participants, shows an avatar only for each participant under the limit', (done) => {
+ const numberOfLessParticipants = 2;
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+ vm.isShowingMoreParticipants = false;
+
+ Vue.nextTick()
+ .then(() => {
+ const participantEls = vm.$el.querySelectorAll('.js-participants-author');
+
+ expect(participantEls.length).toBe(numberOfLessParticipants);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('when only showing all participants, each has an avatar', (done) => {
+ const numberOfLessParticipants = 2;
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+ vm.isShowingMoreParticipants = true;
+
+ Vue.nextTick()
+ .then(() => {
+ const participantEls = vm.$el.querySelectorAll('.js-participants-author');
+
+ expect(participantEls.length).toBe(PARTICIPANT_LIST.length);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not have more participants link when they can all be shown', () => {
+ const numberOfLessParticipants = 100;
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+ const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+ expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
+ expect(moreParticipantLink).toBeNull();
+ });
+
+ it('when too many participants, has more participants link to show more', (done) => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+ vm.isShowingMoreParticipants = false;
+
+ Vue.nextTick()
+ .then(() => {
+ const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+ expect(moreParticipantLink.textContent.trim()).toBe('+ 1 more');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('when too many participants and already showing them, has more participants link to show less', (done) => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+ vm.isShowingMoreParticipants = true;
+
+ Vue.nextTick()
+ .then(() => {
+ const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+ expect(moreParticipantLink.textContent.trim()).toBe('- show less');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('clicking more participants link emits event', () => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+ const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
+
+ expect(vm.isShowingMoreParticipants).toBe(false);
+
+ moreParticipantLink.click();
+
+ expect(vm.isShowingMoreParticipants).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
index 3aa8ca5db0d..7deb1fd2118 100644
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -57,8 +57,8 @@ describe('Sidebar mediator', () => {
.then(() => {
expect(this.mediator.service.getProjectsAutocomplete).toHaveBeenCalledWith(searchTerm);
expect(this.mediator.store.setAutocompleteProjects).toHaveBeenCalled();
- done();
})
+ .then(done)
.catch(done.fail);
});
@@ -72,8 +72,21 @@ describe('Sidebar mediator', () => {
.then(() => {
expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
expect(gl.utils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
- done();
})
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('toggle subscription', (done) => {
+ this.mediator.store.setSubscribedState(false);
+ spyOn(this.mediator.service, 'toggleSubscription').and.callThrough();
+
+ this.mediator.toggleSubscription()
+ .then(() => {
+ expect(this.mediator.service.toggleSubscription).toHaveBeenCalled();
+ expect(this.mediator.store.subscribed).toEqual(true);
+ })
+ .then(done)
.catch(done.fail);
});
});
diff --git a/spec/javascripts/sidebar/sidebar_service_spec.js b/spec/javascripts/sidebar/sidebar_service_spec.js
deleted file mode 100644
index a4bd8ba8d88..00000000000
--- a/spec/javascripts/sidebar/sidebar_service_spec.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import Vue from 'vue';
-import SidebarService from '~/sidebar/services/sidebar_service';
-import Mock from './mock_data';
-
-describe('Sidebar service', () => {
- beforeEach(() => {
- Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
- 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(() => {
- SidebarService.singleton = null;
- Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor);
- });
-
- it('gets the data', (done) => {
- this.service.get()
- .then((resp) => {
- expect(resp).toBeDefined();
- done();
- })
- .catch(done.fail);
- });
-
- it('updates the data', (done) => {
- this.service.update('issue[assignee_ids]', [1])
- .then((resp) => {
- expect(resp).toBeDefined();
- done();
- })
- .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 69eb3839d67..51dee64fb93 100644
--- a/spec/javascripts/sidebar/sidebar_store_spec.js
+++ b/spec/javascripts/sidebar/sidebar_store_spec.js
@@ -2,21 +2,36 @@ import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
import UsersMockHelper from '../helpers/user_mock_data_helper';
-describe('Sidebar store', () => {
- const assignee = {
- id: 2,
- name: 'gitlab user 2',
- username: 'gitlab2',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- };
-
- const anotherAssignee = {
- id: 3,
- name: 'gitlab user 3',
- username: 'gitlab3',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- };
+const ASSIGNEE = {
+ id: 2,
+ name: 'gitlab user 2',
+ username: 'gitlab2',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+};
+
+const ANOTHER_ASSINEE = {
+ id: 3,
+ name: 'gitlab user 3',
+ username: 'gitlab3',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+};
+
+const PARTICIPANT = {
+ id: 1,
+ state: 'active',
+ username: 'marcene',
+ name: 'Allie Will',
+ web_url: 'foo.com',
+ avatar_url: 'gravatar.com/avatar/xxx',
+};
+
+const PARTICIPANT_LIST = [
+ PARTICIPANT,
+ { ...PARTICIPANT, id: 2 },
+ { ...PARTICIPANT, id: 3 },
+];
+describe('Sidebar store', () => {
beforeEach(() => {
this.store = new SidebarStore({
currentUser: {
@@ -40,23 +55,23 @@ describe('Sidebar store', () => {
});
it('adds a new assignee', () => {
- this.store.addAssignee(assignee);
+ this.store.addAssignee(ASSIGNEE);
expect(this.store.assignees.length).toEqual(1);
});
it('removes an assignee', () => {
- this.store.removeAssignee(assignee);
+ this.store.removeAssignee(ASSIGNEE);
expect(this.store.assignees.length).toEqual(0);
});
it('finds an existent assignee', () => {
let foundAssignee;
- this.store.addAssignee(assignee);
- foundAssignee = this.store.findAssignee(assignee);
+ this.store.addAssignee(ASSIGNEE);
+ foundAssignee = this.store.findAssignee(ASSIGNEE);
expect(foundAssignee).toBeDefined();
- expect(foundAssignee).toEqual(assignee);
- foundAssignee = this.store.findAssignee(anotherAssignee);
+ expect(foundAssignee).toEqual(ASSIGNEE);
+ foundAssignee = this.store.findAssignee(ANOTHER_ASSINEE);
expect(foundAssignee).toBeUndefined();
});
@@ -65,6 +80,28 @@ describe('Sidebar store', () => {
expect(this.store.assignees.length).toEqual(0);
});
+ it('sets participants data', () => {
+ expect(this.store.participants.length).toEqual(0);
+
+ this.store.setParticipantsData({
+ participants: PARTICIPANT_LIST,
+ });
+
+ expect(this.store.isFetching.participants).toEqual(false);
+ expect(this.store.participants.length).toEqual(PARTICIPANT_LIST.length);
+ });
+
+ it('sets subcriptions data', () => {
+ expect(this.store.subscribed).toEqual(null);
+
+ this.store.setSubscriptionsData({
+ subscribed: true,
+ });
+
+ expect(this.store.isFetching.subscriptions).toEqual(false);
+ expect(this.store.subscribed).toEqual(true);
+ });
+
it('set assigned data', () => {
const users = {
assignees: UsersMockHelper.createNumberRandomUsers(3),
@@ -75,6 +112,14 @@ describe('Sidebar store', () => {
expect(this.store.assignees.length).toEqual(3);
});
+ it('sets fetching state', () => {
+ expect(this.store.isFetching.participants).toEqual(true);
+
+ this.store.setFetchingState('participants', false);
+
+ expect(this.store.isFetching.participants).toEqual(false);
+ });
+
it('set time tracking data', () => {
this.store.setTimeTrackingData(Mock.time);
expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate);
@@ -90,6 +135,14 @@ describe('Sidebar store', () => {
expect(this.store.autocompleteProjects).toEqual(projects);
});
+ it('sets subscribed state', () => {
+ expect(this.store.subscribed).toEqual(null);
+
+ this.store.setSubscribedState(true);
+
+ expect(this.store.subscribed).toEqual(true);
+ });
+
it('set move to project ID', () => {
const projectId = 7;
this.store.setMoveToProjectId(projectId);
diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
new file mode 100644
index 00000000000..7adf22b0f1f
--- /dev/null
+++ b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import sidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import eventHub from '~/sidebar/event_hub';
+import mountComponent from '../helpers/vue_mount_component_helper';
+import Mock from './mock_data';
+
+describe('Sidebar Subscriptions', function () {
+ let vm;
+ let SidebarSubscriptions;
+
+ beforeEach(() => {
+ SidebarSubscriptions = Vue.extend(sidebarSubscriptions);
+ // Setup the stores, services, etc
+ // eslint-disable-next-line no-new
+ new SidebarMediator(Mock.mediator);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+ });
+
+ it('calls the mediator toggleSubscription on event', () => {
+ spyOn(SidebarMediator.prototype, 'toggleSubscription').and.returnValue(Promise.resolve());
+ vm = mountComponent(SidebarSubscriptions, {});
+
+ eventHub.$emit('toggleSubscription');
+
+ expect(SidebarMediator.prototype.toggleSubscription).toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js
new file mode 100644
index 00000000000..9b33dd02fb9
--- /dev/null
+++ b/spec/javascripts/sidebar/subscriptions_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Subscriptions', function () {
+ let vm;
+ let Subscriptions;
+
+ beforeEach(() => {
+ Subscriptions = Vue.extend(subscriptions);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('shows loading spinner when loading', () => {
+ vm = mountComponent(Subscriptions, {
+ loading: true,
+ subscribed: undefined,
+ });
+
+ expect(vm.$refs.loadingButton.loading).toBe(true);
+ expect(vm.$refs.loadingButton.label).toBeUndefined();
+ });
+
+ it('has "Subscribe" text when currently not subscribed', () => {
+ vm = mountComponent(Subscriptions, {
+ subscribed: false,
+ });
+
+ expect(vm.$refs.loadingButton.label).toBe('Subscribe');
+ });
+
+ it('has "Unsubscribe" text when currently not subscribed', () => {
+ vm = mountComponent(Subscriptions, {
+ subscribed: true,
+ });
+
+ expect(vm.$refs.loadingButton.label).toBe('Unsubscribe');
+ });
+});
diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js
index 7833bf3fb04..1c87fcec245 100644
--- a/spec/javascripts/smart_interval_spec.js
+++ b/spec/javascripts/smart_interval_spec.js
@@ -1,6 +1,6 @@
-import '~/smart_interval';
+import SmartInterval from '~/smart_interval';
-(() => {
+describe('SmartInterval', function () {
const DEFAULT_MAX_INTERVAL = 100;
const DEFAULT_STARTING_INTERVAL = 5;
const DEFAULT_SHORT_TIMEOUT = 75;
@@ -9,7 +9,7 @@ import '~/smart_interval';
function createDefaultSmartInterval(config) {
const defaultParams = {
- callback: () => {},
+ callback: () => Promise.resolve(),
startingInterval: DEFAULT_STARTING_INTERVAL,
maxInterval: DEFAULT_MAX_INTERVAL,
incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
@@ -22,158 +22,171 @@ import '~/smart_interval';
_.extend(defaultParams, config);
}
- return new gl.SmartInterval(defaultParams);
+ return new SmartInterval(defaultParams);
}
- describe('SmartInterval', function () {
- describe('Increment Interval', function () {
- beforeEach(function () {
- this.smartInterval = createDefaultSmartInterval();
- });
+ describe('Increment Interval', function () {
+ beforeEach(function () {
+ this.smartInterval = createDefaultSmartInterval();
+ });
- it('should increment the interval delay', function (done) {
- const interval = this.smartInterval;
- setTimeout(() => {
- const intervalConfig = this.smartInterval.cfg;
- const iterationCount = 4;
- const maxIntervalAfterIterations = intervalConfig.startingInterval *
- (intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40
- const currentInterval = interval.getCurrentInterval();
-
- // Provide some flexibility for performance of testing environment
- expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
- expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy();
-
- done();
- }, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40)
- });
+ it('should increment the interval delay', function (done) {
+ const interval = this.smartInterval;
+ setTimeout(() => {
+ const intervalConfig = this.smartInterval.cfg;
+ const iterationCount = 4;
+ const maxIntervalAfterIterations = intervalConfig.startingInterval *
+ (intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40
+ const currentInterval = interval.getCurrentInterval();
+
+ // Provide some flexibility for performance of testing environment
+ expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
+ expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy();
+
+ done();
+ }, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40)
+ });
- it('should not increment past maxInterval', function (done) {
- const interval = this.smartInterval;
+ it('should not increment past maxInterval', function (done) {
+ const interval = this.smartInterval;
- setTimeout(() => {
- const currentInterval = interval.getCurrentInterval();
- expect(currentInterval).toBe(interval.cfg.maxInterval);
+ setTimeout(() => {
+ const currentInterval = interval.getCurrentInterval();
+ expect(currentInterval).toBe(interval.cfg.maxInterval);
- done();
- }, DEFAULT_LONG_TIMEOUT);
- });
+ done();
+ }, DEFAULT_LONG_TIMEOUT);
});
- describe('Public methods', function () {
- beforeEach(function () {
- this.smartInterval = createDefaultSmartInterval();
+ it('does not increment while waiting for callback', function () {
+ jasmine.clock().install();
+
+ const smartInterval = createDefaultSmartInterval({
+ callback: () => new Promise($.noop),
});
- it('should cancel an interval', function (done) {
- const interval = this.smartInterval;
+ jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
+
+ const oneInterval = smartInterval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR;
+ expect(smartInterval.getCurrentInterval()).toEqual(oneInterval);
- setTimeout(() => {
- interval.cancel();
+ jasmine.clock().uninstall();
+ });
+ });
- const intervalId = interval.state.intervalId;
- const currentInterval = interval.getCurrentInterval();
- const intervalLowerLimit = interval.cfg.startingInterval;
+ describe('Public methods', function () {
+ beforeEach(function () {
+ this.smartInterval = createDefaultSmartInterval();
+ });
- expect(intervalId).toBeUndefined();
- expect(currentInterval).toBe(intervalLowerLimit);
+ it('should cancel an interval', function (done) {
+ const interval = this.smartInterval;
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
+ setTimeout(() => {
+ interval.cancel();
- it('should resume an interval', function (done) {
- const interval = this.smartInterval;
+ const intervalId = interval.state.intervalId;
+ const currentInterval = interval.getCurrentInterval();
+ const intervalLowerLimit = interval.cfg.startingInterval;
- setTimeout(() => {
- interval.cancel();
+ expect(intervalId).toBeUndefined();
+ expect(currentInterval).toBe(intervalLowerLimit);
- interval.resume();
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
- const intervalId = interval.state.intervalId;
+ it('should resume an interval', function (done) {
+ const interval = this.smartInterval;
- expect(intervalId).toBeTruthy();
+ setTimeout(() => {
+ interval.cancel();
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
+ interval.resume();
+
+ const intervalId = interval.state.intervalId;
+
+ expect(intervalId).toBeTruthy();
+
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
});
+ });
- describe('DOM Events', function () {
- beforeEach(function () {
- // This ensures DOM and DOM events are initialized for these specs.
- setFixtures('<div></div>');
+ describe('DOM Events', function () {
+ beforeEach(function () {
+ // This ensures DOM and DOM events are initialized for these specs.
+ setFixtures('<div></div>');
- this.smartInterval = createDefaultSmartInterval();
- });
+ this.smartInterval = createDefaultSmartInterval();
+ });
- it('should pause when page is not visible', function (done) {
- const interval = this.smartInterval;
+ it('should pause when page is not visible', function (done) {
+ const interval = this.smartInterval;
- setTimeout(() => {
- expect(interval.state.intervalId).toBeTruthy();
+ setTimeout(() => {
+ expect(interval.state.intervalId).toBeTruthy();
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
- expect(interval.state.intervalId).toBeUndefined();
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
+ expect(interval.state.intervalId).toBeUndefined();
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
- it('should change to the hidden interval when page is not visible', function (done) {
- const HIDDEN_INTERVAL = 1500;
- const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
+ it('should change to the hidden interval when page is not visible', function (done) {
+ const HIDDEN_INTERVAL = 1500;
+ const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
- setTimeout(() => {
- expect(interval.state.intervalId).toBeTruthy();
- expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
- interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy();
+ setTimeout(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
+ interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy();
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
- expect(interval.state.intervalId).toBeTruthy();
- expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
- it('should resume when page is becomes visible at the previous interval', function (done) {
- const interval = this.smartInterval;
+ it('should resume when page is becomes visible at the previous interval', function (done) {
+ const interval = this.smartInterval;
- setTimeout(() => {
- expect(interval.state.intervalId).toBeTruthy();
+ setTimeout(() => {
+ expect(interval.state.intervalId).toBeTruthy();
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
- expect(interval.state.intervalId).toBeUndefined();
+ expect(interval.state.intervalId).toBeUndefined();
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
+ // simulates triggering of visibilitychange event
+ interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
- expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.state.intervalId).toBeTruthy();
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
- it('should cancel on page unload', function (done) {
- const interval = this.smartInterval;
+ it('should cancel on page unload', function (done) {
+ const interval = this.smartInterval;
- setTimeout(() => {
- $(document).triggerHandler('beforeunload');
- expect(interval.state.intervalId).toBeUndefined();
- expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
- done();
- }, DEFAULT_SHORT_TIMEOUT);
- });
+ setTimeout(() => {
+ $(document).triggerHandler('beforeunload');
+ expect(interval.state.intervalId).toBeUndefined();
+ expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
+ done();
+ }, DEFAULT_SHORT_TIMEOUT);
+ });
- it('should execute callback before first interval', function () {
- const interval = createDefaultSmartInterval({ immediateExecution: true });
- expect(interval.cfg.immediateExecution).toBeFalsy();
- });
+ it('should execute callback before first interval', function () {
+ const interval = createDefaultSmartInterval({ immediateExecution: true });
+ expect(interval.cfg.immediateExecution).toBeFalsy();
});
});
-})(window.gl || (window.gl = {}));
+});
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index d4e134583c7..fd7aa332d17 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -11,6 +11,12 @@ const isHeadlessChrome = /\bHeadlessChrome\//.test(navigator.userAgent);
Vue.config.devtools = !isHeadlessChrome;
Vue.config.productionTip = false;
+let hasVueWarnings = false;
+Vue.config.warnHandler = (msg, vm, trace) => {
+ hasVueWarnings = true;
+ fail(`${msg}${trace}`);
+};
+
Vue.use(VueResource);
// enable test fixtures
@@ -34,11 +40,6 @@ window.addEventListener('unhandledrejection', (event) => {
console.error(event.reason.stack || event.reason);
});
-const checkUnhandledPromiseRejections = (done) => {
- expect(hasUnhandledPromiseRejections).toBe(false);
- done();
-};
-
// HACK: Chrome 59 disconnects if there are too many synchronous tests in a row
// because it appears to lock up the thread that communicates to Karma's socket
// This async beforeEach gets called on every spec and releases the JS thread long
@@ -47,17 +48,6 @@ const checkUnhandledPromiseRejections = (done) => {
// to run our unit tests.
beforeEach(done => done());
-beforeAll(() => {
- const origError = console.error;
- spyOn(console, 'error').and.callFake((message) => {
- if (/^\[Vue warn\]/.test(message)) {
- fail(message);
- } else {
- origError(message);
- }
- });
-});
-
const builtinVueHttpInterceptors = Vue.http.interceptors.slice();
beforeEach(() => {
@@ -80,8 +70,22 @@ testsContext.keys().forEach(function (path) {
}
});
-it('has no unhandled Promise rejections', (done) => {
- setTimeout(checkUnhandledPromiseRejections(done), 1000);
+describe('test errors', () => {
+ beforeAll((done) => {
+ if (hasUnhandledPromiseRejections || hasVueWarnings) {
+ setTimeout(done, 1000);
+ } else {
+ done();
+ }
+ });
+
+ it('has no unhandled Promise rejections', () => {
+ expect(hasUnhandledPromiseRejections).toBe(false);
+ });
+
+ it('has no Vue warnings', () => {
+ expect(hasVueWarnings).toBe(false);
+ });
});
// if we're generating coverage reports, make sure to include all files so
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index a160c86308d..29b15f3a782 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -1,72 +1,63 @@
-/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, max-len */
-/* global MockU2FDevice */
-/* global U2FAuthenticate */
-
-import '~/u2f/authenticate';
-import '~/u2f/util';
-import '~/u2f/error';
+import U2FAuthenticate from '~/u2f/authenticate';
import 'vendor/u2f';
-import './mock_u2f_device';
+import MockU2FDevice from './mock_u2f_device';
+
+describe('U2FAuthenticate', () => {
+ preloadFixtures('u2f/authenticate.html.raw');
-(function() {
- describe('U2FAuthenticate', function() {
- preloadFixtures('u2f/authenticate.html.raw');
+ beforeEach(() => {
+ loadFixtures('u2f/authenticate.html.raw');
+ this.u2fDevice = new MockU2FDevice();
+ this.container = $('#js-authenticate-u2f');
+ this.component = new U2FAuthenticate(
+ this.container,
+ '#js-login-u2f-form',
+ {
+ sign_requests: [],
+ },
+ document.querySelector('#js-login-2fa-device'),
+ document.querySelector('.js-2fa-form'),
+ );
- beforeEach(function() {
- loadFixtures('u2f/authenticate.html.raw');
- this.u2fDevice = new MockU2FDevice;
- this.container = $("#js-authenticate-u2f");
- this.component = new window.gl.U2FAuthenticate(
- this.container,
- '#js-login-u2f-form',
- {
- sign_requests: []
- },
- document.querySelector('#js-login-2fa-device'),
- document.querySelector('.js-2fa-form')
- );
+ // bypass automatic form submission within renderAuthenticated
+ spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
- // bypass automatic form submission within renderAuthenticated
- spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
+ return this.component.start();
+ });
- return this.component.start();
+ it('allows authenticating via a U2F device', () => {
+ const inProgressMessage = this.container.find('p');
+ expect(inProgressMessage.text()).toContain('Trying to communicate with your device');
+ this.u2fDevice.respondToAuthenticateRequest({
+ deviceData: 'this is data from the device',
});
- it('allows authenticating via a U2F device', function() {
- var inProgressMessage;
- inProgressMessage = this.container.find("p");
- expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
+ expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
+ });
+
+ return describe('errors', () => {
+ it('displays an error message', () => {
+ const setupButton = this.container.find('#js-login-u2f-device');
+ setupButton.trigger('click');
this.u2fDevice.respondToAuthenticateRequest({
- deviceData: "this is data from the device"
+ errorCode: 'error!',
});
- expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
+ const errorMessage = this.container.find('p');
+ return expect(errorMessage.text()).toContain('There was a problem communicating with your device');
});
- return describe("errors", function() {
- it("displays an error message", function() {
- var errorMessage, setupButton;
- setupButton = this.container.find("#js-login-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToAuthenticateRequest({
- errorCode: "error!"
- });
- errorMessage = this.container.find("p");
- return expect(errorMessage.text()).toContain("There was a problem communicating with your device");
+ return it('allows retrying authentication after an error', () => {
+ let setupButton = this.container.find('#js-login-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ errorCode: 'error!',
});
- return it("allows retrying authentication after an error", function() {
- var retryButton, setupButton;
- setupButton = this.container.find("#js-login-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToAuthenticateRequest({
- errorCode: "error!"
- });
- retryButton = this.container.find("#js-u2f-try-again");
- retryButton.trigger('click');
- setupButton = this.container.find("#js-login-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToAuthenticateRequest({
- deviceData: "this is data from the device"
- });
- expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
+ const retryButton = this.container.find('#js-u2f-try-again');
+ retryButton.trigger('click');
+ setupButton = this.container.find('#js-login-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ deviceData: 'this is data from the device',
});
+ expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
});
-}).call(window);
+});
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
index 4eb8ad3d9e4..5a1ace2b4d6 100644
--- a/spec/javascripts/u2f/mock_u2f_device.js
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -1,31 +1,28 @@
-/* eslint-disable space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-expressions, no-return-assign, no-param-reassign, max-len */
+/* eslint-disable prefer-rest-params, wrap-iife,
+no-unused-expressions, no-return-assign, no-param-reassign*/
-(function() {
- this.MockU2FDevice = (function() {
- function MockU2FDevice() {
- this.respondToAuthenticateRequest = this.respondToAuthenticateRequest.bind(this);
- this.respondToRegisterRequest = this.respondToRegisterRequest.bind(this);
- window.u2f || (window.u2f = {});
- window.u2f.register = (function(_this) {
- return function(appId, registerRequests, signRequests, callback) {
- return _this.registerCallback = callback;
- };
- })(this);
- window.u2f.sign = (function(_this) {
- return function(appId, challenges, signRequests, callback) {
- return _this.authenticateCallback = callback;
- };
- })(this);
- }
+export default class MockU2FDevice {
+ constructor() {
+ this.respondToAuthenticateRequest = this.respondToAuthenticateRequest.bind(this);
+ this.respondToRegisterRequest = this.respondToRegisterRequest.bind(this);
+ window.u2f || (window.u2f = {});
+ window.u2f.register = (function (_this) {
+ return function (appId, registerRequests, signRequests, callback) {
+ return _this.registerCallback = callback;
+ };
+ })(this);
+ window.u2f.sign = (function (_this) {
+ return function (appId, challenges, signRequests, callback) {
+ return _this.authenticateCallback = callback;
+ };
+ })(this);
+ }
- MockU2FDevice.prototype.respondToRegisterRequest = function(params) {
- return this.registerCallback(params);
- };
+ respondToRegisterRequest(params) {
+ return this.registerCallback(params);
+ }
- MockU2FDevice.prototype.respondToAuthenticateRequest = function(params) {
- return this.authenticateCallback(params);
- };
-
- return MockU2FDevice;
- })();
-}).call(window);
+ respondToAuthenticateRequest(params) {
+ return this.authenticateCallback(params);
+ }
+}
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index a445c80f2af..b0051f11362 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -1,77 +1,69 @@
-/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, max-len */
-/* global MockU2FDevice */
-/* global U2FRegister */
-
-import '~/u2f/register';
-import '~/u2f/util';
-import '~/u2f/error';
+import U2FRegister from '~/u2f/register';
import 'vendor/u2f';
-import './mock_u2f_device';
+import MockU2FDevice from './mock_u2f_device';
+
+describe('U2FRegister', () => {
+ preloadFixtures('u2f/register.html.raw');
-(function() {
- describe('U2FRegister', function() {
- preloadFixtures('u2f/register.html.raw');
+ beforeEach(() => {
+ loadFixtures('u2f/register.html.raw');
+ this.u2fDevice = new MockU2FDevice();
+ this.container = $('#js-register-u2f');
+ this.component = new U2FRegister(this.container, $('#js-register-u2f-templates'), {}, 'token');
+ return this.component.start();
+ });
- beforeEach(function() {
- loadFixtures('u2f/register.html.raw');
- this.u2fDevice = new MockU2FDevice;
- this.container = $("#js-register-u2f");
- this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token");
- return this.component.start();
+ it('allows registering a U2F device', () => {
+ const setupButton = this.container.find('#js-setup-u2f-device');
+ expect(setupButton.text()).toBe('Setup new U2F device');
+ setupButton.trigger('click');
+ const inProgressMessage = this.container.children('p');
+ expect(inProgressMessage.text()).toContain('Trying to communicate with your device');
+ this.u2fDevice.respondToRegisterRequest({
+ deviceData: 'this is data from the device',
});
- it('allows registering a U2F device', function() {
- var deviceResponse, inProgressMessage, registeredMessage, setupButton;
- setupButton = this.container.find("#js-setup-u2f-device");
- expect(setupButton.text()).toBe('Setup new U2F device');
+ const registeredMessage = this.container.find('p');
+ const deviceResponse = this.container.find('#js-device-response');
+ expect(registeredMessage.text()).toContain('Your device was successfully set up!');
+ return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
+ });
+
+ return describe('errors', () => {
+ it('doesn\'t allow the same device to be registered twice (for the same user', () => {
+ const setupButton = this.container.find('#js-setup-u2f-device');
setupButton.trigger('click');
- inProgressMessage = this.container.children("p");
- expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
this.u2fDevice.respondToRegisterRequest({
- deviceData: "this is data from the device"
+ errorCode: 4,
});
- registeredMessage = this.container.find('p');
- deviceResponse = this.container.find('#js-device-response');
- expect(registeredMessage.text()).toContain("Your device was successfully set up!");
- return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
+ const errorMessage = this.container.find('p');
+ return expect(errorMessage.text()).toContain('already been registered with us');
});
- return describe("errors", function() {
- it("doesn't allow the same device to be registered twice (for the same user", function() {
- var errorMessage, setupButton;
- setupButton = this.container.find("#js-setup-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToRegisterRequest({
- errorCode: 4
- });
- errorMessage = this.container.find("p");
- return expect(errorMessage.text()).toContain("already been registered with us");
+
+ it('displays an error message for other errors', () => {
+ const setupButton = this.container.find('#js-setup-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ errorCode: 'error!',
});
- it("displays an error message for other errors", function() {
- var errorMessage, setupButton;
- setupButton = this.container.find("#js-setup-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToRegisterRequest({
- errorCode: "error!"
- });
- errorMessage = this.container.find("p");
- return expect(errorMessage.text()).toContain("There was a problem communicating with your device");
+ const errorMessage = this.container.find('p');
+ return expect(errorMessage.text()).toContain('There was a problem communicating with your device');
+ });
+
+ return it('allows retrying registration after an error', () => {
+ let setupButton = this.container.find('#js-setup-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ errorCode: 'error!',
});
- return it("allows retrying registration after an error", function() {
- var registeredMessage, retryButton, setupButton;
- setupButton = this.container.find("#js-setup-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToRegisterRequest({
- errorCode: "error!"
- });
- retryButton = this.container.find("#U2FTryAgain");
- retryButton.trigger('click');
- setupButton = this.container.find("#js-setup-u2f-device");
- setupButton.trigger('click');
- this.u2fDevice.respondToRegisterRequest({
- deviceData: "this is data from the device"
- });
- registeredMessage = this.container.find("p");
- return expect(registeredMessage.text()).toContain("Your device was successfully set up!");
+ const retryButton = this.container.find('#U2FTryAgain');
+ retryButton.trigger('click');
+ setupButton = this.container.find('#js-setup-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ deviceData: 'this is data from the device',
});
+ const registeredMessage = this.container.find('p');
+ return expect(registeredMessage.text()).toContain('Your device was successfully set up!');
});
});
-}).call(window);
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index 690665ae12c..d7af956c9c1 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -1,148 +1,147 @@
import Vue from 'vue';
-import { statusIconEntityMap } from '~/vue_shared/ci_status_icons';
-import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline';
+import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
import mockData from '../mock_data';
-const createComponent = (mr) => {
- const Component = Vue.extend(pipelineComponent);
- return new Component({
- el: document.createElement('div'),
- propsData: { mr },
- });
-};
-
describe('MRWidgetPipeline', () => {
- describe('props', () => {
- it('should have props', () => {
- const { mr } = pipelineComponent.props;
+ let vm;
+ let Component;
- expect(mr.type instanceof Object).toBeTruthy();
- expect(mr.required).toBeTruthy();
- });
+ beforeEach(() => {
+ Component = Vue.extend(pipelineComponent);
});
- describe('components', () => {
- it('should have components added', () => {
- expect(pipelineComponent.components['pipeline-stage']).toBeDefined();
- expect(pipelineComponent.components.ciIcon).toBeDefined();
- });
+ afterEach(() => {
+ vm.$destroy();
});
describe('computed', () => {
- describe('svg', () => {
- it('should have the proper SVG icon', () => {
- const vm = createComponent({ pipeline: mockData.pipeline });
-
- expect(vm.svg).toEqual(statusIconEntityMap.icon_status_failed);
- });
- });
-
describe('hasPipeline', () => {
it('should return true when there is a pipeline', () => {
- expect(Object.keys(mockData.pipeline).length).toBeGreaterThan(0);
-
- const vm = createComponent({
+ vm = mountComponent(Component, {
pipeline: mockData.pipeline,
+ ciStatus: 'success',
+ hasCi: true,
});
- expect(vm.hasPipeline).toBeTruthy();
+ expect(vm.hasPipeline).toEqual(true);
});
it('should return false when there is no pipeline', () => {
- const vm = createComponent({
- pipeline: null,
+ vm = mountComponent(Component, {
+ pipeline: {},
});
- expect(vm.hasPipeline).toBeFalsy();
+ expect(vm.hasPipeline).toEqual(false);
});
});
describe('hasCIError', () => {
it('should return false when there is no CI error', () => {
- const vm = createComponent({
+ vm = mountComponent(Component, {
pipeline: mockData.pipeline,
- hasCI: true,
+ hasCi: true,
ciStatus: 'success',
});
- expect(vm.hasCIError).toBeFalsy();
+ expect(vm.hasCIError).toEqual(false);
});
it('should return true when there is a CI error', () => {
- const vm = createComponent({
+ vm = mountComponent(Component, {
pipeline: mockData.pipeline,
- hasCI: true,
+ hasCi: true,
ciStatus: null,
});
- expect(vm.hasCIError).toBeTruthy();
+ expect(vm.hasCIError).toEqual(true);
});
});
});
- describe('template', () => {
- let vm;
- let el;
- const { pipeline } = mockData;
- const mr = {
- hasCI: true,
- ciStatus: 'success',
- pipelineDetailedStatus: pipeline.details.status,
- pipeline,
- };
-
- beforeEach(() => {
- vm = createComponent(mr);
- el = vm.$el;
- });
+ describe('rendered output', () => {
+ it('should render CI error', () => {
+ vm = mountComponent(Component, {
+ pipeline: mockData.pipeline,
+ hasCi: true,
+ ciStatus: null,
+ });
- it('should render template elements correctly', () => {
- expect(el.classList.contains('mr-widget-heading')).toBeTruthy();
- expect(el.querySelectorAll('.ci-status-icon.ci-status-icon-success').length).toEqual(1);
- expect(el.querySelector('.pipeline-id').textContent).toContain(`#${pipeline.id}`);
- expect(el.innerText).toContain('passed');
- expect(el.querySelector('.pipeline-id').getAttribute('href')).toEqual(pipeline.path);
- expect(el.querySelectorAll('.stage-container').length).toEqual(2);
- expect(el.querySelector('.js-ci-error')).toEqual(null);
- expect(el.querySelector('.js-commit-link').getAttribute('href')).toEqual(pipeline.commit.commit_path);
- expect(el.querySelector('.js-commit-link').textContent).toContain(pipeline.commit.short_id);
- expect(el.querySelector('.js-mr-coverage').textContent).toContain(`Coverage ${pipeline.coverage}%`);
+ expect(
+ vm.$el.querySelector('.media-body').textContent.trim(),
+ ).toEqual('Could not connect to the CI server. Please check your settings and try again');
});
- it('should list single stage', (done) => {
- pipeline.details.stages.splice(0, 1);
+ describe('with a pipeline', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ pipeline: mockData.pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ });
+ });
+
+ it('should render pipeline ID', () => {
+ expect(
+ vm.$el.querySelector('.pipeline-id').textContent.trim(),
+ ).toEqual(`#${mockData.pipeline.id}`);
+ });
- Vue.nextTick(() => {
- expect(el.querySelectorAll('.stage-container button').length).toEqual(1);
- done();
+ it('should render pipeline status and commit id', () => {
+ expect(
+ vm.$el.querySelector('.media-body').textContent.trim(),
+ ).toContain(mockData.pipeline.details.status.label);
+
+ expect(
+ vm.$el.querySelector('.js-commit-link').textContent.trim(),
+ ).toEqual(mockData.pipeline.commit.short_id);
+
+ expect(
+ vm.$el.querySelector('.js-commit-link').getAttribute('href'),
+ ).toEqual(mockData.pipeline.commit.commit_path);
});
- });
- it('should not have stages when there is no stage', (done) => {
- vm.mr.pipeline.details.stages = [];
+ it('should render pipeline graph', () => {
+ expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined();
+ expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length);
+ });
- Vue.nextTick(() => {
- expect(el.querySelectorAll('.stage-container button').length).toEqual(0);
- done();
+ it('should render coverage information', () => {
+ expect(
+ vm.$el.querySelector('.media-body').textContent,
+ ).toContain(`Coverage ${mockData.pipeline.coverage}`);
});
});
- it('should not have coverage text when pipeline has no coverage info', (done) => {
- vm.mr.pipeline.coverage = null;
+ describe('without coverage', () => {
+ it('should not render a coverage', () => {
+ const mockCopy = Object.assign({}, mockData);
+ delete mockCopy.pipeline.coverage;
+
+ vm = mountComponent(Component, {
+ pipeline: mockCopy.pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ });
- Vue.nextTick(() => {
- expect(el.querySelector('.js-mr-coverage')).toEqual(null);
- done();
+ expect(
+ vm.$el.querySelector('.media-body').textContent,
+ ).not.toContain('Coverage');
});
});
- it('should show CI error when there is a CI error', (done) => {
- vm.mr.ciStatus = null;
+ describe('without a pipeline graph', () => {
+ it('should not render a pipeline graph', () => {
+ const mockCopy = Object.assign({}, mockData);
+ delete mockCopy.pipeline.details.stages;
+
+ vm = mountComponent(Component, {
+ pipeline: mockCopy.pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ });
- Vue.nextTick(() => {
- expect(el.querySelectorAll('.js-ci-error').length).toEqual(1);
- expect(el.innerText).toContain('Could not connect to the CI server');
- done();
+ expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null);
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index d7019ea408b..df3d29ee1f9 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -43,6 +43,10 @@ describe('MRWidgetReadyToMerge', () => {
vm = createComponent();
});
+ afterEach(() => {
+ vm.$destroy();
+ });
+
describe('props', () => {
it('should have props', () => {
const { mr, service } = readyToMergeComponent.props;
@@ -495,6 +499,48 @@ describe('MRWidgetReadyToMerge', () => {
});
});
+ describe('Merge controls', () => {
+ describe('when allowed to merge', () => {
+ beforeEach(() => {
+ vm = createComponent({
+ mr: { isMergeAllowed: true },
+ });
+ });
+
+ it('shows remove source branch checkbox', () => {
+ expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeDefined();
+ });
+
+ it('shows modify commit message button', () => {
+ expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined();
+ });
+
+ it('does not show message about needing to resolve items', () => {
+ expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeNull();
+ });
+ });
+
+ describe('when not allowed to merge', () => {
+ beforeEach(() => {
+ vm = createComponent({
+ mr: { isMergeAllowed: false },
+ });
+ });
+
+ it('does not show remove source branch checkbox', () => {
+ expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeNull();
+ });
+
+ it('does not show modify commit message button', () => {
+ expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeNull();
+ });
+
+ it('shows message to resolve all items before being allowed to merge', () => {
+ expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeDefined();
+ });
+ });
+ });
+
describe('Commit message area', () => {
it('when using merge commits, should show "Modify commit message" button', () => {
const customVm = createComponent({
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 0795d0aaa82..1ad7c2d8efa 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -202,7 +202,6 @@ export default {
"revert_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1",
"email_patches_path": "/root/acets-app/merge_requests/22.patch",
"plain_diff_path": "/root/acets-app/merge_requests/22.diff",
- "ci_status_path": "/root/acets-app/merge_requests/22/ci_status",
"status_path": "/root/acets-app/merge_requests/22.json",
"merge_check_path": "/root/acets-app/merge_requests/22/merge_check",
"ci_environments_status_url": "/root/acets-app/merge_requests/22/ci_environments_status",
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index e4324e91502..9e6d0aa472c 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -1,16 +1,9 @@
import Vue from 'vue';
-import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
import eventHub from '~/vue_merge_request_widget/event_hub';
import notify from '~/lib/utils/notify';
import mockData from './mock_data';
-
-const createComponent = () => {
- delete mrWidgetOptions.el; // Prevent component mounting
- gl.mrWidgetData = mockData;
- const Component = Vue.extend(mrWidgetOptions);
- return new Component();
-};
+import mountComponent from '../helpers/vue_mount_component_helper';
const returnPromise = data => new Promise((resolve) => {
resolve({
@@ -23,9 +16,16 @@ const returnPromise = data => new Promise((resolve) => {
describe('mrWidgetOptions', () => {
let vm;
+ let MrWidgetOptions;
beforeEach(() => {
- vm = createComponent();
+ // Prevent component mounting
+ delete mrWidgetOptions.el;
+
+ MrWidgetOptions = Vue.extend(mrWidgetOptions);
+ vm = mountComponent(MrWidgetOptions, {
+ mrData: { ...mockData },
+ });
});
describe('data', () => {
@@ -78,7 +78,7 @@ describe('mrWidgetOptions', () => {
});
it('should return true if there is relatedLinks in MR', () => {
- vm.mr.relatedLinks = {};
+ Vue.set(vm.mr, 'relatedLinks', {});
expect(vm.shouldRenderRelatedLinks).toBeTruthy();
});
});
@@ -121,24 +121,28 @@ describe('mrWidgetOptions', () => {
describe('initPolling', () => {
it('should call SmartInterval', () => {
- spyOn(gl, 'SmartInterval').and.returnValue({
- resume() {},
- stopTimer() {},
- });
+ spyOn(vm, 'checkStatus').and.returnValue(Promise.resolve());
+ jasmine.clock().install();
vm.initPolling();
+ expect(vm.checkStatus).not.toHaveBeenCalled();
+
+ jasmine.clock().tick(10000);
+
expect(vm.pollingInterval).toBeDefined();
- expect(gl.SmartInterval).toHaveBeenCalled();
+ expect(vm.checkStatus).toHaveBeenCalled();
+
+ jasmine.clock().uninstall();
});
});
describe('initDeploymentsPolling', () => {
it('should call SmartInterval', () => {
- spyOn(gl, 'SmartInterval');
+ spyOn(vm, 'fetchDeployments').and.returnValue(Promise.resolve());
vm.initDeploymentsPolling();
expect(vm.deploymentsInterval).toBeDefined();
- expect(gl.SmartInterval).toHaveBeenCalled();
+ expect(vm.fetchDeployments).toHaveBeenCalled();
});
});
@@ -312,28 +316,6 @@ describe('mrWidgetOptions', () => {
expect(vm.pollingInterval.stopTimer).toHaveBeenCalled();
});
});
-
- describe('createService', () => {
- it('should instantiate a Service', () => {
- const endpoints = {
- mergePath: '/nice/path',
- mergeCheckPath: '/nice/path',
- cancelAutoMergePath: '/nice/path',
- removeWIPPath: '/nice/path',
- sourceBranchPath: '/nice/path',
- ciEnvironmentsStatusPath: '/nice/path',
- statusPath: '/nice/path',
- mergeActionsContentPath: '/nice/path',
- };
-
- const serviceInstance = vm.createService(endpoints);
- const isInstanceOfMRService = serviceInstance instanceof MRWidgetService;
- expect(isInstanceOfMRService).toBe(true);
- Object.keys(serviceInstance).forEach((key) => {
- expect(serviceInstance[key]).toBeDefined();
- });
- });
- });
});
describe('components', () => {
diff --git a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
deleted file mode 100644
index e667b4b3677..00000000000
--- a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
-
-Vue.use(VueResource);
-
-describe('MRWidgetService', () => {
- const mr = {
- mergePath: './',
- mergeCheckPath: './',
- cancelAutoMergePath: './',
- removeWIPPath: './',
- sourceBranchPath: './',
- ciEnvironmentsStatusPath: './',
- statusPath: './',
- mergeActionsContentPath: './',
- isServiceStore: true,
- };
-
- it('should have store and resources created in constructor', () => {
- const service = new MRWidgetService(mr);
-
- expect(service.mergeResource).toBeDefined();
- expect(service.mergeCheckResource).toBeDefined();
- expect(service.cancelAutoMergeResource).toBeDefined();
- expect(service.removeWIPResource).toBeDefined();
- expect(service.removeSourceBranchResource).toBeDefined();
- expect(service.deploymentsResource).toBeDefined();
- expect(service.pollResource).toBeDefined();
- expect(service.mergeActionsContentResource).toBeDefined();
- });
-
- it('should have methods defined', () => {
- window.history.pushState({}, null, '/');
- const service = new MRWidgetService(mr);
-
- expect(service.merge()).toBeDefined();
- expect(service.cancelAutomaticMerge()).toBeDefined();
- expect(service.removeWIP()).toBeDefined();
- expect(service.removeSourceBranch()).toBeDefined();
- expect(service.fetchDeployments()).toBeDefined();
- expect(service.poll()).toBeDefined();
- expect(service.checkStatus()).toBeDefined();
- expect(service.fetchMergeActionsContent()).toBeDefined();
- expect(MRWidgetService.stopEnvironment()).toBeDefined();
- });
-});
diff --git a/spec/javascripts/vue_shared/ci_action_icons_spec.js b/spec/javascripts/vue_shared/ci_action_icons_spec.js
deleted file mode 100644
index 3d53a5ab24d..00000000000
--- a/spec/javascripts/vue_shared/ci_action_icons_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import getActionIcon from '~/vue_shared/ci_action_icons';
-import cancelSVG from 'icons/_icon_action_cancel.svg';
-import retrySVG from 'icons/_icon_action_retry.svg';
-import playSVG from 'icons/_icon_action_play.svg';
-import stopSVG from 'icons/_icon_action_stop.svg';
-
-describe('getActionIcon', () => {
- it('should return an empty string', () => {
- expect(getActionIcon()).toEqual('');
- });
-
- it('should return cancel svg', () => {
- expect(getActionIcon('icon_action_cancel')).toEqual(cancelSVG);
- });
-
- it('should return retry svg', () => {
- expect(getActionIcon('icon_action_retry')).toEqual(retrySVG);
- });
-
- it('should return play svg', () => {
- expect(getActionIcon('icon_action_play')).toEqual(playSVG);
- });
-
- it('should render stop svg', () => {
- expect(getActionIcon('icon_action_stop')).toEqual(stopSVG);
- });
-});
diff --git a/spec/javascripts/vue_shared/ci_status_icon_spec.js b/spec/javascripts/vue_shared/ci_status_icon_spec.js
deleted file mode 100644
index b6621d6054d..00000000000
--- a/spec/javascripts/vue_shared/ci_status_icon_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { borderlessStatusIconEntityMap, statusIconEntityMap } from '~/vue_shared/ci_status_icons';
-
-describe('CI status icons', () => {
- const statuses = [
- 'icon_status_canceled',
- 'icon_status_created',
- 'icon_status_failed',
- 'icon_status_manual',
- 'icon_status_pending',
- 'icon_status_running',
- 'icon_status_skipped',
- 'icon_status_success',
- 'icon_status_warning',
- ];
-
- it('should have a dictionary for borderless icons', () => {
- statuses.forEach((status) => {
- expect(borderlessStatusIconEntityMap[status]).toBeDefined();
- });
- });
-
- it('should have a dictionary for icons', () => {
- statuses.forEach((status) => {
- expect(statusIconEntityMap[status]).toBeDefined();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
index daed4da3e15..8762ce9903b 100644
--- a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
@@ -1,84 +1,88 @@
import Vue from 'vue';
import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('CI Badge Link Component', () => {
let CIBadge;
+ let vm;
const statuses = {
canceled: {
text: 'canceled',
label: 'canceled',
group: 'canceled',
- icon: 'icon_status_canceled',
+ icon: 'status_canceled',
details_path: 'status/canceled',
},
created: {
text: 'created',
label: 'created',
group: 'created',
- icon: 'icon_status_created',
+ icon: 'status_created',
details_path: 'status/created',
},
failed: {
text: 'failed',
label: 'failed',
group: 'failed',
- icon: 'icon_status_failed',
+ icon: 'status_failed',
details_path: 'status/failed',
},
manual: {
text: 'manual',
label: 'manual action',
group: 'manual',
- icon: 'icon_status_manual',
+ icon: 'status_manual',
details_path: 'status/manual',
},
pending: {
text: 'pending',
label: 'pending',
group: 'pending',
- icon: 'icon_status_pending',
+ icon: 'status_pending',
details_path: 'status/pending',
},
running: {
text: 'running',
label: 'running',
group: 'running',
- icon: 'icon_status_running',
+ icon: 'status_running',
details_path: 'status/running',
},
skipped: {
text: 'skipped',
label: 'skipped',
group: 'skipped',
- icon: 'icon_status_skipped',
+ icon: 'status_skipped',
details_path: 'status/skipped',
},
success_warining: {
text: 'passed',
label: 'passed',
group: 'success_with_warnings',
- icon: 'icon_status_warning',
+ icon: 'status_warning',
details_path: 'status/warning',
},
success: {
text: 'passed',
label: 'passed',
group: 'passed',
- icon: 'icon_status_success',
+ icon: 'status_success',
details_path: 'status/passed',
},
};
- it('should render each status badge', () => {
+ beforeEach(() => {
CIBadge = Vue.extend(ciBadge);
- Object.keys(statuses).map((status) => {
- const vm = new CIBadge({
- propsData: {
- status: statuses[status],
- },
- }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+ it('should render each status badge', () => {
+ Object.keys(statuses).map((status) => {
+ vm = mountComponent(CIBadge, { status: statuses[status] });
expect(vm.$el.getAttribute('href')).toEqual(statuses[status].details_path);
expect(vm.$el.textContent.trim()).toEqual(statuses[status].text);
expect(vm.$el.getAttribute('class')).toEqual(`ci-status ci-${statuses[status].group}`);
@@ -86,4 +90,9 @@ describe('CI Badge Link Component', () => {
return vm;
});
});
+
+ it('should not render label', () => {
+ vm = mountComponent(CIBadge, { status: statuses.canceled, showText: false });
+ expect(vm.$el.textContent.trim()).toEqual('');
+ });
});
diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js
new file mode 100644
index 00000000000..a22b6bd3a67
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/icon_spec.js
@@ -0,0 +1,54 @@
+import Vue from 'vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Sprite Icon Component', function () {
+ describe('Initialization', function () {
+ let icon;
+
+ beforeEach(function () {
+ const IconComponent = Vue.extend(Icon);
+
+ icon = mountComponent(IconComponent, {
+ name: 'test',
+ size: 32,
+ cssClasses: 'extraclasses',
+ });
+ });
+
+ afterEach(() => {
+ icon.$destroy();
+ });
+
+ it('should return a defined Vue component', function () {
+ expect(icon).toBeDefined();
+ });
+
+ it('should have <svg> as a child element', function () {
+ expect(icon.$el.tagName).toBe('svg');
+ });
+
+ it('should have <use> as a child element with the correct href', function () {
+ expect(icon.$el.firstChild.tagName).toBe('use');
+ expect(icon.$el.firstChild.getAttribute('xlink:href')).toBe(`${gon.sprite_icons}#test`);
+ });
+
+ it('should properly compute iconSizeClass', function () {
+ expect(icon.iconSizeClass).toBe('s32');
+ });
+
+ it('forbids invalid size prop', () => {
+ expect(icon.$options.props.size.validator(NaN)).toBeFalsy();
+ expect(icon.$options.props.size.validator(0)).toBeFalsy();
+ expect(icon.$options.props.size.validator(9001)).toBeFalsy();
+ });
+
+ it('should properly render img css', function () {
+ const classList = icon.$el.classList;
+ const containsSizeClass = classList.contains('s32');
+ const containsCustomClass = classList.contains('extraclasses');
+ expect(containsSizeClass).toBe(true);
+ expect(containsCustomClass).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
index 2cf4d8e00ed..24484796bf1 100644
--- a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
+++ b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
@@ -16,7 +16,7 @@ describe('Issue Warning Component', () => {
isLocked: true,
});
- expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-lock');
+ expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/lock$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is locked. Only project members can comment.');
});
});
@@ -27,7 +27,7 @@ describe('Issue Warning Component', () => {
isConfidential: true,
});
- expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-eye-slash');
+ expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/eye-slash$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This is a confidential issue. Your comment will not be visible to the public.');
});
});
@@ -39,7 +39,7 @@ describe('Issue Warning Component', () => {
isConfidential: true,
});
- expect(vm.$el.querySelector('i')).toBeFalsy();
+ expect(vm.$el.querySelector('.icon')).toBeFalsy();
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is confidential and locked. People without permission will never get a notification and won\'t be able to comment.');
});
});
diff --git a/spec/javascripts/vue_shared/components/loading_button_spec.js b/spec/javascripts/vue_shared/components/loading_button_spec.js
new file mode 100644
index 00000000000..49bf8ee6f7c
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/loading_button_spec.js
@@ -0,0 +1,109 @@
+import Vue from 'vue';
+import loadingButton from '~/vue_shared/components/loading_button.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const LABEL = 'Hello';
+
+describe('LoadingButton', function () {
+ let vm;
+ let LoadingButton;
+
+ beforeEach(() => {
+ LoadingButton = Vue.extend(loadingButton);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('loading spinner', () => {
+ it('shown when loading', () => {
+ vm = mountComponent(LoadingButton, {
+ loading: true,
+ });
+
+ expect(vm.$el.querySelector('.js-loading-button-icon')).toBeDefined();
+ });
+ });
+
+ describe('disabled state', () => {
+ it('disabled when loading', () => {
+ vm = mountComponent(LoadingButton, {
+ loading: true,
+ });
+
+ expect(vm.$el.disabled).toEqual(true);
+ });
+
+ it('not disabled when normal', () => {
+ vm = mountComponent(LoadingButton, {
+ loading: false,
+ });
+
+ expect(vm.$el.disabled).toEqual(false);
+ });
+ });
+
+ describe('label', () => {
+ it('shown when normal', () => {
+ vm = mountComponent(LoadingButton, {
+ loading: false,
+ label: LABEL,
+ });
+ const label = vm.$el.querySelector('.js-loading-button-label');
+
+ expect(label.textContent.trim()).toEqual(LABEL);
+ });
+
+ it('shown when loading', () => {
+ vm = mountComponent(LoadingButton, {
+ loading: true,
+ label: LABEL,
+ });
+ const label = vm.$el.querySelector('.js-loading-button-label');
+
+ expect(label.textContent.trim()).toEqual(LABEL);
+ });
+ });
+
+ describe('container class', () => {
+ it('should default to btn btn-align-content', () => {
+ vm = mountComponent(LoadingButton, {});
+ expect(vm.$el.classList.contains('btn')).toEqual(true);
+ expect(vm.$el.classList.contains('btn-align-content')).toEqual(true);
+ });
+
+ it('should be configurable through props', () => {
+ vm = mountComponent(LoadingButton, {
+ containerClass: 'test-class',
+ });
+ expect(vm.$el.classList.contains('btn')).toEqual(false);
+ expect(vm.$el.classList.contains('btn-align-content')).toEqual(false);
+ expect(vm.$el.classList.contains('test-class')).toEqual(true);
+ });
+ });
+
+ describe('click callback prop', () => {
+ it('calls given callback when normal', () => {
+ vm = mountComponent(LoadingButton, {
+ loading: false,
+ });
+ spyOn(vm, '$emit');
+
+ vm.$el.click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('click', jasmine.any(Object));
+ });
+
+ it('does not call given callback when disabled because of loading', () => {
+ vm = mountComponent(LoadingButton, {
+ loading: true,
+ });
+ spyOn(vm, '$emit');
+
+ vm.$el.click();
+
+ expect(vm.$emit).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js
index 60a5c2ae74e..24209be83fe 100644
--- a/spec/javascripts/vue_shared/components/markdown/field_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js
@@ -1,6 +1,12 @@
import Vue from 'vue';
import fieldComponent from '~/vue_shared/components/markdown/field.vue';
+function assertMarkdownTabs(isWrite, writeLink, previewLink, vm) {
+ expect(writeLink.parentNode.classList.contains('active')).toEqual(isWrite);
+ expect(previewLink.parentNode.classList.contains('active')).toEqual(!isWrite);
+ expect(vm.$el.querySelector('.md-preview').style.display).toEqual(isWrite ? 'none' : '');
+}
+
describe('Markdown field component', () => {
let vm;
@@ -39,19 +45,23 @@ describe('Markdown field component', () => {
describe('markdown preview', () => {
let previewLink;
+ let writeLink;
beforeEach(() => {
spyOn(Vue.http, 'post').and.callFake(() => new Promise((resolve) => {
- resolve({
- json() {
- return {
- body: '<p>markdown preview</p>',
- };
- },
+ setTimeout(() => {
+ resolve({
+ json() {
+ return {
+ body: '<p>markdown preview</p>',
+ };
+ },
+ });
});
}));
- previewLink = vm.$el.querySelector('.nav-links li:nth-child(2) a');
+ previewLink = vm.$el.querySelector('.nav-links .js-preview-link');
+ writeLink = vm.$el.querySelector('.nav-links .js-write-link');
});
it('sets preview link as active', (done) => {
@@ -103,6 +113,23 @@ describe('Markdown field component', () => {
done();
}, 0);
});
+
+ it('clicking already active write or preview link does nothing', (done) => {
+ writeLink.click();
+ Vue.nextTick()
+ .then(() => assertMarkdownTabs(true, writeLink, previewLink, vm))
+ .then(() => writeLink.click())
+ .then(() => Vue.nextTick())
+ .then(() => assertMarkdownTabs(true, writeLink, previewLink, vm))
+ .then(() => previewLink.click())
+ .then(() => Vue.nextTick())
+ .then(() => assertMarkdownTabs(false, writeLink, previewLink, vm))
+ .then(() => previewLink.click())
+ .then(() => Vue.nextTick())
+ .then(() => assertMarkdownTabs(false, writeLink, previewLink, vm))
+ .then(done)
+ .catch(done.fail);
+ });
});
describe('markdown buttons', () => {
diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js
index 7110ff36937..edebd822295 100644
--- a/spec/javascripts/vue_shared/components/markdown/header_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/header_spec.js
@@ -43,11 +43,13 @@ describe('Markdown field header component', () => {
it('emits toggle markdown event when clicking preview', () => {
spyOn(vm, '$emit');
- vm.$el.querySelector('li:nth-child(2) a').click();
+ vm.$el.querySelector('.js-preview-link').click();
- expect(
- vm.$emit,
- ).toHaveBeenCalledWith('toggle-markdown');
+ expect(vm.$emit).toHaveBeenCalledWith('preview-markdown');
+
+ vm.$el.querySelector('.js-write-link').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('write-markdown');
});
it('blurs preview link after click', (done) => {
diff --git a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
new file mode 100644
index 00000000000..818ef0af3c2
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('toolbar', () => {
+ let vm;
+ const Toolbar = Vue.extend(toolbar);
+ const props = {
+ markdownDocsPath: '',
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('user can attach file', () => {
+ beforeEach(() => {
+ vm = mountComponent(Toolbar, props);
+ });
+
+ it('should render uploading-container', () => {
+ expect(vm.$el.querySelector('.uploading-container')).not.toBeNull();
+ });
+ });
+
+ describe('user cannot attach file', () => {
+ beforeEach(() => {
+ vm = mountComponent(Toolbar, Object.assign({}, props, {
+ canAttachFile: false,
+ }));
+ });
+
+ it('should not render uploading-container', () => {
+ expect(vm.$el.querySelector('.uploading-container')).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js b/spec/javascripts/vue_shared/components/navigation_tabs_spec.js
new file mode 100644
index 00000000000..78e7d747b92
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/navigation_tabs_spec.js
@@ -0,0 +1,61 @@
+import Vue from 'vue';
+import navigationTabs from '~/vue_shared/components/navigation_tabs.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('navigation tabs component', () => {
+ let vm;
+ let Component;
+ let data;
+
+ beforeEach(() => {
+ data = [
+ {
+ name: 'All',
+ scope: 'all',
+ count: 1,
+ isActive: true,
+ },
+ {
+ name: 'Pending',
+ scope: 'pending',
+ count: 0,
+ isActive: false,
+ },
+ {
+ name: 'Running',
+ scope: 'running',
+ isActive: false,
+ },
+ ];
+
+ Component = Vue.extend(navigationTabs);
+ vm = mountComponent(Component, { tabs: data, scope: 'pipelines' });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render tabs', () => {
+ expect(vm.$el.querySelectorAll('li').length).toEqual(data.length);
+ });
+
+ it('should render active tab', () => {
+ expect(vm.$el.querySelector('.active .js-pipelines-tab-all')).toBeDefined();
+ });
+
+ it('should render badge', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all .badge').textContent.trim()).toEqual('1');
+ expect(vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim()).toEqual('0');
+ });
+
+ it('should not render badge', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-running .badge')).toEqual(null);
+ });
+
+ it('should trigger onTabClick', () => {
+ spyOn(vm, '$emit');
+ vm.$el.querySelector('.js-pipelines-tab-pending').click();
+ expect(vm.$emit).toHaveBeenCalledWith('onChangeTab', 'pending');
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
new file mode 100644
index 00000000000..ba8ab0b2cd7
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
+import store from '~/notes/stores';
+import { userDataMock } from '../../../notes/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/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js
new file mode 100644
index 00000000000..7b8e6c330c2
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('placeholder system note component', () => {
+ let PlaceholderSystemNote;
+ let vm;
+
+ beforeEach(() => {
+ PlaceholderSystemNote = Vue.extend(placeholderSystemNote);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render system note placeholder with plain text', () => {
+ vm = mountComponent(PlaceholderSystemNote, {
+ note: { body: '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/vue_shared/components/notes/system_note_spec.js b/spec/javascripts/vue_shared/components/notes/system_note_spec.js
new file mode 100644
index 00000000000..36aaf0a6c2e
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/notes/system_note_spec.js
@@ -0,0 +1,57 @@
+import Vue from 'vue';
+import issueSystemNote from '~/vue_shared/components/notes/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();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ 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/vue_shared/components/pikaday_spec.js b/spec/javascripts/vue_shared/components/pikaday_spec.js
new file mode 100644
index 00000000000..47af9534737
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/pikaday_spec.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import datePicker from '~/vue_shared/components/pikaday.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('datePicker', () => {
+ let vm;
+ beforeEach(() => {
+ const DatePicker = Vue.extend(datePicker);
+ vm = mountComponent(DatePicker, {
+ label: 'label',
+ });
+ });
+
+ it('should render label text', () => {
+ expect(vm.$el.querySelector('.dropdown-toggle-text').innerText.trim()).toEqual('label');
+ });
+
+ it('should show calendar', () => {
+ expect(vm.$el.querySelector('.pika-single')).toBeDefined();
+ });
+
+ it('should toggle when dropdown is clicked', () => {
+ const hidePicker = jasmine.createSpy();
+ vm.$on('hidePicker', hidePicker);
+
+ vm.$el.querySelector('.dropdown-menu-toggle').click();
+ expect(hidePicker).toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
new file mode 100644
index 00000000000..cce53193870
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('collapsedCalendarIcon', () => {
+ let vm;
+ beforeEach(() => {
+ const CollapsedCalendarIcon = Vue.extend(collapsedCalendarIcon);
+ vm = mountComponent(CollapsedCalendarIcon, {
+ containerClass: 'test-class',
+ text: 'text',
+ showIcon: false,
+ });
+ });
+
+ it('should add class to container', () => {
+ expect(vm.$el.classList.contains('test-class')).toEqual(true);
+ });
+
+ it('should hide calendar icon if showIcon', () => {
+ expect(vm.$el.querySelector('.fa-calendar')).toBeNull();
+ });
+
+ it('should render text', () => {
+ expect(vm.$el.querySelector('span').innerText.trim()).toEqual('text');
+ });
+
+ it('should emit click event when container is clicked', () => {
+ const click = jasmine.createSpy();
+ vm.$on('click', click);
+
+ vm.$el.click();
+ expect(click).toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
new file mode 100644
index 00000000000..20363e78094
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
@@ -0,0 +1,91 @@
+import Vue from 'vue';
+import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('collapsedGroupedDatePicker', () => {
+ let vm;
+ beforeEach(() => {
+ const CollapsedGroupedDatePicker = Vue.extend(collapsedGroupedDatePicker);
+ vm = mountComponent(CollapsedGroupedDatePicker, {
+ showToggleSidebar: true,
+ });
+ });
+
+ it('should render toggle sidebar if showToggleSidebar', (done) => {
+ expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeDefined();
+
+ vm.showToggleSidebar = false;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeNull();
+ done();
+ });
+ });
+
+ it('toggleCollapse events', () => {
+ const toggleCollapse = jasmine.createSpy();
+
+ beforeEach((done) => {
+ vm.minDate = new Date('07/17/2016');
+ Vue.nextTick(done);
+ });
+
+ it('should emit when sidebar is toggled', () => {
+ vm.$el.querySelector('.gutter-toggle').click();
+ expect(toggleCollapse).toHaveBeenCalled();
+ });
+
+ it('should emit when collapsed-calendar-icon is clicked', () => {
+ vm.$el.querySelector('.sidebar-collapsed-icon').click();
+ expect(toggleCollapse).toHaveBeenCalled();
+ });
+ });
+
+ describe('minDate and maxDate', () => {
+ beforeEach((done) => {
+ vm.minDate = new Date('07/17/2016');
+ vm.maxDate = new Date('07/17/2017');
+ Vue.nextTick(done);
+ });
+
+ it('should render both collapsed-calendar-icon', () => {
+ const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+ expect(icons.length).toEqual(2);
+ expect(icons[0].innerText.trim()).toEqual('Jul 17 2016');
+ expect(icons[1].innerText.trim()).toEqual('Jul 17 2017');
+ });
+ });
+
+ describe('minDate', () => {
+ beforeEach((done) => {
+ vm.minDate = new Date('07/17/2016');
+ Vue.nextTick(done);
+ });
+
+ it('should render minDate in collapsed-calendar-icon', () => {
+ const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+ expect(icons.length).toEqual(1);
+ expect(icons[0].innerText.trim()).toEqual('From Jul 17 2016');
+ });
+ });
+
+ describe('maxDate', () => {
+ beforeEach((done) => {
+ vm.maxDate = new Date('07/17/2017');
+ Vue.nextTick(done);
+ });
+
+ it('should render maxDate in collapsed-calendar-icon', () => {
+ const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+ expect(icons.length).toEqual(1);
+ expect(icons[0].innerText.trim()).toEqual('Until Jul 17 2017');
+ });
+ });
+
+ describe('no dates', () => {
+ it('should render None', () => {
+ const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+ expect(icons.length).toEqual(1);
+ expect(icons[0].innerText.trim()).toEqual('None');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
new file mode 100644
index 00000000000..926e11b4d30
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
@@ -0,0 +1,117 @@
+import Vue from 'vue';
+import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('sidebarDatePicker', () => {
+ let vm;
+ beforeEach(() => {
+ const SidebarDatePicker = Vue.extend(sidebarDatePicker);
+ vm = mountComponent(SidebarDatePicker, {
+ label: 'label',
+ isLoading: true,
+ });
+ });
+
+ it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => {
+ const toggleCollapse = jasmine.createSpy();
+ vm.$on('toggleCollapse', toggleCollapse);
+
+ vm.$el.querySelector('.issuable-sidebar-header .gutter-toggle').click();
+ expect(toggleCollapse).toHaveBeenCalled();
+ });
+
+ it('should render collapsed-calendar-icon', () => {
+ expect(vm.$el.querySelector('.sidebar-collapsed-icon')).toBeDefined();
+ });
+
+ it('should render label', () => {
+ expect(vm.$el.querySelector('.title').innerText.trim()).toEqual('label');
+ });
+
+ it('should render loading-icon when isLoading', () => {
+ expect(vm.$el.querySelector('.fa-spin')).toBeDefined();
+ });
+
+ it('should render value when not editing', () => {
+ expect(vm.$el.querySelector('.value-content')).toBeDefined();
+ });
+
+ it('should render None if there is no selectedDate', () => {
+ expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None');
+ });
+
+ it('should render date-picker when editing', (done) => {
+ vm.editing = true;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.pika-label')).toBeDefined();
+ done();
+ });
+ });
+
+ describe('editable', () => {
+ beforeEach((done) => {
+ vm.editable = true;
+ Vue.nextTick(done);
+ });
+
+ it('should render edit button', () => {
+ expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit');
+ });
+
+ it('should enable editing when edit button is clicked', (done) => {
+ vm.isLoading = false;
+ Vue.nextTick(() => {
+ vm.$el.querySelector('.title .btn-blank').click();
+ expect(vm.editing).toEqual(true);
+ done();
+ });
+ });
+ });
+
+ it('should render date if selectedDate', (done) => {
+ vm.selectedDate = new Date('07/07/2017');
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017');
+ done();
+ });
+ });
+
+ describe('selectedDate and editable', () => {
+ beforeEach((done) => {
+ vm.selectedDate = new Date('07/07/2017');
+ vm.editable = true;
+ Vue.nextTick(done);
+ });
+
+ it('should render remove button if selectedDate and editable', () => {
+ expect(vm.$el.querySelector('.value-content .btn-blank').innerText.trim()).toEqual('remove');
+ });
+
+ it('should emit saveDate when remove button is clicked', () => {
+ const saveDate = jasmine.createSpy();
+ vm.$on('saveDate', saveDate);
+
+ vm.$el.querySelector('.value-content .btn-blank').click();
+ expect(saveDate).toHaveBeenCalled();
+ });
+ });
+
+ describe('showToggleSidebar', () => {
+ beforeEach((done) => {
+ vm.showToggleSidebar = true;
+ Vue.nextTick(done);
+ });
+
+ it('should render toggle-sidebar when showToggleSidebar', () => {
+ expect(vm.$el.querySelector('.title .gutter-toggle')).toBeDefined();
+ });
+
+ it('should emit toggleCollapse when toggle sidebar is clicked', () => {
+ const toggleCollapse = jasmine.createSpy();
+ vm.$on('toggleCollapse', toggleCollapse);
+
+ vm.$el.querySelector('.title .gutter-toggle').click();
+ expect(toggleCollapse).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js
new file mode 100644
index 00000000000..752a9e89d50
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('toggleSidebar', () => {
+ let vm;
+ beforeEach(() => {
+ const ToggleSidebar = Vue.extend(toggleSidebar);
+ vm = mountComponent(ToggleSidebar, {
+ collapsed: true,
+ });
+ });
+
+ it('should render << when collapsed', () => {
+ expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-left')).toEqual(true);
+ });
+
+ it('should render >> when collapsed', () => {
+ vm.collapsed = false;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-right')).toEqual(true);
+ });
+ });
+
+ it('should emit toggle event when button clicked', () => {
+ const toggle = jasmine.createSpy();
+ vm.$on('toggle', toggle);
+ vm.$el.click();
+
+ expect(toggle).toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js
new file mode 100644
index 00000000000..a5db0b2c59e
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js
@@ -0,0 +1,49 @@
+import Vue from 'vue';
+import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Skeleton loading container', () => {
+ let vm;
+
+ beforeEach(() => {
+ const component = Vue.extend(skeletonLoadingContainer);
+ vm = mountComponent(component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders 6 skeleton lines by default', () => {
+ expect(vm.$el.querySelector('.skeleton-line-6')).not.toBeNull();
+ });
+
+ it('renders in full mode by default', () => {
+ expect(vm.$el.classList.contains('animation-container-small')).toBeFalsy();
+ });
+
+ describe('small', () => {
+ beforeEach((done) => {
+ vm.small = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('renders in small mode', () => {
+ expect(vm.$el.classList.contains('animation-container-small')).toBeTruthy();
+ });
+ });
+
+ describe('lines', () => {
+ beforeEach((done) => {
+ vm.lines = 5;
+
+ Vue.nextTick(done);
+ });
+
+ it('renders 5 lines', () => {
+ expect(vm.$el.querySelector('.skeleton-line-5')).not.toBeNull();
+ expect(vm.$el.querySelector('.skeleton-line-6')).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
new file mode 100644
index 00000000000..aa93134f2dd
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -0,0 +1,84 @@
+import Vue from 'vue';
+import { placeholderImage } from '~/lazy_loader';
+import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+const DEFAULT_PROPS = {
+ size: 99,
+ imgSrc: 'myavatarurl.com',
+ imgAlt: 'mydisplayname',
+ cssClasses: 'myextraavatarclass',
+ tooltipText: 'tooltip text',
+ tooltipPlacement: 'bottom',
+};
+
+describe('User Avatar Image Component', function () {
+ let vm;
+ let UserAvatarImage;
+
+ beforeEach(() => {
+ UserAvatarImage = Vue.extend(userAvatarImage);
+ });
+
+ describe('Initialization', function () {
+ beforeEach(function () {
+ vm = mountComponent(UserAvatarImage, {
+ ...DEFAULT_PROPS,
+ }).$mount();
+ });
+
+ it('should return a defined Vue component', function () {
+ expect(vm).toBeDefined();
+ });
+
+ it('should have <img> as a child element', function () {
+ expect(vm.$el.tagName).toBe('IMG');
+ expect(vm.$el.getAttribute('src')).toBe(DEFAULT_PROPS.imgSrc);
+ expect(vm.$el.getAttribute('data-src')).toBe(DEFAULT_PROPS.imgSrc);
+ expect(vm.$el.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt);
+ });
+
+ it('should properly compute tooltipContainer', function () {
+ expect(vm.tooltipContainer).toBe('body');
+ });
+
+ it('should properly render tooltipContainer', function () {
+ expect(vm.$el.getAttribute('data-container')).toBe('body');
+ });
+
+ it('should properly compute avatarSizeClass', function () {
+ expect(vm.avatarSizeClass).toBe('s99');
+ });
+
+ it('should properly render img css', function () {
+ const classList = vm.$el.classList;
+ const containsAvatar = classList.contains('avatar');
+ const containsSizeClass = classList.contains('s99');
+ const containsCustomClass = classList.contains(DEFAULT_PROPS.cssClasses);
+ const lazyClass = classList.contains('lazy');
+
+ expect(containsAvatar).toBe(true);
+ expect(containsSizeClass).toBe(true);
+ expect(containsCustomClass).toBe(true);
+ expect(lazyClass).toBe(false);
+ });
+ });
+
+ describe('Initialization when lazy', function () {
+ beforeEach(function () {
+ vm = mountComponent(UserAvatarImage, {
+ ...DEFAULT_PROPS,
+ lazy: true,
+ }).$mount();
+ });
+
+ it('should add lazy attributes', function () {
+ const classList = vm.$el.classList;
+ const lazyClass = classList.contains('lazy');
+
+ expect(lazyClass).toBe(true);
+ expect(vm.$el.getAttribute('src')).toBe(placeholderImage);
+ expect(vm.$el.getAttribute('data-src')).toBe(DEFAULT_PROPS.imgSrc);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
new file mode 100644
index 00000000000..8450ad9dbcb
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -0,0 +1,89 @@
+import Vue from 'vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+
+describe('User Avatar Link Component', function () {
+ beforeEach(function () {
+ this.propsData = {
+ linkHref: 'myavatarurl.com',
+ imgSize: 99,
+ imgSrc: 'myavatarurl.com',
+ imgAlt: 'mydisplayname',
+ imgCssClasses: 'myextraavatarclass',
+ tooltipText: 'tooltip text',
+ tooltipPlacement: 'bottom',
+ username: 'username',
+ };
+
+ const UserAvatarLinkComponent = Vue.extend(UserAvatarLink);
+
+ this.userAvatarLink = new UserAvatarLinkComponent({
+ propsData: this.propsData,
+ }).$mount();
+
+ this.userAvatarImage = this.userAvatarLink.$children[0];
+ });
+
+ it('should return a defined Vue component', function () {
+ expect(this.userAvatarLink).toBeDefined();
+ });
+
+ it('should have user-avatar-image registered as child component', function () {
+ expect(this.userAvatarLink.$options.components.userAvatarImage).toBeDefined();
+ });
+
+ it('user-avatar-link should have user-avatar-image as child component', function () {
+ expect(this.userAvatarImage).toBeDefined();
+ });
+
+ it('should render <a> as a child element', function () {
+ expect(this.userAvatarLink.$el.tagName).toBe('A');
+ });
+
+ it('should have <img> as a child element', function () {
+ expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull();
+ });
+
+ it('should return neccessary props as defined', function () {
+ _.each(this.propsData, (val, key) => {
+ expect(this.userAvatarLink[key]).toBeDefined();
+ });
+ });
+
+ describe('no username', function () {
+ beforeEach(function (done) {
+ this.userAvatarLink.username = '';
+
+ Vue.nextTick(done);
+ });
+
+ it('should only render image tag in link', function () {
+ const childElements = this.userAvatarLink.$el.childNodes;
+ expect(childElements[0].tagName).toBe('IMG');
+
+ // Vue will render the hidden component as <!---->
+ expect(childElements[1].tagName).toBeUndefined();
+ });
+
+ it('should render avatar image tooltip', function () {
+ expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(this.propsData.tooltipText);
+ });
+ });
+
+ describe('username', function () {
+ it('should not render avatar image tooltip', function () {
+ expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual('');
+ });
+
+ it('should render username prop in <span>', function () {
+ expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual(this.propsData.username);
+ });
+
+ it('should render text tooltip for <span>', function () {
+ expect(this.userAvatarLink.$el.querySelector('span').dataset.originalTitle).toEqual(this.propsData.tooltipText);
+ });
+
+ it('should render text tooltip placement for <span>', function () {
+ expect(this.userAvatarLink.$el.querySelector('span').getAttribute('tooltip-placement')).toEqual(this.propsData.tooltipPlacement);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js
index b8d639ffbec..b8d639ffbec 100644
--- a/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js
diff --git a/spec/javascripts/vue_shared/components/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar_image_spec.js
deleted file mode 100644
index 8daa7610274..00000000000
--- a/spec/javascripts/vue_shared/components/user_avatar_image_spec.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import Vue from 'vue';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-
-const UserAvatarImageComponent = Vue.extend(UserAvatarImage);
-
-describe('User Avatar Image Component', function () {
- describe('Initialization', function () {
- beforeEach(function () {
- this.propsData = {
- size: 99,
- imgSrc: 'myavatarurl.com',
- imgAlt: 'mydisplayname',
- cssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
- };
-
- this.userAvatarImage = new UserAvatarImageComponent({
- propsData: this.propsData,
- }).$mount();
- });
-
- it('should return a defined Vue component', function () {
- expect(this.userAvatarImage).toBeDefined();
- });
-
- it('should have <img> as a child element', function () {
- expect(this.userAvatarImage.$el.tagName).toBe('IMG');
- });
-
- it('should properly compute tooltipContainer', function () {
- expect(this.userAvatarImage.tooltipContainer).toBe('body');
- });
-
- it('should properly render tooltipContainer', function () {
- expect(this.userAvatarImage.$el.getAttribute('data-container')).toBe('body');
- });
-
- it('should properly compute avatarSizeClass', function () {
- expect(this.userAvatarImage.avatarSizeClass).toBe('s99');
- });
-
- it('should properly render img css', function () {
- const classList = this.userAvatarImage.$el.classList;
- const containsAvatar = classList.contains('avatar');
- const containsSizeClass = classList.contains('s99');
- const containsCustomClass = classList.contains('myextraavatarclass');
-
- expect(containsAvatar).toBe(true);
- expect(containsSizeClass).toBe(true);
- expect(containsCustomClass).toBe(true);
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js
deleted file mode 100644
index 52e450e9ba5..00000000000
--- a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import Vue from 'vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-
-describe('User Avatar Link Component', function () {
- beforeEach(function () {
- this.propsData = {
- linkHref: 'myavatarurl.com',
- imgSize: 99,
- imgSrc: 'myavatarurl.com',
- imgAlt: 'mydisplayname',
- imgCssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
- };
-
- const UserAvatarLinkComponent = Vue.extend(UserAvatarLink);
-
- this.userAvatarLink = new UserAvatarLinkComponent({
- propsData: this.propsData,
- }).$mount();
-
- this.userAvatarImage = this.userAvatarLink.$children[0];
- });
-
- it('should return a defined Vue component', function () {
- expect(this.userAvatarLink).toBeDefined();
- });
-
- it('should have user-avatar-image registered as child component', function () {
- expect(this.userAvatarLink.$options.components.userAvatarImage).toBeDefined();
- });
-
- it('user-avatar-link should have user-avatar-image as child component', function () {
- expect(this.userAvatarImage).toBeDefined();
- });
-
- it('should render <a> as a child element', function () {
- expect(this.userAvatarLink.$el.tagName).toBe('A');
- });
-
- it('should have <img> as a child element', function () {
- expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull();
- });
-
- it('should return neccessary props as defined', function () {
- _.each(this.propsData, (val, key) => {
- expect(this.userAvatarLink[key]).toBeDefined();
- });
- });
-});
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index bd18f79cea7..45a0bb0650f 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -1,78 +1,93 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, max-len */
-/* global Dropzone */
/* global Mousetrap */
-
+import Dropzone from 'dropzone';
import ZenMode from '~/zen_mode';
-(function() {
- var enterZen, escapeKeydown, exitZen;
-
- describe('ZenMode', function() {
- var fixtureName = 'merge_requests/merge_request_with_comment.html.raw';
- preloadFixtures(fixtureName);
- beforeEach(function() {
- loadFixtures(fixtureName);
- spyOn(Dropzone, 'forElement').and.callFake(function() {
- return {
- enable: function() {
- return true;
- }
- };
- // Stub Dropzone.forElement(...).enable()
- });
- this.zen = new ZenMode();
- // Set this manually because we can't actually scroll the window
- return this.zen.scroll_position = 456;
+describe('ZenMode', () => {
+ let zen;
+ const fixtureName = 'merge_requests/merge_request_with_comment.html.raw';
+
+ preloadFixtures(fixtureName);
+
+ function enterZen() {
+ $('.notes-form .js-zen-enter').click();
+ }
+
+ function exitZen() {
+ $('.notes-form .js-zen-leave').click();
+ }
+
+ function escapeKeydown() {
+ $('.notes-form textarea').trigger($.Event('keydown', {
+ keyCode: 27,
+ }));
+ }
+
+ beforeEach(() => {
+ loadFixtures(fixtureName);
+
+ spyOn(Dropzone, 'forElement').and.callFake(() => ({
+ enable: () => true,
+ }));
+ zen = new ZenMode();
+
+ // Set this manually because we can't actually scroll the window
+ zen.scroll_position = 456;
+ });
+
+ describe('on enter', () => {
+ it('pauses Mousetrap', () => {
+ spyOn(Mousetrap, 'pause');
+ enterZen();
+ expect(Mousetrap.pause).toHaveBeenCalled();
+ });
+
+ it('removes textarea styling', () => {
+ $('.notes-form textarea').attr('style', 'height: 400px');
+ enterZen();
+ expect($('.notes-form textarea')).not.toHaveAttr('style');
});
- describe('on enter', function() {
- it('pauses Mousetrap', function() {
- spyOn(Mousetrap, 'pause');
- enterZen();
- return expect(Mousetrap.pause).toHaveBeenCalled();
- });
- return it('removes textarea styling', function() {
- $('.notes-form textarea').attr('style', 'height: 400px');
- enterZen();
- return expect($('.notes-form textarea')).not.toHaveAttr('style');
- });
+ });
+
+ describe('in use', () => {
+ beforeEach(enterZen);
+
+ it('exits on Escape', () => {
+ escapeKeydown();
+ expect($('.notes-form .zen-backdrop')).not.toHaveClass('fullscreen');
});
- describe('in use', function() {
- beforeEach(function() {
- return enterZen();
- });
- return it('exits on Escape', function() {
- escapeKeydown();
- return expect($('.notes-form .zen-backdrop')).not.toHaveClass('fullscreen');
- });
+ });
+
+ describe('on exit', () => {
+ beforeEach(enterZen);
+
+ it('unpauses Mousetrap', () => {
+ spyOn(Mousetrap, 'unpause');
+ exitZen();
+ expect(Mousetrap.unpause).toHaveBeenCalled();
});
- return describe('on exit', function() {
- beforeEach(function() {
- return enterZen();
- });
- it('unpauses Mousetrap', function() {
- spyOn(Mousetrap, 'unpause');
- exitZen();
- return expect(Mousetrap.unpause).toHaveBeenCalled();
- });
- return it('restores the scroll position', function() {
- spyOn(this.zen, 'scrollTo');
- exitZen();
- return expect(this.zen.scrollTo).toHaveBeenCalled();
- });
+
+ it('restores the scroll position', () => {
+ spyOn(zen, 'scrollTo');
+ exitZen();
+ expect(zen.scrollTo).toHaveBeenCalled();
});
});
- enterZen = function() {
- return $('.notes-form .js-zen-enter').click();
- };
+ describe('enabling dropzone', () => {
+ beforeEach(() => {
+ enterZen();
+ });
- exitZen = function() {
- return $('.notes-form .js-zen-leave').click();
- };
+ it('should not call dropzone if element is not dropzone valid', () => {
+ $('.div-dropzone').addClass('js-invalid-dropzone');
+ exitZen();
+ expect(Dropzone.forElement).not.toHaveBeenCalled();
+ });
- escapeKeydown = function() {
- return $('.notes-form textarea').trigger($.Event('keydown', {
- keyCode: 27
- }));
- };
-}).call(window);
+ it('should call dropzone if element is dropzone valid', () => {
+ $('.div-dropzone').removeClass('js-invalid-dropzone');
+ exitZen();
+ expect(Dropzone.forElement).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/lib/additional_email_headers_interceptor_spec.rb b/spec/lib/additional_email_headers_interceptor_spec.rb
index 580450eef1e..b5c1a360ba9 100644
--- a/spec/lib/additional_email_headers_interceptor_spec.rb
+++ b/spec/lib/additional_email_headers_interceptor_spec.rb
@@ -1,12 +1,29 @@
require 'spec_helper'
describe AdditionalEmailHeadersInterceptor do
- it 'adds Auto-Submitted header' do
- mail = ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello').deliver
+ let(:mail) do
+ ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello')
+ end
+
+ before do
+ mail.deliver_now
+ end
+ it 'adds Auto-Submitted header' do
expect(mail.header['To'].value).to eq('test@mail.com')
expect(mail.header['From'].value).to eq('info@mail.com')
expect(mail.header['Auto-Submitted'].value).to eq('auto-generated')
expect(mail.header['X-Auto-Response-Suppress'].value).to eq('All')
end
+
+ context 'when the same mail object is sent twice' do
+ before do
+ mail.deliver_now
+ end
+
+ it 'does not add the Auto-Submitted header twice' do
+ expect(mail.header['Auto-Submitted'].value).to eq('auto-generated')
+ expect(mail.header['X-Auto-Response-Suppress'].value).to eq('All')
+ end
+ end
end
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
new file mode 100644
index 00000000000..3c4deba4712
--- /dev/null
+++ b/spec/lib/api/helpers_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+describe API::Helpers do
+ subject { Class.new.include(described_class).new }
+
+ describe '#find_namespace' do
+ let(:namespace) { create(:namespace) }
+
+ shared_examples 'namespace finder' do
+ context 'when namespace exists' do
+ it 'returns requested namespace' do
+ expect(subject.find_namespace(existing_id)).to eq(namespace)
+ end
+ end
+
+ context "when namespace doesn't exists" do
+ it 'returns nil' do
+ expect(subject.find_namespace(non_existing_id)).to be_nil
+ end
+ end
+ end
+
+ context 'when ID is used as an argument' do
+ let(:existing_id) { namespace.id }
+ let(:non_existing_id) { 9999 }
+
+ it_behaves_like 'namespace finder'
+ end
+
+ context 'when PATH is used as an argument' do
+ let(:existing_id) { namespace.path }
+ let(:non_existing_id) { 'non-existing-path' }
+
+ it_behaves_like 'namespace finder'
+ end
+ end
+
+ shared_examples 'user namespace finder' do
+ let(:user1) { create(:user) }
+
+ before do
+ allow(subject).to receive(:current_user).and_return(user1)
+ allow(subject).to receive(:header).and_return(nil)
+ allow(subject).to receive(:not_found!).and_raise('404 Namespace not found')
+ end
+
+ context 'when namespace is group' do
+ let(:namespace) { create(:group) }
+
+ context 'when user has access to group' do
+ before do
+ namespace.add_guest(user1)
+ namespace.save!
+ end
+
+ it 'returns requested namespace' do
+ expect(namespace_finder).to eq(namespace)
+ end
+ end
+
+ context "when user doesn't have access to group" do
+ it 'raises not found error' do
+ expect { namespace_finder }.to raise_error(RuntimeError, '404 Namespace not found')
+ end
+ end
+ end
+
+ context "when namespace is user's personal namespace" do
+ let(:namespace) { create(:namespace) }
+
+ context 'when user owns the namespace' do
+ before do
+ namespace.owner = user1
+ namespace.save!
+ end
+
+ it 'returns requested namespace' do
+ expect(namespace_finder).to eq(namespace)
+ end
+ end
+
+ context "when user doesn't own the namespace" do
+ it 'raises not found error' do
+ expect { namespace_finder }.to raise_error(RuntimeError, '404 Namespace not found')
+ end
+ end
+ end
+ end
+
+ describe '#find_namespace!' do
+ let(:namespace_finder) do
+ subject.find_namespace!(namespace.id)
+ end
+
+ it_behaves_like 'user namespace finder'
+ end
+
+ describe '#user_namespace' do
+ let(:namespace_finder) do
+ subject.user_namespace
+ end
+
+ before do
+ allow(subject).to receive(:params).and_return({ id: namespace.id })
+ end
+
+ it_behaves_like 'user namespace finder'
+ end
+end
diff --git a/spec/lib/banzai/commit_renderer_spec.rb b/spec/lib/banzai/commit_renderer_spec.rb
index 049d025a5b9..84adaebdcbe 100644
--- a/spec/lib/banzai/commit_renderer_spec.rb
+++ b/spec/lib/banzai/commit_renderer_spec.rb
@@ -10,7 +10,7 @@ describe Banzai::CommitRenderer do
described_class::ATTRIBUTES.each do |attr|
expect_any_instance_of(Banzai::ObjectRenderer).to receive(:render).with([project.commit], attr).once.and_call_original
- expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr)
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr, {})
end
described_class.render([project.commit], project, user)
diff --git a/spec/lib/banzai/filter/absolute_link_filter_spec.rb b/spec/lib/banzai/filter/absolute_link_filter_spec.rb
new file mode 100644
index 00000000000..a3ad056efcd
--- /dev/null
+++ b/spec/lib/banzai/filter/absolute_link_filter_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Banzai::Filter::AbsoluteLinkFilter do
+ def filter(doc, context = {})
+ described_class.call(doc, context)
+ end
+
+ context 'with html links' do
+ context 'if only_path is false' do
+ let(:only_path_context) do
+ { only_path: false }
+ end
+ let(:fake_url) { 'http://www.example.com' }
+
+ before do
+ allow(Gitlab.config.gitlab).to receive(:url).and_return(fake_url)
+ end
+
+ context 'has the .gfm class' do
+ it 'converts a relative url into absolute' do
+ doc = filter(link('/foo', 'gfm'), only_path_context)
+ expect(doc.at_css('a')['href']).to eq "#{fake_url}/foo"
+ end
+
+ it 'does not change the url if it already absolute' do
+ doc = filter(link("#{fake_url}/foo", 'gfm'), only_path_context)
+ expect(doc.at_css('a')['href']).to eq "#{fake_url}/foo"
+ end
+
+ context 'if relative_url_root is set' do
+ it 'joins the url without without doubling the path' do
+ allow(Gitlab.config.gitlab).to receive(:url).and_return("#{fake_url}/gitlab/")
+ doc = filter(link("/gitlab/foo", 'gfm'), only_path_context)
+ expect(doc.at_css('a')['href']).to eq "#{fake_url}/gitlab/foo"
+ end
+ end
+ end
+
+ context 'has not the .gfm class' do
+ it 'does not convert a relative url into absolute' do
+ doc = filter(link('/foo'), only_path_context)
+ expect(doc.at_css('a')['href']).to eq '/foo'
+ end
+ end
+ end
+
+ context 'if only_path is not false' do
+ it 'does not convert a relative url into absolute' do
+ expect(filter(link('/foo', 'gfm')).at_css('a')['href']).to eq '/foo'
+ expect(filter(link('/foo')).at_css('a')['href']).to eq '/foo'
+ end
+ end
+ end
+
+ def link(path, css_class = '')
+ %(<a class="#{css_class}" href="#{path}">example</a>)
+ end
+end
diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
index 97d612e6347..ca76d6f0881 100644
--- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
+++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
@@ -15,9 +15,13 @@ describe Banzai::Filter::GollumTagsFilter do
context 'linking internal images' do
it 'creates img tag if image exists' do
- file = Gollum::File.new(project_wiki.wiki)
- expect(file).to receive(:path).and_return('images/image.jpg')
- expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(file)
+ gollum_file_double = double('Gollum::File',
+ mime_type: 'image/jpeg',
+ name: 'images/image.jpg',
+ path: 'images/image.jpg',
+ raw_data: '')
+ wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double)
+ expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(wiki_file)
tag = '[[images/image.jpg]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 9c74c9b8c99..f70c69ef588 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -317,6 +317,76 @@ describe Banzai::Filter::IssueReferenceFilter do
end
end
+ context 'group context' do
+ let(:group) { create(:group) }
+ let(:context) { { project: nil, group: group } }
+
+ it 'ignores shorthanded issue reference' do
+ reference = "##{issue.iid}"
+ text = "Fixed #{reference}"
+
+ expect(reference_filter(text, context).to_html).to eq(text)
+ end
+
+ it 'ignores valid references when cross-reference project uses external tracker' do
+ expect_any_instance_of(described_class).to receive(:find_object)
+ .with(project, issue.iid)
+ .and_return(nil)
+
+ reference = "#{project.full_path}##{issue.iid}"
+ text = "Issue #{reference}"
+
+ expect(reference_filter(text, context).to_html).to eq(text)
+ end
+
+ it 'links to a valid reference for complete cross-reference' do
+ reference = "#{project.full_path}##{issue.iid}"
+ doc = reference_filter("See #{reference}", context)
+
+ link = doc.css('a').first
+ expect(link.attr('href')).to eq(helper.url_for_issue(issue.iid, project))
+ expect(link.text).to include("#{project.full_path}##{issue.iid}")
+ end
+
+ it 'ignores reference for shorthand cross-reference' do
+ reference = "#{project.path}##{issue.iid}"
+ text = "See #{reference}"
+
+ expect(reference_filter(text, context).to_html).to eq(text)
+ end
+
+ it 'links to a valid reference for url cross-reference' do
+ reference = helper.url_for_issue(issue.iid, project) + "#note_123"
+
+ doc = reference_filter("See #{reference}", context)
+
+ link = doc.css('a').first
+ expect(link.attr('href')).to eq(helper.url_for_issue(issue.iid, project) + "#note_123")
+ expect(link.text).to include("#{project.full_path}##{issue.iid}")
+ end
+
+ it 'links to a valid reference for cross-reference in link href' do
+ reference = "#{helper.url_for_issue(issue.iid, project) + "#note_123"}"
+ reference_link = %{<a href="#{reference}">Reference</a>}
+
+ doc = reference_filter("See #{reference_link}", context)
+
+ link = doc.css('a').first
+ expect(link.attr('href')).to eq(helper.url_for_issue(issue.iid, project) + "#note_123")
+ expect(link.text).to include('Reference')
+ end
+
+ it 'links to a valid reference for issue reference in the link href' do
+ reference = issue.to_reference(group)
+ reference_link = %{<a href="#{reference}">Reference</a>}
+ doc = reference_filter("See #{reference_link}", context)
+
+ link = doc.css('a').first
+ expect(link.attr('href')).to eq(helper.url_for_issue(issue.iid, project))
+ expect(link.text).to include('Reference')
+ end
+ end
+
describe '#issues_per_project' do
context 'using an internal issue tracker' do
it 'returns a Hash containing the issues per project' do
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 2cd30a5e302..862b1fe3fd3 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -594,4 +594,16 @@ describe Banzai::Filter::LabelReferenceFilter do
expect(reference_filter(act).to_html).to eq exp
end
end
+
+ describe 'group context' do
+ it 'points to referenced project issues page' do
+ project = create(:project)
+ label = create(:label, project: project)
+ reference = "#{project.full_path}~#{label.name}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.project_issues_url(project, label_name: label.name))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index ed2788f8a33..158844e25ae 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -214,4 +214,14 @@ describe Banzai::Filter::MergeRequestReferenceFilter do
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
end
end
+
+ context 'group context' do
+ it 'links to a valid reference' do
+ reference = "#{project.full_path}!#{merge.iid}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.project_merge_request_url(project, merge))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/mermaid_filter_spec.rb b/spec/lib/banzai/filter/mermaid_filter_spec.rb
new file mode 100644
index 00000000000..532d25e121d
--- /dev/null
+++ b/spec/lib/banzai/filter/mermaid_filter_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Banzai::Filter::MermaidFilter do
+ include FilterSpecHelper
+
+ it 'adds `js-render-mermaid` class to the `pre` tag' do
+ doc = filter("<pre class='code highlight js-syntax-highlight mermaid' lang='mermaid' v-pre='true'><code>graph TD;\n A--&gt;B;\n</code></pre>")
+ result = doc.xpath('descendant-or-self::pre').first
+
+ expect(result[:class]).to include('js-render-mermaid')
+ end
+end
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index fe7a8c84c9e..6a9087d2e59 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -294,8 +294,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
end
- context 'project milestones' do
- let(:milestone) { create(:milestone, project: project) }
+ shared_context 'project milestones' do
let(:reference) { milestone.to_reference(format: :iid) }
include_examples 'reference parsing'
@@ -309,8 +308,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do
it_behaves_like 'cross project shorthand reference'
end
- context 'group milestones' do
- let(:milestone) { create(:milestone, group: group) }
+ shared_context 'group milestones' do
let(:reference) { milestone.to_reference(format: :name) }
include_examples 'reference parsing'
@@ -343,4 +341,43 @@ describe Banzai::Filter::MilestoneReferenceFilter do
expect(doc.css('a')).to be_empty
end
end
+
+ context 'group context' do
+ it 'links to a valid reference' do
+ milestone = create(:milestone, project: project)
+ reference = "#{project.full_path}%#{milestone.iid}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone))
+ end
+ end
+
+ context 'when milestone is open' do
+ context 'project milestones' do
+ let(:milestone) { create(:milestone, project: project) }
+
+ include_context 'project milestones'
+ end
+
+ context 'group milestones' do
+ let(:milestone) { create(:milestone, group: group) }
+
+ include_context 'group milestones'
+ end
+ end
+
+ context 'when milestone is closed' do
+ context 'project milestones' do
+ let(:milestone) { create(:milestone, :closed, project: project) }
+
+ include_context 'project milestones'
+ end
+
+ context 'group milestones' do
+ let(:milestone) { create(:milestone, :closed, group: group) }
+
+ include_context 'group milestones'
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index 5f41e28fece..17a620ef603 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -217,6 +217,11 @@ describe Banzai::Filter::SanitizationFilter do
output: '<img>'
},
+ 'protocol-based JS injection: Unicode' => {
+ input: %Q(<a href="\u0001java\u0003script:alert('XSS')">foo</a>),
+ output: '<a>foo</a>'
+ },
+
'protocol-based JS injection: spaces and entities' => {
input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
output: '<a href="">foo</a>'
diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
index 90ac4c7b238..3a07a6dc179 100644
--- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
@@ -201,4 +201,14 @@ describe Banzai::Filter::SnippetReferenceFilter do
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
end
end
+
+ context 'group context' do
+ it 'links to a valid reference' do
+ reference = "#{project.full_path}$#{snippet.id}"
+
+ result = reference_filter("See #{reference}", { project: nil, group: create(:group) } )
+
+ expect(result.css('a').first.attr('href')).to eq(urls.project_snippet_url(project, snippet))
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index 5a23e0e70cc..9f2efa05a01 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -31,7 +31,7 @@ describe Banzai::Filter::SyntaxHighlightFilter do
it "highlights as plaintext" do
result = filter('<pre><code lang="ruby">This is a test</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight" lang="" v-pre="true"><code>This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight" lang="" v-pre="true"><code>This is a test</code></pre>')
end
end
end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 34dac1db69a..fc03741976e 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -208,6 +208,39 @@ describe Banzai::Filter::UserReferenceFilter do
end
end
+ context 'in group context' do
+ let(:group) { create(:group) }
+ let(:group_member) { create(:user) }
+
+ before do
+ group.add_developer(group_member)
+ end
+
+ let(:context) { { author: group_member, project: nil, group: group } }
+
+ it 'supports a special @all mention' do
+ reference = User.reference_prefix + 'all'
+ doc = reference_filter("Hey #{reference}", context)
+
+ expect(doc.css('a').length).to eq(1)
+ expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
+ end
+
+ it 'supports mentioning a single user' do
+ reference = group_member.to_reference
+ doc = reference_filter("Hey #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(group_member)
+ end
+
+ it 'supports mentioning a group' do
+ reference = group.to_reference
+ doc = reference_filter("Hey #{reference}", context)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(group)
+ end
+ end
+
describe '#namespaces' do
it 'returns a Hash containing all Namespaces' do
document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
diff --git a/spec/lib/banzai/note_renderer_spec.rb b/spec/lib/banzai/note_renderer_spec.rb
deleted file mode 100644
index 32764bee5eb..00000000000
--- a/spec/lib/banzai/note_renderer_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require 'spec_helper'
-
-describe Banzai::NoteRenderer do
- describe '.render' do
- it 'renders a Note' do
- note = double(:note)
- project = double(:project)
- wiki = double(:wiki)
- user = double(:user)
-
- expect(Banzai::ObjectRenderer).to receive(:new)
- .with(project, user,
- requested_path: 'foo',
- project_wiki: wiki,
- ref: 'bar')
- .and_call_original
-
- expect_any_instance_of(Banzai::ObjectRenderer)
- .to receive(:render).with([note], :note)
-
- described_class.render([note], project, user, 'foo', wiki, 'bar')
- end
- end
-end
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index b172a1b718c..074d521a5c6 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -22,7 +22,7 @@ describe Banzai::ObjectRenderer do
end
it 'retrieves field content using Banzai::Renderer.render_field' do
- expect(Banzai::Renderer).to receive(:render_field).with(object, :note).and_call_original
+ expect(Banzai::Renderer).to receive(:render_field).with(object, :note, {}).and_call_original
renderer.render([object], :note)
end
@@ -68,7 +68,7 @@ describe Banzai::ObjectRenderer do
end
it 'retrieves field content using Banzai::Renderer.cacheless_render_field' do
- expect(Banzai::Renderer).to receive(:cacheless_render_field).with(commit, :title).and_call_original
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).with(commit, :title, {}).and_call_original
renderer.render([commit], :title)
end
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index 81a04a2d46d..650cecfc778 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -18,7 +18,7 @@ describe Banzai::Renderer do
let(:commit) { create(:project, :repository).commit }
it 'returns cacheless render field' do
- expect(renderer).to receive(:cacheless_render_field).with(commit, :title)
+ expect(renderer).to receive(:cacheless_render_field).with(commit, :title, {})
renderer.render_field(commit, :title)
end
diff --git a/spec/lib/container_registry/path_spec.rb b/spec/lib/container_registry/path_spec.rb
index 84cacdd3f0d..010deae822c 100644
--- a/spec/lib/container_registry/path_spec.rb
+++ b/spec/lib/container_registry/path_spec.rb
@@ -86,6 +86,24 @@ describe ContainerRegistry::Path do
it { is_expected.to be_valid }
end
+
+ context 'when path contains double underscore' do
+ let(:path) { 'my/repository__name' }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when path contains invalid separator with dot' do
+ let(:path) { 'some/registry-.name' }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when path contains invalid separator with underscore' do
+ let(:path) { 'some/registry._name' }
+
+ it { is_expected.not_to be_valid }
+ end
end
describe '#has_repository?' do
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 1076c63b5f2..10020511bf8 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -13,6 +13,47 @@ describe Feature do
end
end
+ describe '.persisted_names' do
+ it 'returns the names of the persisted features' do
+ Feature::FlipperFeature.create!(key: 'foo')
+
+ expect(described_class.persisted_names).to eq(%w[foo])
+ end
+
+ it 'returns an empty Array when no features are presisted' do
+ expect(described_class.persisted_names).to be_empty
+ end
+
+ it 'caches the feature names when request store is active', :request_store do
+ Feature::FlipperFeature.create!(key: 'foo')
+
+ expect(Feature::FlipperFeature)
+ .to receive(:feature_names)
+ .once
+ .and_call_original
+
+ 2.times do
+ expect(described_class.persisted_names).to eq(%w[foo])
+ end
+ end
+ end
+
+ describe '.persisted?' do
+ it 'returns true for a persisted feature' do
+ Feature::FlipperFeature.create!(key: 'foo')
+
+ feature = double(:feature, name: 'foo')
+
+ expect(described_class.persisted?(feature)).to eq(true)
+ end
+
+ it 'returns false for a feature that is not persisted' do
+ feature = double(:feature, name: 'foo')
+
+ expect(described_class.persisted?(feature)).to eq(false)
+ end
+ end
+
describe '.all' do
let(:features) { Set.new }
diff --git a/spec/lib/github/client_spec.rb b/spec/lib/github/client_spec.rb
deleted file mode 100644
index b846096fe25..00000000000
--- a/spec/lib/github/client_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'spec_helper'
-
-describe Github::Client do
- let(:connection) { spy }
- let(:rate_limit) { double(get: [false, 1]) }
- let(:client) { described_class.new({}) }
- let(:results) { double }
- let(:response) { double }
-
- before do
- allow(Faraday).to receive(:new).and_return(connection)
- allow(Github::RateLimit).to receive(:new).with(connection).and_return(rate_limit)
- end
-
- describe '#get' do
- before do
- allow(Github::Response).to receive(:new).with(results).and_return(response)
- end
-
- it 'uses a default per_page param' do
- expect(connection).to receive(:get).with('/foo', per_page: 100).and_return(results)
-
- expect(client.get('/foo')).to eq(response)
- end
-
- context 'with per_page given' do
- it 'overwrites the default per_page' do
- expect(connection).to receive(:get).with('/foo', per_page: 30).and_return(results)
-
- expect(client.get('/foo', per_page: 30)).to eq(response)
- end
- end
- end
-end
diff --git a/spec/lib/github/import/legacy_diff_note_spec.rb b/spec/lib/github/import/legacy_diff_note_spec.rb
deleted file mode 100644
index 8c50b46cacb..00000000000
--- a/spec/lib/github/import/legacy_diff_note_spec.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-require 'spec_helper'
-
-describe Github::Import::LegacyDiffNote do
- describe '#type' do
- it 'returns the original note type' do
- expect(described_class.new.type).to eq('LegacyDiffNote')
- end
- end
-end
diff --git a/spec/lib/github/import/note_spec.rb b/spec/lib/github/import/note_spec.rb
deleted file mode 100644
index fcdccd9e097..00000000000
--- a/spec/lib/github/import/note_spec.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-require 'spec_helper'
-
-describe Github::Import::Note do
- describe '#type' do
- it 'returns the original note type' do
- expect(described_class.new.type).to eq('Note')
- end
- end
-end
diff --git a/spec/lib/gitlab/app_logger_spec.rb b/spec/lib/gitlab/app_logger_spec.rb
new file mode 100644
index 00000000000..c86d30ce6df
--- /dev/null
+++ b/spec/lib/gitlab/app_logger_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Gitlab::AppLogger, :request_store do
+ subject { described_class }
+
+ it 'builds a logger once' do
+ expect(::Logger).to receive(:new).and_call_original
+
+ subject.info('hello world')
+ subject.error('hello again')
+ end
+end
diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb
new file mode 100644
index 00000000000..ffcd90b9fcb
--- /dev/null
+++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::RequestAuthenticator do
+ let(:env) do
+ {
+ 'rack.input' => '',
+ 'REQUEST_METHOD' => 'GET'
+ }
+ end
+ let(:request) { ActionDispatch::Request.new(env) }
+
+ subject { described_class.new(request) }
+
+ describe '#user' do
+ let!(:sessionless_user) { build(:user) }
+ let!(:session_user) { build(:user) }
+
+ it 'returns sessionless user first' do
+ allow_any_instance_of(described_class).to receive(:find_sessionless_user).and_return(sessionless_user)
+ allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user)
+
+ expect(subject.user).to eq sessionless_user
+ end
+
+ it 'returns session user if no sessionless user found' do
+ allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user)
+
+ expect(subject.user).to eq session_user
+ end
+
+ it 'returns nil if no user found' do
+ expect(subject.user).to be_blank
+ end
+
+ it 'bubbles up exceptions' do
+ allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_raise(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+
+ describe '#find_sessionless_user' do
+ let!(:access_token_user) { build(:user) }
+ let!(:rss_token_user) { build(:user) }
+
+ it 'returns access_token user first' do
+ allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_return(access_token_user)
+ allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user)
+
+ expect(subject.find_sessionless_user).to eq access_token_user
+ end
+
+ it 'returns rss_token user if no access_token user found' do
+ allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user)
+
+ expect(subject.find_sessionless_user).to eq rss_token_user
+ end
+
+ it 'returns nil if no user found' do
+ expect(subject.find_sessionless_user).to be_blank
+ end
+
+ it 'rescue Gitlab::Auth::AuthenticationError exceptions' do
+ allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_raise(Gitlab::Auth::UnauthorizedError)
+
+ expect(subject.find_sessionless_user).to be_blank
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
new file mode 100644
index 00000000000..4637816570c
--- /dev/null
+++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
@@ -0,0 +1,194 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::UserAuthFinders do
+ include described_class
+
+ let(:user) { create(:user) }
+ let(:env) do
+ {
+ 'rack.input' => ''
+ }
+ end
+ let(:request) { Rack::Request.new(env)}
+
+ def set_param(key, value)
+ request.update_param(key, value)
+ end
+
+ describe '#find_user_from_warden' do
+ context 'with CSRF token' do
+ before do
+ allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(true)
+ end
+
+ context 'with invalid credentials' do
+ it 'returns nil' do
+ expect(find_user_from_warden).to be_nil
+ end
+ end
+
+ context 'with valid credentials' do
+ it 'returns the user' do
+ env['warden'] = double("warden", authenticate: user)
+
+ expect(find_user_from_warden).to eq user
+ end
+ end
+ end
+
+ context 'without CSRF token' do
+ it 'returns nil' do
+ allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(false)
+ env['warden'] = double("warden", authenticate: user)
+
+ expect(find_user_from_warden).to be_nil
+ end
+ end
+ end
+
+ describe '#find_user_from_rss_token' do
+ context 'when the request format is atom' do
+ before do
+ env['HTTP_ACCEPT'] = 'application/atom+xml'
+ end
+
+ it 'returns user if valid rss_token' do
+ set_param(:rss_token, user.rss_token)
+
+ expect(find_user_from_rss_token).to eq user
+ end
+
+ it 'returns nil if rss_token is blank' do
+ expect(find_user_from_rss_token).to be_nil
+ end
+
+ it 'returns exception if invalid rss_token' do
+ set_param(:rss_token, 'invalid_token')
+
+ expect { find_user_from_rss_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+
+ context 'when the request format is not atom' do
+ it 'returns nil' do
+ set_param(:rss_token, user.rss_token)
+
+ expect(find_user_from_rss_token).to be_nil
+ end
+ end
+ end
+
+ describe '#find_user_from_access_token' do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it 'returns nil if no access_token present' do
+ expect(find_personal_access_token).to be_nil
+ end
+
+ context 'when validate_access_token! returns valid' do
+ it 'returns user' do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+
+ expect(find_user_from_access_token).to eq user
+ end
+
+ it 'returns exception if token has no user' do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow_any_instance_of(PersonalAccessToken).to receive(:user).and_return(nil)
+
+ expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+ end
+
+ describe '#find_personal_access_token' do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ context 'passed as header' do
+ it 'returns token if valid personal_access_token' do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+
+ expect(find_personal_access_token).to eq personal_access_token
+ end
+ end
+
+ context 'passed as param' do
+ it 'returns token if valid personal_access_token' do
+ set_param(Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_PARAM, personal_access_token.token)
+
+ expect(find_personal_access_token).to eq personal_access_token
+ end
+ end
+
+ it 'returns nil if no personal_access_token' do
+ expect(find_personal_access_token).to be_nil
+ end
+
+ it 'returns exception if invalid personal_access_token' do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = 'invalid_token'
+
+ expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+
+ describe '#find_oauth_access_token' do
+ let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) }
+ let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') }
+
+ context 'passed as header' do
+ it 'returns token if valid oauth_access_token' do
+ env['HTTP_AUTHORIZATION'] = "Bearer #{token.token}"
+
+ expect(find_oauth_access_token.token).to eq token.token
+ end
+ end
+
+ context 'passed as param' do
+ it 'returns user if valid oauth_access_token' do
+ set_param(:access_token, token.token)
+
+ expect(find_oauth_access_token.token).to eq token.token
+ end
+ end
+
+ it 'returns nil if no oauth_access_token' do
+ expect(find_oauth_access_token).to be_nil
+ end
+
+ it 'returns exception if invalid oauth_access_token' do
+ env['HTTP_AUTHORIZATION'] = "Bearer invalid_token"
+
+ expect { find_oauth_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+
+ describe '#validate_access_token!' do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it 'returns nil if no access_token present' do
+ expect(validate_access_token!).to be_nil
+ end
+
+ context 'token is not valid' do
+ before do
+ allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token)
+ end
+
+ it 'returns Gitlab::Auth::ExpiredError if token expired' do
+ personal_access_token.expires_at = 1.day.ago
+
+ expect { validate_access_token! }.to raise_error(Gitlab::Auth::ExpiredError)
+ end
+
+ it 'returns Gitlab::Auth::RevokedError if token revoked' do
+ personal_access_token.revoke!
+
+ expect { validate_access_token! }.to raise_error(Gitlab::Auth::RevokedError)
+ end
+
+ it 'returns Gitlab::Auth::InsufficientScopeError if invalid token scope' do
+ expect { validate_access_token!(scopes: [:sudo]) }.to raise_error(Gitlab::Auth::InsufficientScopeError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index af1db2c3455..a6fbec295b5 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Auth do
describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do
- expect(subject::API_SCOPES).to eq [:api, :read_user]
+ expect(subject::API_SCOPES).to eq %i[api read_user sudo]
end
it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
@@ -19,7 +19,7 @@ describe Gitlab::Auth do
it 'optional_scopes contains all non-default scopes' do
stub_container_registry_config(enabled: true)
- expect(subject.optional_scopes).to eq %i[read_user read_registry openid]
+ expect(subject.optional_scopes).to eq %i[read_user sudo read_registry openid]
end
context 'registry_scopes' do
@@ -133,6 +133,25 @@ describe Gitlab::Auth do
gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')
end
+
+ it 'grants deploy key write permissions' do
+ project = create(:project)
+ key = create(:deploy_key, can_push: true)
+ create(:deploy_keys_project, deploy_key: key, project: project)
+ token = Gitlab::LfsToken.new(key).token
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_write_authentication_abilities))
+ end
+
+ it 'does not grant deploy key write permissions' do
+ project = create(:project)
+ key = create(:deploy_key, can_push: true)
+ token = Gitlab::LfsToken.new(key).token
+
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
+ end
end
context 'while using OAuth tokens as passwords' do
@@ -164,7 +183,7 @@ describe Gitlab::Auth do
personal_access_token = create(:personal_access_token, scopes: ['api'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities))
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, full_authentication_abilities))
end
context 'when registry is enabled' do
@@ -176,7 +195,7 @@ describe Gitlab::Auth do
personal_access_token = create(:personal_access_token, scopes: ['read_registry'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image]))
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:read_container_image]))
end
end
@@ -184,14 +203,14 @@ describe Gitlab::Auth do
impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_token, full_authentication_abilities))
+ expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_access_token, full_authentication_abilities))
end
it 'limits abilities based on scope' do
- personal_access_token = create(:personal_access_token, scopes: ['read_user'])
+ personal_access_token = create(:personal_access_token, scopes: %w[read_user sudo])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, []))
+ expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, []))
end
it 'fails if password is nil' do
@@ -232,9 +251,9 @@ describe Gitlab::Auth do
end
it 'throws an error suggesting user create a PAT when internal auth is disabled' do
- allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled?) { false }
+ allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled_for_git?) { false }
- expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalTokenError)
+ expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError)
end
end
@@ -305,6 +324,26 @@ describe Gitlab::Auth do
gl_auth.find_with_user_password('ldap_user', 'password')
end
end
+
+ context "with password authentication disabled for Git" do
+ before do
+ stub_application_setting(password_authentication_enabled_for_git: 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
+
+ context "with ldap enabled" do
+ before do
+ allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
+ end
+
+ it "does not find non-ldap user by valid login/password" do
+ expect(gl_auth.find_with_user_password(username, password)).to be_nil
+ end
+ end
+ end
end
private
@@ -326,10 +365,15 @@ describe Gitlab::Auth do
]
end
- def full_authentication_abilities
+ def read_write_authentication_abilities
read_authentication_abilities + [
:push_code,
- :create_container_image,
+ :create_container_image
+ ]
+ end
+
+ def full_authentication_abilities
+ read_write_authentication_abilities + [
:admin_container_image
]
end
diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
index 1a4ea2bac48..79d2c071446 100644
--- a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
@@ -93,7 +93,14 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat
end
it 'knows it is finished for this range' do
- expect(migration.missing_members?(1, 7)).to be_falsy
+ expect(migration.missing_members?(1, 8)).to be_falsy
+ end
+
+ it 'does not miss members for forks of forks for which the root was deleted' do
+ forked_project_links.create(id: 9, forked_from_project_id: base1_fork1.id, forked_to_project_id: create(:project).id)
+ base1.destroy
+
+ expect(migration.missing_members?(7, 10)).to be_falsy
end
context 'with more forks' do
diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
index d2e7243ee05..84d9e635810 100644
--- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
+++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
@@ -1,9 +1,13 @@
require 'spec_helper'
-describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :truncate do
+describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :truncate, :migration, schema: 20171114162227 do
+ let(:merge_request_diffs) { table(:merge_request_diffs) }
+ let(:merge_requests) { table(:merge_requests) }
+
describe '#perform' do
- let(:merge_request) { create(:merge_request) }
- let(:merge_request_diff) { merge_request.merge_request_diff }
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { merge_requests.create!(iid: 1, target_project_id: project.id, source_project_id: project.id, target_branch: 'feature', source_branch: 'master').becomes(MergeRequest) }
+ let(:merge_request_diff) { MergeRequest.find(merge_request.id).create_merge_request_diff }
let(:updated_merge_request_diff) { MergeRequestDiff.find(merge_request_diff.id) }
def diffs_to_hashes(diffs)
@@ -31,8 +35,8 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t
end
it 'creates correct entries in the merge_request_diff_commits table' do
- expect(updated_merge_request_diff.merge_request_diff_commits.count).to eq(commits.count)
- expect(updated_merge_request_diff.commits.map(&:to_hash)).to eq(commits)
+ expect(updated_merge_request_diff.merge_request_diff_commits.count).to eq(expected_commits.count)
+ expect(updated_merge_request_diff.commits.map(&:to_hash)).to eq(expected_commits)
end
it 'creates correct entries in the merge_request_diff_files table' do
@@ -68,7 +72,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t
let(:stop_id) { described_class::MergeRequestDiff.maximum(:id) }
before do
- merge_request.reload_diff(true)
+ merge_request.create_merge_request_diff
convert_to_yaml(start_id, merge_request_diff.commits, diffs_to_hashes(merge_request_diff.merge_request_diff_files))
convert_to_yaml(stop_id, updated_merge_request_diff.commits, diffs_to_hashes(updated_merge_request_diff.merge_request_diff_files))
@@ -199,6 +203,16 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t
context 'when the merge request diff has valid commits and diffs' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
+ let(:expected_commits) { commits }
+ let(:diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) }
+ let(:expected_diffs) { diffs }
+
+ include_examples 'updated MR diff'
+ end
+
+ context 'when the merge request diff has diffs but no commits' do
+ let(:commits) { nil }
+ let(:expected_commits) { [] }
let(:diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) }
let(:expected_diffs) { diffs }
@@ -207,6 +221,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t
context 'when the merge request diffs do not have too_large set' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
+ let(:expected_commits) { commits }
let(:expected_diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) }
let(:diffs) do
@@ -218,6 +233,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t
context 'when the merge request diffs do not have a_mode and b_mode set' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
+ let(:expected_commits) { commits }
let(:expected_diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) }
let(:diffs) do
@@ -229,6 +245,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t
context 'when the merge request diffs have binary content' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
+ let(:expected_commits) { commits }
let(:expected_diffs) { diffs }
# The start of a PDF created by Illustrator
@@ -257,6 +274,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t
context 'when the merge request diff has commits, but no diffs' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
+ let(:expected_commits) { commits }
let(:diffs) { [] }
let(:expected_diffs) { diffs }
@@ -265,6 +283,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t
context 'when the merge request diffs have invalid content' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
+ let(:expected_commits) { commits }
let(:diffs) { ['--broken-diff'] }
let(:expected_diffs) { [] }
@@ -273,7 +292,8 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t
context 'when the merge request diffs are Rugged::Patch instances' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
- let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) }
+ let(:first_commit) { project.repository.commit(merge_request_diff.head_commit_sha) }
+ let(:expected_commits) { commits }
let(:diffs) { first_commit.rugged_diff_from_parent.patches }
let(:expected_diffs) { [] }
@@ -282,7 +302,8 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t
context 'when the merge request diffs are Rugged::Diff::Delta instances' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
- let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) }
+ let(:first_commit) { project.repository.commit(merge_request_diff.head_commit_sha) }
+ let(:expected_commits) { commits }
let(:diffs) { first_commit.rugged_diff_from_parent.deltas }
let(:expected_diffs) { [] }
diff --git a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
index 59f69d1e4b1..7b5a00c6111 100644
--- a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder do
end
describe '#perform' do
- it 'renames the path of system-uploads', truncate: true do
+ it 'renames the path of system-uploads', :truncate do
upload = create(:upload, model: create(:project), path: 'uploads/system/project/avatar.jpg')
migration.perform('uploads/system/', 'uploads/-/system/')
diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
index 3ef1873e615..e52baf8dde7 100644
--- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
@@ -3,12 +3,9 @@ require 'spec_helper'
describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do
let(:migration) { described_class.new }
let(:base1) { create(:project) }
- let(:base1_fork1) { create(:project) }
- let(:base1_fork2) { create(:project) }
let(:base2) { create(:project) }
let(:base2_fork1) { create(:project) }
- let(:base2_fork2) { create(:project) }
let!(:forked_project_links) { table(:forked_project_links) }
let!(:fork_networks) { table(:fork_networks) }
@@ -21,21 +18,24 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
# A normal fork link
forked_project_links.create(id: 1,
forked_from_project_id: base1.id,
- forked_to_project_id: base1_fork1.id)
+ forked_to_project_id: create(:project).id)
forked_project_links.create(id: 2,
forked_from_project_id: base1.id,
- forked_to_project_id: base1_fork2.id)
-
+ forked_to_project_id: create(:project).id)
forked_project_links.create(id: 3,
forked_from_project_id: base2.id,
forked_to_project_id: base2_fork1.id)
+
+ # create a fork of a fork
forked_project_links.create(id: 4,
forked_from_project_id: base2_fork1.id,
forked_to_project_id: create(:project).id)
-
forked_project_links.create(id: 5,
- forked_from_project_id: base2.id,
- forked_to_project_id: base2_fork2.id)
+ forked_from_project_id: create(:project).id,
+ forked_to_project_id: create(:project).id)
+
+ # Stub out the calls to the other migrations
+ allow(BackgroundMigrationWorker).to receive(:perform_in)
migration.perform(1, 3)
end
@@ -62,6 +62,17 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
expect(base2_membership).not_to be_nil
end
+ it 'creates a fork network for the fork of which the source was deleted' do
+ fork = create(:project)
+ forked_project_links.create(id: 6, forked_from_project_id: 99999, forked_to_project_id: fork.id)
+
+ migration.perform(5, 8)
+
+ expect(fork_networks.find_by(root_project_id: 99999)).to be_nil
+ expect(fork_networks.find_by(root_project_id: fork.id)).not_to be_nil
+ expect(fork_network_members.find_by(project_id: fork.id)).not_to be_nil
+ end
+
it 'schedules a job for inserting memberships for forks-of-forks' do
delay = Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY
@@ -72,11 +83,11 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
end
it 'only processes a single batch of links at a time' do
- expect(fork_network_members.count).to eq(5)
+ expect(fork_networks.count).to eq(2)
migration.perform(3, 5)
- expect(fork_network_members.count).to eq(7)
+ expect(fork_networks.count).to eq(3)
end
it 'can be repeated without effect' do
diff --git a/spec/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id_spec.rb
new file mode 100644
index 00000000000..0cb753c5853
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::PopulateMergeRequestsLatestMergeRequestDiffId, :migration, schema: 20171026082505 do
+ let(:projects_table) { table(:projects) }
+ let(:merge_requests_table) { table(:merge_requests) }
+ let(:merge_request_diffs_table) { table(:merge_request_diffs) }
+
+ let(:project) { projects_table.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce') }
+
+ def create_mr!(name, diffs: 0)
+ merge_request =
+ merge_requests_table.create!(target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: name,
+ title: name)
+
+ diffs.times do
+ merge_request_diffs_table.create!(merge_request_id: merge_request.id)
+ end
+
+ merge_request
+ end
+
+ def diffs_for(merge_request)
+ merge_request_diffs_table.where(merge_request_id: merge_request.id)
+ end
+
+ describe '#perform' do
+ it 'ignores MRs without diffs' do
+ merge_request_without_diff = create_mr!('without_diff')
+ mr_id = merge_request_without_diff.id
+
+ expect(merge_request_without_diff.latest_merge_request_diff_id).to be_nil
+
+ expect { subject.perform(mr_id, mr_id) }
+ .not_to change { merge_request_without_diff.reload.latest_merge_request_diff_id }
+ end
+
+ it 'ignores MRs that have a diff ID already set' do
+ merge_request_with_multiple_diffs = create_mr!('with_multiple_diffs', diffs: 3)
+ diff_id = diffs_for(merge_request_with_multiple_diffs).minimum(:id)
+ mr_id = merge_request_with_multiple_diffs.id
+
+ merge_request_with_multiple_diffs.update!(latest_merge_request_diff_id: diff_id)
+
+ expect { subject.perform(mr_id, mr_id) }
+ .not_to change { merge_request_with_multiple_diffs.reload.latest_merge_request_diff_id }
+ end
+
+ it 'migrates multiple MR diffs to the correct values' do
+ merge_requests = Array.new(3).map.with_index { |_, i| create_mr!(i, diffs: 3) }
+
+ subject.perform(merge_requests.first.id, merge_requests.last.id)
+
+ merge_requests.each do |merge_request|
+ expect(merge_request.reload.latest_merge_request_diff_id)
+ .to eq(diffs_for(merge_request).maximum(:id))
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb
index 422f2af7266..b68301a066a 100644
--- a/spec/lib/gitlab/backup/manager_spec.rb
+++ b/spec/lib/gitlab/backup/manager_spec.rb
@@ -172,10 +172,6 @@ describe Backup::Manager do
end
describe '#unpack' do
- before do
- allow(Dir).to receive(:chdir)
- end
-
context 'when there are no backup files in the directory' do
before do
allow(Dir).to receive(:glob).and_return([])
diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
new file mode 100644
index 00000000000..7f3bf5fc41c
--- /dev/null
+++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
@@ -0,0 +1,168 @@
+require 'spec_helper'
+
+describe Gitlab::BareRepositoryImport::Importer, repository: true do
+ let!(:admin) { create(:admin) }
+ let!(:base_dir) { Dir.mktmpdir + '/' }
+ let(:bare_repository) { Gitlab::BareRepositoryImport::Repository.new(base_dir, File.join(base_dir, "#{project_path}.git")) }
+
+ subject(:importer) { described_class.new(admin, bare_repository) }
+
+ before do
+ allow(described_class).to receive(:log)
+ end
+
+ after do
+ FileUtils.rm_rf(base_dir)
+ end
+
+ shared_examples 'importing a repository' do
+ describe '.execute' do
+ it 'creates a project for a repository in storage' do
+ FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git"))
+ fake_importer = double
+
+ expect(described_class).to receive(:new).and_return(fake_importer)
+ expect(fake_importer).to receive(:create_project_if_needed)
+
+ described_class.execute(base_dir)
+ end
+
+ it 'skips wiki repos' do
+ repo_dir = File.join(base_dir, 'the-group', 'the-project.wiki.git')
+ FileUtils.mkdir_p(File.join(repo_dir))
+
+ expect(described_class).to receive(:log).with(" * Skipping repo #{repo_dir}")
+ expect(described_class).not_to receive(:new)
+
+ described_class.execute(base_dir)
+ end
+
+ context 'without admin users' do
+ let(:admin) { nil }
+
+ it 'raises an error' do
+ expect { described_class.execute(base_dir) }.to raise_error(Gitlab::BareRepositoryImport::Importer::NoAdminError)
+ end
+ end
+ end
+
+ describe '#create_project_if_needed' do
+ it 'starts an import for a project that did not exist' do
+ expect(importer).to receive(:create_project)
+
+ importer.create_project_if_needed
+ end
+
+ it 'skips importing when the project already exists' do
+ project = create(:project, path: 'a-project', namespace: existing_group)
+
+ expect(importer).not_to receive(:create_project)
+ expect(importer).to receive(:log).with(" * #{project.name} (#{project_path}) exists")
+
+ importer.create_project_if_needed
+ end
+
+ it 'creates a project with the correct path in the database' do
+ importer.create_project_if_needed
+
+ expect(Project.find_by_full_path(project_path)).not_to be_nil
+ end
+
+ it 'creates the Git repo in disk' do
+ FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git"))
+
+ importer.create_project_if_needed
+
+ project = Project.find_by_full_path(project_path)
+
+ expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.git'))
+ end
+
+ context 'hashed storage enabled' do
+ it 'creates a project with the correct path in the database' do
+ stub_application_setting(hashed_storage_enabled: true)
+
+ importer.create_project_if_needed
+
+ expect(Project.find_by_full_path(project_path)).not_to be_nil
+ end
+ end
+ end
+ end
+
+ context 'with subgroups', :nested_groups do
+ let(:project_path) { 'a-group/a-sub-group/a-project' }
+
+ let(:existing_group) do
+ group = create(:group, path: 'a-group')
+ create(:group, path: 'a-sub-group', parent: group)
+ end
+
+ it_behaves_like 'importing a repository'
+ end
+
+ context 'without subgroups' do
+ let(:project_path) { 'a-group/a-project' }
+ let(:existing_group) { create(:group, path: 'a-group') }
+
+ it_behaves_like 'importing a repository'
+ end
+
+ context 'without groups' do
+ let(:project_path) { 'a-project' }
+
+ it 'starts an import for a project that did not exist' do
+ expect(importer).to receive(:create_project)
+
+ importer.create_project_if_needed
+ end
+
+ it 'creates a project with the correct path in the database' do
+ importer.create_project_if_needed
+
+ expect(Project.find_by_full_path("#{admin.full_path}/#{project_path}")).not_to be_nil
+ end
+
+ it 'creates the Git repo in disk' do
+ FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git"))
+
+ importer.create_project_if_needed
+
+ project = Project.find_by_full_path("#{admin.full_path}/#{project_path}")
+
+ expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.git'))
+ end
+ end
+
+ context 'with Wiki' do
+ let(:project_path) { 'a-group/a-project' }
+ let(:existing_group) { create(:group, path: 'a-group') }
+
+ it_behaves_like 'importing a repository'
+
+ it 'creates the Wiki git repo in disk' do
+ FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.git"))
+ FileUtils.mkdir_p(File.join(base_dir, "#{project_path}.wiki.git"))
+
+ importer.create_project_if_needed
+
+ project = Project.find_by_full_path(project_path)
+
+ expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.wiki.git'))
+ end
+ end
+
+ context 'when subgroups are not available' do
+ let(:project_path) { 'a-group/a-sub-group/a-project' }
+
+ before do
+ expect(Group).to receive(:supports_nested_groups?) { false }
+ end
+
+ describe '#create_project_if_needed' do
+ it 'raises an error' do
+ expect { importer.create_project_if_needed }.to raise_error('Nested groups are not supported on MySQL')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
new file mode 100644
index 00000000000..2db737f5fb6
--- /dev/null
+++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe ::Gitlab::BareRepositoryImport::Repository do
+ let(:project_repo_path) { described_class.new('/full/path/', '/full/path/to/repo.git') }
+
+ it 'stores the repo path' do
+ expect(project_repo_path.repo_path).to eq('/full/path/to/repo.git')
+ end
+
+ it 'stores the group path' do
+ expect(project_repo_path.group_path).to eq('to')
+ end
+
+ it 'stores the project name' do
+ expect(project_repo_path.project_name).to eq('repo')
+ end
+
+ it 'stores the wiki path' do
+ expect(project_repo_path.wiki_path).to eq('/full/path/to/repo.wiki.git')
+ end
+
+ describe '#wiki?' do
+ it 'returns true if it is a wiki' do
+ wiki_path = described_class.new('/full/path/', '/full/path/to/a/b/my.wiki.git')
+
+ expect(wiki_path.wiki?).to eq(true)
+ end
+
+ it 'returns false if it is not a wiki' do
+ expect(project_repo_path.wiki?).to eq(false)
+ end
+ end
+
+ describe '#hashed?' do
+ it 'returns true if it is a hashed folder' do
+ path = described_class.new('/full/path/', '/full/path/@hashed/my.repo.git')
+
+ expect(path.hashed?).to eq(true)
+ end
+
+ it 'returns false if it is not a hashed folder' do
+ expect(project_repo_path.hashed?).to eq(false)
+ end
+ end
+
+ describe '#project_full_path' do
+ it 'returns the project full path' do
+ expect(project_repo_path.repo_path).to eq('/full/path/to/repo.git')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/bare_repository_importer_spec.rb b/spec/lib/gitlab/bare_repository_importer_spec.rb
deleted file mode 100644
index 36d1844b5b1..00000000000
--- a/spec/lib/gitlab/bare_repository_importer_spec.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::BareRepositoryImporter, repository: true do
- subject(:importer) { described_class.new('default', project_path) }
-
- let!(:admin) { create(:admin) }
-
- before do
- allow(described_class).to receive(:log)
- end
-
- shared_examples 'importing a repository' do
- describe '.execute' do
- it 'creates a project for a repository in storage' do
- FileUtils.mkdir_p(File.join(TestEnv.repos_path, "#{project_path}.git"))
- fake_importer = double
-
- expect(described_class).to receive(:new).with('default', project_path)
- .and_return(fake_importer)
- expect(fake_importer).to receive(:create_project_if_needed)
-
- described_class.execute
- end
-
- it 'skips wiki repos' do
- FileUtils.mkdir_p(File.join(TestEnv.repos_path, 'the-group', 'the-project.wiki.git'))
-
- expect(described_class).to receive(:log).with(' * Skipping wiki repo')
- expect(described_class).not_to receive(:new)
-
- described_class.execute
- end
- end
-
- describe '#initialize' do
- context 'without admin users' do
- let(:admin) { nil }
-
- it 'raises an error' do
- expect { importer }.to raise_error(Gitlab::BareRepositoryImporter::NoAdminError)
- end
- end
- end
-
- describe '#create_project_if_needed' do
- it 'starts an import for a project that did not exist' do
- expect(importer).to receive(:create_project)
-
- importer.create_project_if_needed
- end
-
- it 'skips importing when the project already exists' do
- project = create(:project, path: 'a-project', namespace: existing_group)
-
- expect(importer).not_to receive(:create_project)
- expect(importer).to receive(:log).with(" * #{project.name} (#{project_path}) exists")
-
- importer.create_project_if_needed
- end
-
- it 'creates a project with the correct path in the database' do
- importer.create_project_if_needed
-
- expect(Project.find_by_full_path(project_path)).not_to be_nil
- end
- end
- end
-
- context 'with subgroups', :nested_groups do
- let(:project_path) { 'a-group/a-sub-group/a-project' }
-
- let(:existing_group) do
- group = create(:group, path: 'a-group')
- create(:group, path: 'a-sub-group', parent: group)
- end
-
- it_behaves_like 'importing a repository'
- end
-
- context 'without subgroups' do
- let(:project_path) { 'a-group/a-project' }
- let(:existing_group) { create(:group, path: 'a-group') }
-
- it_behaves_like 'importing a repository'
- end
-
- context 'when subgroups are not available' do
- let(:project_path) { 'a-group/a-sub-group/a-project' }
-
- before do
- expect(Group).to receive(:supports_nested_groups?) { false }
- end
-
- describe '#create_project_if_needed' do
- it 'raises an error' do
- expect { importer.create_project_if_needed }.to raise_error('Nested groups are not supported on MySQL')
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index a66347ead76..a6a1d9e619f 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -54,11 +54,13 @@ describe Gitlab::BitbucketImport::Importer do
create(
:project,
import_source: project_identifier,
+ import_url: "https://bitbucket.org/#{project_identifier}.git",
import_data_attributes: { credentials: data }
)
end
let(:importer) { described_class.new(project) }
+ let(:gitlab_shell) { double }
let(:issues_statuses_sample_data) do
{
@@ -67,6 +69,10 @@ describe Gitlab::BitbucketImport::Importer do
}
end
+ before do
+ allow(importer).to receive(:gitlab_shell) { gitlab_shell }
+ end
+
context 'issues statuses' do
before do
# HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this
@@ -110,15 +116,36 @@ describe Gitlab::BitbucketImport::Importer do
end
it 'maps statuses to open or closed' do
+ allow(importer).to receive(:import_wiki)
+
importer.execute
expect(project.issues.where(state: "closed").size).to eq(5)
expect(project.issues.where(state: "opened").size).to eq(2)
end
- it 'calls import_wiki' do
- expect(importer).to receive(:import_wiki)
- importer.execute
+ describe 'wiki import' do
+ it 'is skipped when the wiki exists' do
+ expect(project.wiki).to receive(:repository_exists?) { true }
+ expect(importer.gitlab_shell).not_to receive(:import_repository)
+
+ importer.execute
+
+ expect(importer.errors).to be_empty
+ end
+
+ it 'imports to the project disk_path' do
+ expect(project.wiki).to receive(:repository_exists?) { false }
+ expect(importer.gitlab_shell).to receive(:import_repository).with(
+ project.repository_storage_path,
+ project.wiki.disk_path,
+ project.import_url + '/wiki'
+ )
+
+ importer.execute
+
+ expect(importer.errors).to be_empty
+ end
end
end
end
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index 6c25b7349e1..c2bca816aae 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -11,13 +11,13 @@ describe Gitlab::Checks::ChangeAccess do
let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } }
let(:protocol) { 'ssh' }
- subject do
+ subject(:change_access) do
described_class.new(
changes,
project: project,
user_access: user_access,
protocol: protocol
- ).exec
+ )
end
before do
@@ -26,7 +26,7 @@ describe Gitlab::Checks::ChangeAccess do
context 'without failed checks' do
it "doesn't raise an error" do
- expect { subject }.not_to raise_error
+ expect { subject.exec }.not_to raise_error
end
end
@@ -34,7 +34,7 @@ describe Gitlab::Checks::ChangeAccess do
it 'raises an error' do
expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
end
end
@@ -45,7 +45,7 @@ describe Gitlab::Checks::ChangeAccess do
allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true)
expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false)
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.')
end
context 'with protected tag' do
@@ -61,7 +61,7 @@ describe Gitlab::Checks::ChangeAccess do
let(:newrev) { '0000000000000000000000000000000000000000' }
it 'is prevented' do
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be deleted/)
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be deleted/)
end
end
@@ -70,7 +70,7 @@ describe Gitlab::Checks::ChangeAccess do
let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
it 'is prevented' do
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be updated/)
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be updated/)
end
end
end
@@ -81,14 +81,14 @@ describe Gitlab::Checks::ChangeAccess do
let(:ref) { 'refs/tags/v9.1.0' }
it 'prevents creation below access level' do
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /allowed to create this tag as it is protected/)
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /allowed to create this tag as it is protected/)
end
context 'when user has access' do
let!(:protected_tag) { create(:protected_tag, :developers_can_create, project: project, name: 'v*') }
it 'allows tag creation' do
- expect { subject }.not_to raise_error
+ expect { subject.exec }.not_to raise_error
end
end
end
@@ -101,7 +101,7 @@ describe Gitlab::Checks::ChangeAccess do
let(:ref) { 'refs/heads/master' }
it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'The default branch of a project cannot be deleted.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'The default branch of a project cannot be deleted.')
end
end
@@ -114,7 +114,7 @@ describe Gitlab::Checks::ChangeAccess do
it 'raises an error if the user is not allowed to do forced pushes to protected branches' do
expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true)
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to force push code to a protected branch on this project.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to force push code to a protected branch on this project.')
end
it 'raises an error if the user is not allowed to merge to protected branches' do
@@ -122,13 +122,13 @@ describe Gitlab::Checks::ChangeAccess do
expect(user_access).to receive(:can_merge_to_branch?).and_return(false)
expect(user_access).to receive(:can_push_to_branch?).and_return(false)
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to merge code into protected branches on this project.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to merge code into protected branches on this project.')
end
it 'raises an error if the user is not allowed to push to protected branches' do
expect(user_access).to receive(:can_push_to_branch?).and_return(false)
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.')
end
context 'branch deletion' do
@@ -137,7 +137,7 @@ describe Gitlab::Checks::ChangeAccess do
context 'if the user is not allowed to delete protected branches' do
it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.')
end
end
@@ -150,18 +150,32 @@ describe Gitlab::Checks::ChangeAccess do
let(:protocol) { 'web' }
it 'allows branch deletion' do
- expect { subject }.not_to raise_error
+ expect { subject.exec }.not_to raise_error
end
end
context 'over SSH or HTTP' do
it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only delete protected branches using the web interface.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only delete protected branches using the web interface.')
end
end
end
end
end
end
+
+ context 'LFS integrity check' do
+ it 'fails if any LFS blobs are missing' do
+ allow_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).and_return(true)
+
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /LFS objects are missing/)
+ end
+
+ it 'succeeds if LFS objects have already been uploaded' do
+ allow_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).and_return(false)
+
+ expect { subject.exec }.not_to raise_error
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb
index 2c7ef622c51..633e319f46d 100644
--- a/spec/lib/gitlab/checks/force_push_spec.rb
+++ b/spec/lib/gitlab/checks/force_push_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Checks::ForcePush do
let(:project) { create(:project, :repository) }
- context "exit code checking", skip_gitaly_mock: true do
+ context "exit code checking", :skip_gitaly_mock do
it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['normal output', 0])
diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
new file mode 100644
index 00000000000..17756621221
--- /dev/null
+++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Gitlab::Checks::LfsIntegrity do
+ include ProjectForksHelper
+ let(:project) { create(:project, :repository) }
+ let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+
+ subject { described_class.new(project, newrev) }
+
+ describe '#objects_missing?' do
+ let(:blob_object) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') }
+
+ before do
+ allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects) do |&lazy_block|
+ lazy_block.call([blob_object.id])
+ end
+ end
+
+ context 'with LFS not enabled' do
+ it 'skips integrity check' do
+ expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects)
+
+ subject.objects_missing?
+ end
+ end
+
+ context 'with LFS enabled' do
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ end
+
+ context 'deletion' do
+ let(:newrev) { nil }
+
+ it 'skips integrity check' do
+ expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects)
+
+ expect(subject.objects_missing?).to be_falsey
+ end
+ end
+
+ it 'is true if any LFS blobs are missing' do
+ expect(subject.objects_missing?).to be_truthy
+ end
+
+ it 'is false if LFS objects have already been uploaded' do
+ lfs_object = create(:lfs_object, oid: blob_object.lfs_oid)
+ create(:lfs_objects_project, project: project, lfs_object: lfs_object)
+
+ expect(subject.objects_missing?).to be_falsey
+ end
+ end
+
+ context 'for forked project' do
+ let(:parent_project) { create(:project, :repository) }
+ let(:project) { fork_project(parent_project, nil, repository: true) }
+
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ end
+
+ it 'is true parent project is missing LFS objects' do
+ expect(subject.objects_missing?).to be_truthy
+ end
+
+ it 'is false parent project already conatins LFS objects for the fork' do
+ lfs_object = create(:lfs_object, oid: blob_object.lfs_oid)
+ create(:lfs_objects_project, project: parent_project, lfs_object: lfs_object)
+
+ expect(subject.objects_missing?).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb
index 15eb01eb472..4884d5f8ba4 100644
--- a/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb
@@ -4,11 +4,24 @@ describe Gitlab::Ci::Build::Policy::Kubernetes do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when kubernetes service is active' do
- set(:project) { create(:kubernetes_project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'is satisfied by a kubernetes pipeline' do
+ expect(described_class.new('active'))
+ .to be_satisfied_by(pipeline)
+ end
+ end
- it 'is satisfied by a kubernetes pipeline' do
- expect(described_class.new('active'))
- .to be_satisfied_by(pipeline)
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index 809fda11879..2a3f7807fdb 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -77,8 +77,20 @@ describe Gitlab::Ci::CronParser do
it_behaves_like "returns time in the future"
- it 'converts time in server time zone' do
- expect(subject.hour).to eq(hour_in_utc)
+ context 'when PST (Pacific Standard Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 1, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
+ end
+
+ context 'when PDT (Pacific Daylight Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 6, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
end
end
end
@@ -100,8 +112,20 @@ describe Gitlab::Ci::CronParser do
it_behaves_like "returns time in the future"
- it 'converts time in server time zone' do
- expect(subject.hour).to eq(hour_in_utc)
+ context 'when CET (Central European Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 1, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
+ end
+
+ context 'when CEST (Central European Summer Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 6, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
end
end
@@ -111,8 +135,20 @@ describe Gitlab::Ci::CronParser do
it_behaves_like "returns time in the future"
- it 'converts time in server time zone' do
- expect(subject.hour).to eq(hour_in_utc)
+ context 'when EST (Eastern Standard Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 1, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
+ end
+
+ context 'when EDT (Eastern Daylight Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 6, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
index 5a7a42d84c0..9cdebaa5cf2 100644
--- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
@@ -66,7 +66,7 @@ describe Gitlab::Ci::Status::Build::Cancelable do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'icon_action_cancel' }
+ it { expect(subject.action_icon).to eq 'cancel' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 8768302eda1..d196bc6a4c2 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'passed'
- expect(status.icon).to eq 'icon_status_success'
+ expect(status.icon).to eq 'status_success'
expect(status.favicon).to eq 'favicon_status_success'
expect(status.label).to eq 'passed'
expect(status).to have_details
@@ -57,7 +57,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
- expect(status.icon).to eq 'icon_status_failed'
+ expect(status.icon).to eq 'status_failed'
expect(status.favicon).to eq 'favicon_status_failed'
expect(status.label).to eq 'failed'
expect(status).to have_details
@@ -84,7 +84,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
- expect(status.icon).to eq 'icon_status_warning'
+ expect(status.icon).to eq 'status_warning'
expect(status.favicon).to eq 'favicon_status_failed'
expect(status.label).to eq 'failed (allowed to fail)'
expect(status).to have_details
@@ -113,7 +113,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'canceled'
- expect(status.icon).to eq 'icon_status_canceled'
+ expect(status.icon).to eq 'status_canceled'
expect(status.favicon).to eq 'favicon_status_canceled'
expect(status.label).to eq 'canceled'
expect(status).to have_details
@@ -139,7 +139,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'running'
- expect(status.icon).to eq 'icon_status_running'
+ expect(status.icon).to eq 'status_running'
expect(status.favicon).to eq 'favicon_status_running'
expect(status.label).to eq 'running'
expect(status).to have_details
@@ -165,7 +165,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'pending'
- expect(status.icon).to eq 'icon_status_pending'
+ expect(status.icon).to eq 'status_pending'
expect(status.favicon).to eq 'favicon_status_pending'
expect(status.label).to eq 'pending'
expect(status).to have_details
@@ -190,7 +190,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'skipped'
- expect(status.icon).to eq 'icon_status_skipped'
+ expect(status.icon).to eq 'status_skipped'
expect(status.favicon).to eq 'favicon_status_skipped'
expect(status.label).to eq 'skipped'
expect(status).to have_details
@@ -219,7 +219,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'manual'
expect(status.group).to eq 'manual'
- expect(status.icon).to eq 'icon_status_manual'
+ expect(status.icon).to eq 'status_manual'
expect(status.favicon).to eq 'favicon_status_manual'
expect(status.label).to include 'manual play action'
expect(status).to have_details
@@ -274,7 +274,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'manual'
expect(status.group).to eq 'manual'
- expect(status.icon).to eq 'icon_status_manual'
+ expect(status.icon).to eq 'status_manual'
expect(status.favicon).to eq 'favicon_status_manual'
expect(status.label).to eq 'manual stop action (not allowed)'
expect(status).to have_details
diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
index 20f71459738..99a5a7e4aca 100644
--- a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Build::FailedAllowed do
describe '#icon' do
it 'returns a warning icon' do
- expect(subject.icon).to eq 'icon_status_warning'
+ expect(subject.icon).to eq 'status_warning'
end
end
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index 32b2e62e4e0..81d5f553fd1 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -46,7 +46,7 @@ describe Gitlab::Ci::Status::Build::Play do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'icon_action_play' }
+ it { expect(subject.action_icon).to eq 'play' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
index 21026f2c968..14d42e0d70f 100644
--- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
@@ -66,7 +66,7 @@ describe Gitlab::Ci::Status::Build::Retryable do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'icon_action_retry' }
+ it { expect(subject.action_icon).to eq 'retry' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb
index e0425103f41..18e250772f0 100644
--- a/spec/lib/gitlab/ci/status/build/stop_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb
@@ -38,7 +38,7 @@ describe Gitlab::Ci::Status::Build::Stop do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'icon_action_stop' }
+ it { expect(subject.action_icon).to eq 'stop' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb
index 530639a5897..dc74d7e28c5 100644
--- a/spec/lib/gitlab/ci/status/canceled_spec.rb
+++ b/spec/lib/gitlab/ci/status/canceled_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Canceled do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_canceled' }
+ it { expect(subject.icon).to eq 'status_canceled' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb
index aef982e17f1..ce4333f2aca 100644
--- a/spec/lib/gitlab/ci/status/created_spec.rb
+++ b/spec/lib/gitlab/ci/status/created_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Created do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_created' }
+ it { expect(subject.icon).to eq 'status_created' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb
index 9a25743885c..a4a92117c7f 100644
--- a/spec/lib/gitlab/ci/status/failed_spec.rb
+++ b/spec/lib/gitlab/ci/status/failed_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Failed do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_failed' }
+ it { expect(subject.icon).to eq 'status_failed' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb
index 6fdc3801d71..0463f2e1aff 100644
--- a/spec/lib/gitlab/ci/status/manual_spec.rb
+++ b/spec/lib/gitlab/ci/status/manual_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Manual do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_manual' }
+ it { expect(subject.icon).to eq 'status_manual' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb
index ffc53f0506b..0e25358dd8a 100644
--- a/spec/lib/gitlab/ci/status/pending_spec.rb
+++ b/spec/lib/gitlab/ci/status/pending_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Pending do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_pending' }
+ it { expect(subject.icon).to eq 'status_pending' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb
index 0babf1fb54e..9c9d431bb5d 100644
--- a/spec/lib/gitlab/ci/status/running_spec.rb
+++ b/spec/lib/gitlab/ci/status/running_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Running do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_running' }
+ it { expect(subject.icon).to eq 'status_running' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb
index 670747c9f0b..63694ca0ea6 100644
--- a/spec/lib/gitlab/ci/status/skipped_spec.rb
+++ b/spec/lib/gitlab/ci/status/skipped_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Skipped do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_skipped' }
+ it { expect(subject.icon).to eq 'status_skipped' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb
index ff65b074808..2f67df71c4f 100644
--- a/spec/lib/gitlab/ci/status/success_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Success do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_success' }
+ it { expect(subject.icon).to eq 'status_success' }
end
describe '#favicon' do
diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb
index 7e2269397c6..4582354e739 100644
--- a/spec/lib/gitlab/ci/status/success_warning_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::SuccessWarning do
end
describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_warning' }
+ it { expect(subject.icon).to eq 'status_warning' }
end
describe '#group' do
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index d72f8553f55..98880fe9f28 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -178,15 +178,29 @@ module Gitlab
end
context 'when kubernetes is active' do
- let(:project) { create(:kubernetes_project) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns seeds for kubernetes dependent job' do
+ seeds = subject.stage_seeds(pipeline)
- 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
- 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'
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/lib/gitlab/conflict/file_collection_spec.rb b/spec/lib/gitlab/conflict/file_collection_spec.rb
index a4d7628b03a..5944ce8049a 100644
--- a/spec/lib/gitlab/conflict/file_collection_spec.rb
+++ b/spec/lib/gitlab/conflict/file_collection_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::Conflict::FileCollection do
let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start') }
- let(:file_collection) { described_class.read_only(merge_request) }
+ let(:file_collection) { described_class.new(merge_request) }
describe '#files' do
it 'returns an array of Conflict::Files' do
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index 5356e9742b4..92792144429 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -8,9 +8,10 @@ describe Gitlab::Conflict::File do
let(:our_commit) { rugged.branches['conflict-resolvable'].target }
let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) }
let(:index) { rugged.merge_commits(our_commit, their_commit) }
- let(:conflict) { index.conflicts.last }
- let(:merge_file_result) { index.merge_file('files/ruby/regex.rb') }
- let(:conflict_file) { described_class.new(merge_file_result, conflict, merge_request: merge_request) }
+ let(:rugged_conflict) { index.conflicts.last }
+ let(:raw_conflict_content) { index.merge_file('files/ruby/regex.rb')[:data] }
+ let(:raw_conflict_file) { Gitlab::Git::Conflict::File.new(repository, our_commit.oid, rugged_conflict, raw_conflict_content) }
+ let(:conflict_file) { described_class.new(raw_conflict_file, merge_request: merge_request) }
describe '#resolve_lines' do
let(:section_keys) { conflict_file.sections.map { |section| section[:id] }.compact }
@@ -48,18 +49,18 @@ describe Gitlab::Conflict::File do
end
end
- it 'raises MissingResolution when passed a hash without resolutions for all sections' do
+ it 'raises ResolutionError when passed a hash without resolutions for all sections' do
empty_hash = section_keys.map { |key| [key, nil] }.to_h
invalid_hash = section_keys.map { |key| [key, 'invalid'] }.to_h
expect { conflict_file.resolve_lines({}) }
- .to raise_error(Gitlab::Conflict::File::MissingResolution)
+ .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
expect { conflict_file.resolve_lines(empty_hash) }
- .to raise_error(Gitlab::Conflict::File::MissingResolution)
+ .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
expect { conflict_file.resolve_lines(invalid_hash) }
- .to raise_error(Gitlab::Conflict::File::MissingResolution)
+ .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
end
end
@@ -83,6 +84,13 @@ describe Gitlab::Conflict::File do
expect(line.text).to eq(html_to_text(line.rich_text))
end
end
+
+ # This spec will break if Rouge's highlighting changes, but we need to
+ # ensure that the lines are actually highlighted.
+ it 'highlights the lines correctly' do
+ expect(conflict_file.lines.first.rich_text)
+ .to eq("<span id=\"LC1\" class=\"line\" lang=\"ruby\"><span class=\"k\">module</span> <span class=\"nn\">Gitlab</span></span>\n")
+ end
end
describe '#sections' do
@@ -144,7 +152,7 @@ describe Gitlab::Conflict::File do
end
context 'with an example file' do
- let(:file) do
+ let(:raw_conflict_content) do
<<FILE
# Ensure there is no match line header here
def username_regexp
@@ -220,7 +228,6 @@ end
FILE
end
- let(:conflict_file) { described_class.new({ data: file }, conflict, merge_request: merge_request) }
let(:sections) { conflict_file.sections }
it 'sets the correct match line headers' do
diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb
deleted file mode 100644
index fce606a2bb5..00000000000
--- a/spec/lib/gitlab/conflict/parser_spec.rb
+++ /dev/null
@@ -1,222 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Conflict::Parser do
- let(:parser) { described_class.new }
-
- describe '#parse' do
- def parse_text(text)
- parser.parse(text, our_path: 'README.md', their_path: 'README.md')
- end
-
- context 'when the file has valid conflicts' do
- let(:text) do
- <<CONFLICT
-module Gitlab
- module Regexp
- extend self
-
- def username_regexp
- default_regexp
- end
-
-<<<<<<< files/ruby/regex.rb
- def project_name_regexp
- /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/
- end
-
- def name_regexp
- /\A[a-zA-Z0-9_\-\. ]*\z/
-=======
- def project_name_regex
- %r{\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z}
- end
-
- def name_regex
- %r{\A[a-zA-Z0-9_\-\. ]*\z}
->>>>>>> files/ruby/regex.rb
- end
-
- def path_regexp
- default_regexp
- end
-
-<<<<<<< files/ruby/regex.rb
- def archive_formats_regexp
- /(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
-=======
- def archive_formats_regex
- %r{(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)}
->>>>>>> files/ruby/regex.rb
- end
-
- def git_reference_regexp
- # Valid git ref regexp, see:
- # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
- %r{
- (?!
- (?# doesn't begins with)
- \/| (?# rule #6)
- (?# doesn't contain)
- .*(?:
- [\/.]\.| (?# rule #1,3)
- \/\/| (?# rule #6)
- @\{| (?# rule #8)
- \\ (?# rule #9)
- )
- )
- [^\000-\040\177~^:?*\[]+ (?# rule #4-5)
- (?# doesn't end with)
- (?<!\.lock) (?# rule #1)
- (?<![\/.]) (?# rule #6-7)
- }x
- end
-
- protected
-
-<<<<<<< files/ruby/regex.rb
- def default_regexp
- /\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
-=======
- def default_regex
- %r{\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z}
->>>>>>> files/ruby/regex.rb
- end
- end
-end
-CONFLICT
- end
-
- let(:lines) do
- parser.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb')
- end
-
- it 'sets our lines as new lines' do
- expect(lines[8..13]).to all(have_attributes(type: 'new'))
- expect(lines[26..27]).to all(have_attributes(type: 'new'))
- expect(lines[56..57]).to all(have_attributes(type: 'new'))
- end
-
- it 'sets their lines as old lines' do
- expect(lines[14..19]).to all(have_attributes(type: 'old'))
- expect(lines[28..29]).to all(have_attributes(type: 'old'))
- expect(lines[58..59]).to all(have_attributes(type: 'old'))
- end
-
- it 'sets non-conflicted lines as both' do
- expect(lines[0..7]).to all(have_attributes(type: nil))
- expect(lines[20..25]).to all(have_attributes(type: nil))
- expect(lines[30..55]).to all(have_attributes(type: nil))
- expect(lines[60..62]).to all(have_attributes(type: nil))
- end
-
- it 'sets consecutive line numbers for index, old_pos, and new_pos' do
- old_line_numbers = lines.select { |line| line.type != 'new' }.map(&:old_pos)
- new_line_numbers = lines.select { |line| line.type != 'old' }.map(&:new_pos)
-
- expect(lines.map(&:index)).to eq(0.upto(62).to_a)
- expect(old_line_numbers).to eq(1.upto(53).to_a)
- expect(new_line_numbers).to eq(1.upto(53).to_a)
- end
- end
-
- context 'when the file contents include conflict delimiters' do
- context 'when there is a non-start delimiter first' do
- it 'raises UnexpectedDelimiter when there is a middle delimiter first' do
- expect { parse_text('=======') }
- .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
- end
-
- it 'raises UnexpectedDelimiter when there is an end delimiter first' do
- expect { parse_text('>>>>>>> README.md') }
- .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
- end
-
- it 'does not raise when there is an end delimiter for a different path first' do
- expect { parse_text('>>>>>>> some-other-path.md') }
- .not_to raise_error
- end
- end
-
- context 'when a start delimiter is followed by a non-middle delimiter' do
- let(:start_text) { "<<<<<<< README.md\n" }
- let(:end_text) { "\n=======\n>>>>>>> README.md" }
-
- it 'raises UnexpectedDelimiter when it is followed by an end delimiter' do
- expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }
- .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
- end
-
- it 'raises UnexpectedDelimiter when it is followed by another start delimiter' do
- expect { parse_text(start_text + start_text + end_text) }
- .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
- end
-
- it 'does not raise when it is followed by a start delimiter for a different path' do
- expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }
- .not_to raise_error
- end
- end
-
- context 'when a middle delimiter is followed by a non-end delimiter' do
- let(:start_text) { "<<<<<<< README.md\n=======\n" }
- let(:end_text) { "\n>>>>>>> README.md" }
-
- it 'raises UnexpectedDelimiter when it is followed by another middle delimiter' do
- expect { parse_text(start_text + '=======' + end_text) }
- .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
- end
-
- it 'raises UnexpectedDelimiter when it is followed by a start delimiter' do
- expect { parse_text(start_text + start_text + end_text) }
- .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
- end
-
- it 'does not raise when it is followed by a start delimiter for another path' do
- expect { parse_text(start_text + '<<<<<<< some-other-path.md' + end_text) }
- .not_to raise_error
- end
- end
-
- it 'raises MissingEndDelimiter when there is no end delimiter at the end' do
- start_text = "<<<<<<< README.md\n=======\n"
-
- expect { parse_text(start_text) }
- .to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
-
- expect { parse_text(start_text + '>>>>>>> some-other-path.md') }
- .to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
- end
- end
-
- context 'other file types' do
- it 'raises UnmergeableFile when lines is blank, indicating a binary file' do
- expect { parse_text('') }
- .to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
-
- expect { parse_text(nil) }
- .to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
- end
-
- it 'raises UnmergeableFile when the file is over 200 KB' do
- expect { parse_text('a' * 204801) }
- .to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
- end
-
- # All text from Rugged has an encoding of ASCII_8BIT, so force that in
- # these strings.
- context 'when the file contains UTF-8 characters' do
- it 'does not raise' do
- expect { parse_text("Espa\xC3\xB1a".force_encoding(Encoding::ASCII_8BIT)) }
- .not_to raise_error
- end
- end
-
- context 'when the file contains non-UTF-8 characters' do
- it 'raises UnsupportedEncoding' do
- expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }
- .to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding)
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index d57ffcae8e1..492659a82b0 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::CurrentSettings do
it 'falls back to DB if Redis returns an empty value' do
expect(ApplicationSetting).to receive(:cached).and_return(nil)
- expect(ApplicationSetting).to receive(:last).and_call_original
+ expect(ApplicationSetting).to receive(:last).and_call_original.twice
expect(current_application_settings).to be_a(ApplicationSetting)
end
diff --git a/spec/lib/gitlab/database/grant_spec.rb b/spec/lib/gitlab/database/grant_spec.rb
index 651da3e8476..5ebf3f399b6 100644
--- a/spec/lib/gitlab/database/grant_spec.rb
+++ b/spec/lib/gitlab/database/grant_spec.rb
@@ -1,16 +1,6 @@
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
@@ -18,13 +8,11 @@ describe Gitlab::Database::Grant do
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)
+ it 'returns false when the user can not create and/or execute a trigger', :postgresql do
+ # In case of MySQL the user may have SUPER permissions, making it
+ # impossible to have `false` returned when running tests; hence we only
+ # run these tests on PostgreSQL.
+ expect(described_class.create_and_execute_trigger?('foo')).to eq(false)
end
end
end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
index 90aa4f63dd5..596cc435bd9 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
@@ -229,7 +229,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca
end
end
- describe '#track_rename', redis: true do
+ describe '#track_rename', :redis do
it 'tracks a rename in redis' do
key = 'rename:FakeRenameReservedPathMigrationV1:namespace'
@@ -246,7 +246,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca
end
end
- describe '#reverts_for_type', redis: true do
+ describe '#reverts_for_type', :redis do
it 'yields for each tracked rename' do
subject.track_rename('project', 'old_path', 'new_path')
subject.track_rename('project', 'old_path2', 'new_path2')
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
index 32ac0b88a9b..1143182531f 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
@@ -241,7 +241,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :
end
end
- describe '#revert_renames', redis: true do
+ describe '#revert_renames', :redis do
it 'renames the routes back to the previous values' do
project = create(:project, :repository, path: 'a-project', namespace: namespace)
subject.rename_namespace(namespace)
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
index 595e06a9748..e850b5cd6a4 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
@@ -87,6 +87,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
end
+ it 'does not move the repositories when hashed storage is enabled' do
+ project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:repository])
+
+ expect(subject).not_to receive(:move_repository)
+
+ subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
+ end
+
it 'moves uploads' do
expect(subject).to receive(:move_uploads)
.with('known-parent/the-path', 'known-parent/the-path0')
@@ -94,6 +102,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
end
+ it 'does not move uploads when hashed storage is enabled for attachments' do
+ project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:attachments])
+
+ expect(subject).not_to receive(:move_uploads)
+
+ subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0')
+ end
+
it 'moves pages' do
expect(subject).to receive(:move_pages)
.with('known-parent/the-path', 'known-parent/the-path0')
@@ -115,7 +131,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr
end
end
- describe '#revert_renames', redis: true do
+ describe '#revert_renames', :redis do
it 'renames the routes back to the previous values' do
subject.rename_project(project)
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 5fa94999d25..fcddfad3f9f 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -202,6 +202,26 @@ describe Gitlab::Database do
it 'handles non-UTF-8 data' do
expect { described_class.bulk_insert('test', [{ a: "\255" }]) }.not_to raise_error
end
+
+ context 'when using PostgreSQL' do
+ before do
+ allow(described_class).to receive(:mysql?).and_return(false)
+ end
+
+ it 'allows the returning of the IDs of the inserted rows' do
+ result = double(:result, values: [['10']])
+
+ expect(connection)
+ .to receive(:execute)
+ .with(/RETURNING id/)
+ .and_return(result)
+
+ ids = described_class
+ .bulk_insert('test', [{ number: 10 }], return_ids: true)
+
+ expect(ids).to eq([10])
+ end
+ end
end
describe '.create_connection_pool' do
@@ -256,4 +276,26 @@ describe Gitlab::Database do
expect(described_class.false_value).to eq 0
end
end
+
+ describe '#sanitize_timestamp' do
+ let(:max_timestamp) { Time.at((1 << 31) - 1) }
+
+ subject { described_class.sanitize_timestamp(timestamp) }
+
+ context 'with a timestamp smaller than MAX_TIMESTAMP_VALUE' do
+ let(:timestamp) { max_timestamp - 10.years }
+
+ it 'returns the given timestamp' do
+ expect(subject).to eq(timestamp)
+ end
+ end
+
+ context 'with a timestamp larger than MAX_TIMESTAMP_VALUE' do
+ let(:timestamp) { max_timestamp + 1.second }
+
+ it 'returns MAX_TIMESTAMP_VALUE' do
+ expect(subject).to eq(max_timestamp)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index c91895cedc3..ff9acfd08b9 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -116,12 +116,8 @@ describe Gitlab::Diff::File do
end
context 'when renamed' do
- let(:commit) { project.commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f') }
- let(:diff_file) { commit.diffs.diff_file_with_new_path('files/js/commit.coffee') }
-
- before do
- allow(diff_file.new_blob).to receive(:id).and_return(diff_file.old_blob.id)
- end
+ let(:commit) { project.commit('94bb47ca1297b7b3731ff2a36923640991e9236f') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('CHANGELOG.md') }
it 'returns false' do
expect(diff_file.content_changed?).to be_falsey
diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb
index 8af49ed50ff..80c8c189665 100644
--- a/spec/lib/gitlab/diff/parser_spec.rb
+++ b/spec/lib/gitlab/diff/parser_spec.rb
@@ -143,4 +143,21 @@ eos
it { expect(parser.parse([])).to eq([]) }
it { expect(parser.parse(nil)).to eq([]) }
end
+
+ describe 'tolerates special diff markers in a content' do
+ it "counts lines correctly" do
+ diff = <<~END
+ --- a/test
+ +++ b/test
+ @@ -1,2 +1,2 @@
+ +ipsum
+ +++ b
+ -ipsum
+ END
+
+ lines = parser.parse(diff.lines).to_a
+
+ expect(lines.size).to eq(3)
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index 9bf54fdecc4..677eb373d22 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -40,7 +40,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
- line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0)
+ line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 0)
expect(subject.line_code(project.repository)).to eq(line_code)
end
@@ -108,7 +108,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
- line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 15)
+ line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 15)
expect(subject.line_code(project.repository)).to eq(line_code)
end
@@ -149,7 +149,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
- line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line)
+ line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, subject.old_line)
expect(subject.line_code(project.repository)).to eq(line_code)
end
@@ -189,7 +189,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
- line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 13, subject.old_line)
+ line_code = Gitlab::Git.diff_line_code(subject.file_path, 13, subject.old_line)
expect(subject.line_code(project.repository)).to eq(line_code)
end
@@ -233,7 +233,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
- line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 5)
+ line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 5)
expect(subject.line_code(project.repository)).to eq(line_code)
end
@@ -274,7 +274,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
- line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line)
+ line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, subject.old_line)
expect(subject.line_code(project.repository)).to eq(line_code)
end
@@ -314,7 +314,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
- line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 4, subject.old_line)
+ line_code = Gitlab::Git.diff_line_code(subject.file_path, 4, subject.old_line)
expect(subject.line_code(project.repository)).to eq(line_code)
end
@@ -357,13 +357,50 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
- line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 0, subject.old_line)
+ line_code = Gitlab::Git.diff_line_code(subject.file_path, 0, subject.old_line)
expect(subject.line_code(project.repository)).to eq(line_code)
end
end
end
+ describe "position for a missing ref" do
+ let(:diff_refs) do
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: "not_existing_sha",
+ head_sha: "existing_sha"
+ )
+ end
+
+ subject do
+ described_class.new(
+ old_path: "files/ruby/feature.rb",
+ new_path: "files/ruby/feature.rb",
+ old_line: 3,
+ new_line: nil,
+ diff_refs: diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "does not raise exception" do
+ expect { subject.diff_file(project.repository) }.not_to raise_error
+ end
+ end
+
+ describe "#diff_line" do
+ it "does not raise exception" do
+ expect { subject.diff_line(project.repository) }.not_to raise_error
+ end
+ end
+
+ describe "#line_code" do
+ it "does not raise exception" do
+ expect { subject.line_code(project.repository) }.not_to raise_error
+ end
+ end
+ end
+
describe "position for a file in the initial commit" do
let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") }
@@ -399,7 +436,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
- line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0)
+ line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 0)
expect(subject.line_code(project.repository)).to eq(line_code)
end
@@ -447,7 +484,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
- line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 0, subject.old_line)
+ line_code = Gitlab::Git.diff_line_code(subject.file_path, 0, subject.old_line)
expect(subject.line_code(project.repository)).to eq(line_code)
end
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index e5138705443..ddc4f6c5b5c 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -1771,9 +1771,9 @@ describe Gitlab::Diff::PositionTracer do
describe "merge of target branch" do
let(:merge_commit) do
- update_file_again_commit
+ second_create_file_commit
- merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project)
+ merge_request = create(:merge_request, source_branch: second_branch_name, target_branch: branch_name, source_project: project)
repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches")
diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index 8b14b227e65..f6e5c55240f 100644
--- a/spec/lib/gitlab/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -6,6 +6,10 @@ describe Gitlab::EncodingHelper do
describe '#encode!' do
[
+ ["nil", nil, nil],
+ ["empty string", "".encode("ASCII-8BIT"), "".encode("UTF-8")],
+ ["invalid utf-8 encoded string", "my bad string\xE5".force_encoding("UTF-8"), "my bad string"],
+ ["frozen non-ascii string", "é".force_encoding("ASCII-8BIT").freeze, "é".encode("UTF-8")],
[
'leaves ascii only string as is',
'ascii only string',
@@ -81,6 +85,9 @@ describe Gitlab::EncodingHelper do
describe '#encode_utf8' do
[
+ ["nil", nil, nil],
+ ["empty string", "".encode("ASCII-8BIT"), "".encode("UTF-8")],
+ ["invalid utf-8 encoded string", "my bad string\xE5".force_encoding("UTF-8"), "my bad stringå"],
[
"encodes valid utf8 encoded string to utf8",
"λ, λ, λ".encode("UTF-8"),
@@ -95,12 +102,18 @@ describe Gitlab::EncodingHelper do
"encodes valid ISO-8859-1 encoded string to utf8",
"Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("ISO-8859-1", "UTF-8"),
"Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("UTF-8")
+ ],
+ [
+ # Test case from https://gitlab.com/gitlab-org/gitlab-ce/issues/39227
+ "Equifax branch name",
+ "refs/heads/Equifax".encode("UTF-8"),
+ "refs/heads/Equifax".encode("UTF-8")
]
].each do |description, test_string, xpect|
it description do
- r = ext_class.encode_utf8(test_string.force_encoding('UTF-8'))
+ r = ext_class.encode_utf8(test_string)
expect(r).to eq(xpect)
- expect(r.encoding.name).to eq('UTF-8')
+ expect(r.encoding.name).to eq('UTF-8') if xpect
end
end
diff --git a/spec/lib/gitlab/fake_application_settings_spec.rb b/spec/lib/gitlab/fake_application_settings_spec.rb
index 34322c2a693..af12e13d36d 100644
--- a/spec/lib/gitlab/fake_application_settings_spec.rb
+++ b/spec/lib/gitlab/fake_application_settings_spec.rb
@@ -1,25 +1,25 @@
require 'spec_helper'
describe Gitlab::FakeApplicationSettings do
- let(:defaults) { { password_authentication_enabled: false, foobar: 'asdf', signup_enabled: true, 'test?' => 123 } }
+ let(:defaults) { { password_authentication_enabled_for_web: false, foobar: 'asdf', signup_enabled: true, 'test?' => 123 } }
subject { described_class.new(defaults) }
it 'wraps OpenStruct variables properly' do
- expect(subject.password_authentication_enabled).to be_falsey
+ expect(subject.password_authentication_enabled_for_web).to be_falsey
expect(subject.signup_enabled).to be_truthy
expect(subject.foobar).to eq('asdf')
end
it 'defines predicate methods' do
- expect(subject.password_authentication_enabled?).to be_falsey
+ expect(subject.password_authentication_enabled_for_web?).to be_falsey
expect(subject.signup_enabled?).to be_truthy
end
it 'predicate method changes when value is updated' do
- subject.password_authentication_enabled = true
+ subject.password_authentication_enabled_for_web = true
- expect(subject.password_authentication_enabled?).to be_truthy
+ expect(subject.password_authentication_enabled_for_web?).to be_truthy
end
it 'does not define a predicate method' do
diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb
index 695fd6f8573..8e524f9b05a 100644
--- a/spec/lib/gitlab/file_detector_spec.rb
+++ b/spec/lib/gitlab/file_detector_spec.rb
@@ -18,6 +18,10 @@ describe Gitlab::FileDetector do
expect(described_class.type_of('README.md')).to eq(:readme)
end
+ it 'returns nil for a README file in a directory' do
+ expect(described_class.type_of('foo/README.md')).to be_nil
+ end
+
it 'returns the type of a changelog file' do
%w(CHANGELOG HISTORY CHANGES NEWS).each do |file|
expect(described_class.type_of(file)).to eq(:changelog)
@@ -52,6 +56,14 @@ describe Gitlab::FileDetector do
end
end
+ it 'returns the type of an issue template' do
+ expect(described_class.type_of('.gitlab/issue_templates/foo.md')).to eq(:issue_template)
+ end
+
+ it 'returns the type of a merge request template' do
+ expect(described_class.type_of('.gitlab/merge_request_templates/foo.md')).to eq(:merge_request_template)
+ end
+
it 'returns nil for an unknown file' do
expect(described_class.type_of('foo.txt')).to be_nil
end
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index 465c2012b05..793228701cf 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -73,7 +73,7 @@ describe Gitlab::Git::Blame, seed_helper: true do
it_behaves_like 'blaming a file'
end
- context 'when Gitaly blame feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly blame feature is disabled', :skip_gitaly_mock do
it_behaves_like 'blaming a file'
end
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index f3945e748ab..c04a9688503 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -112,7 +112,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
it_behaves_like 'finding blobs'
end
- context 'when project_raw_show Gitaly feature is disabled', skip_gitaly_mock: true do
+ context 'when project_raw_show Gitaly feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding blobs'
end
end
@@ -143,6 +143,16 @@ describe Gitlab::Git::Blob, seed_helper: true do
expect(blob.loaded_size).to eq(blob_size)
end
end
+
+ context 'when sha references a tree' do
+ it 'returns nil' do
+ tree = Gitlab::Git::Commit.find(repository, 'master').tree
+
+ blob = Gitlab::Git::Blob.raw(repository, tree.oid)
+
+ expect(blob).to be_nil
+ end
+ end
end
describe '.raw' do
@@ -150,7 +160,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
it_behaves_like 'finding blobs by ID'
end
- context 'when the blob_raw Gitaly feature is disabled', skip_gitaly_mock: true do
+ context 'when the blob_raw Gitaly feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding blobs by ID'
end
end
@@ -226,6 +236,51 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
+ describe '.batch_lfs_pointers' do
+ let(:tree_object) { Gitlab::Git::Commit.find(repository, 'master').tree }
+
+ let(:non_lfs_blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ 'master',
+ 'README.md'
+ )
+ end
+
+ let(:lfs_blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'files/lfs/image.jpg'
+ )
+ end
+
+ it 'returns a list of Gitlab::Git::Blob' do
+ blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id])
+
+ expect(blobs.count).to eq(1)
+ expect(blobs).to all( be_a(Gitlab::Git::Blob) )
+ end
+
+ it 'silently ignores tree objects' do
+ blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid])
+
+ expect(blobs).to eq([])
+ end
+
+ it 'silently ignores non lfs objects' do
+ blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
+
+ expect(blobs).to eq([])
+ end
+
+ it 'avoids loading large blobs into memory' do
+ expect(repository).not_to receive(:lookup)
+
+ described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
+ end
+ end
+
describe 'encoding' do
context 'file with russian text' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") }
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
index 318a7b7a332..708870060e7 100644
--- a/spec/lib/gitlab/git/branch_spec.rb
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -7,6 +7,38 @@ describe Gitlab::Git::Branch, seed_helper: true do
it { is_expected.to be_kind_of Array }
+ describe '.find' do
+ subject { described_class.find(repository, branch) }
+
+ before do
+ allow(repository).to receive(:find_branch).with(branch)
+ .and_call_original
+ end
+
+ context 'when finding branch via branch name' do
+ let(:branch) { 'master' }
+
+ it 'returns a branch object' do
+ expect(subject).to be_a(described_class)
+ expect(subject.name).to eq(branch)
+
+ expect(repository).to have_received(:find_branch).with(branch)
+ end
+ end
+
+ context 'when the branch is already a branch' do
+ let(:commit) { repository.commit('master') }
+ let(:branch) { described_class.new(repository, 'master', commit.sha, commit) }
+
+ it 'returns a branch object' do
+ expect(subject).to be_a(described_class)
+ expect(subject).to eq(branch)
+
+ expect(repository).not_to have_received(:find_branch).with(branch)
+ end
+ end
+ end
+
describe '#size' do
subject { super().size }
it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 3815055139a..9f4e3c49adc 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -261,7 +261,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it_should_behave_like '.where'
end
- describe '.where without gitaly', skip_gitaly_mock: true do
+ describe '.where without gitaly', :skip_gitaly_mock do
it_should_behave_like '.where'
end
@@ -336,7 +336,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it_behaves_like 'finding all commits'
end
- context 'when Gitaly find_all_commits feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly find_all_commits feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding all commits'
context 'while applying a sort order based on the `order` option' do
@@ -405,7 +405,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it_should_behave_like '#stats'
end
- describe '#stats with gitaly disabled', skip_gitaly_mock: true do
+ describe '#stats with gitaly disabled', :skip_gitaly_mock do
it_should_behave_like '#stats'
end
diff --git a/spec/lib/gitlab/git/conflict/parser_spec.rb b/spec/lib/gitlab/git/conflict/parser_spec.rb
new file mode 100644
index 00000000000..7b035a381f1
--- /dev/null
+++ b/spec/lib/gitlab/git/conflict/parser_spec.rb
@@ -0,0 +1,224 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Conflict::Parser do
+ describe '.parse' do
+ def parse_text(text)
+ described_class.parse(text, our_path: 'README.md', their_path: 'README.md')
+ end
+
+ context 'when the file has valid conflicts' do
+ let(:text) do
+ <<CONFLICT
+module Gitlab
+ module Regexp
+ extend self
+
+ def username_regexp
+ default_regexp
+ end
+
+<<<<<<< files/ruby/regex.rb
+ def project_name_regexp
+ /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/
+ end
+
+ def name_regexp
+ /\A[a-zA-Z0-9_\-\. ]*\z/
+=======
+ def project_name_regex
+ %r{\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z}
+ end
+
+ def name_regex
+ %r{\A[a-zA-Z0-9_\-\. ]*\z}
+>>>>>>> files/ruby/regex.rb
+ end
+
+ def path_regexp
+ default_regexp
+ end
+
+<<<<<<< files/ruby/regex.rb
+ def archive_formats_regexp
+ /(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
+=======
+ def archive_formats_regex
+ %r{(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)}
+>>>>>>> files/ruby/regex.rb
+ end
+
+ def git_reference_regexp
+ # Valid git ref regexp, see:
+ # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
+ %r{
+ (?!
+ (?# doesn't begins with)
+ \/| (?# rule #6)
+ (?# doesn't contain)
+ .*(?:
+ [\/.]\.| (?# rule #1,3)
+ \/\/| (?# rule #6)
+ @\{| (?# rule #8)
+ \\ (?# rule #9)
+ )
+ )
+ [^\000-\040\177~^:?*\[]+ (?# rule #4-5)
+ (?# doesn't end with)
+ (?<!\.lock) (?# rule #1)
+ (?<![\/.]) (?# rule #6-7)
+ }x
+ end
+
+ protected
+
+<<<<<<< files/ruby/regex.rb
+ def default_regexp
+ /\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
+=======
+ def default_regex
+ %r{\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z}
+>>>>>>> files/ruby/regex.rb
+ end
+ end
+end
+CONFLICT
+ end
+
+ let(:lines) do
+ described_class.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb')
+ end
+ let(:old_line_numbers) do
+ lines.select { |line| line[:type] != 'new' }.map { |line| line[:line_old] }
+ end
+ let(:new_line_numbers) do
+ lines.select { |line| line[:type] != 'old' }.map { |line| line[:line_new] }
+ end
+ let(:line_indexes) { lines.map { |line| line[:line_obj_index] } }
+
+ it 'sets our lines as new lines' do
+ expect(lines[8..13]).to all(include(type: 'new'))
+ expect(lines[26..27]).to all(include(type: 'new'))
+ expect(lines[56..57]).to all(include(type: 'new'))
+ end
+
+ it 'sets their lines as old lines' do
+ expect(lines[14..19]).to all(include(type: 'old'))
+ expect(lines[28..29]).to all(include(type: 'old'))
+ expect(lines[58..59]).to all(include(type: 'old'))
+ end
+
+ it 'sets non-conflicted lines as both' do
+ expect(lines[0..7]).to all(include(type: nil))
+ expect(lines[20..25]).to all(include(type: nil))
+ expect(lines[30..55]).to all(include(type: nil))
+ expect(lines[60..62]).to all(include(type: nil))
+ end
+
+ it 'sets consecutive line numbers for line_obj_index, line_old, and line_new' do
+ expect(line_indexes).to eq(0.upto(62).to_a)
+ expect(old_line_numbers).to eq(1.upto(53).to_a)
+ expect(new_line_numbers).to eq(1.upto(53).to_a)
+ end
+ end
+
+ context 'when the file contents include conflict delimiters' do
+ context 'when there is a non-start delimiter first' do
+ it 'raises UnexpectedDelimiter when there is a middle delimiter first' do
+ expect { parse_text('=======') }
+ .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter)
+ end
+
+ it 'raises UnexpectedDelimiter when there is an end delimiter first' do
+ expect { parse_text('>>>>>>> README.md') }
+ .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter)
+ end
+
+ it 'does not raise when there is an end delimiter for a different path first' do
+ expect { parse_text('>>>>>>> some-other-path.md') }
+ .not_to raise_error
+ end
+ end
+
+ context 'when a start delimiter is followed by a non-middle delimiter' do
+ let(:start_text) { "<<<<<<< README.md\n" }
+ let(:end_text) { "\n=======\n>>>>>>> README.md" }
+
+ it 'raises UnexpectedDelimiter when it is followed by an end delimiter' do
+ expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }
+ .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter)
+ end
+
+ it 'raises UnexpectedDelimiter when it is followed by another start delimiter' do
+ expect { parse_text(start_text + start_text + end_text) }
+ .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter)
+ end
+
+ it 'does not raise when it is followed by a start delimiter for a different path' do
+ expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }
+ .not_to raise_error
+ end
+ end
+
+ context 'when a middle delimiter is followed by a non-end delimiter' do
+ let(:start_text) { "<<<<<<< README.md\n=======\n" }
+ let(:end_text) { "\n>>>>>>> README.md" }
+
+ it 'raises UnexpectedDelimiter when it is followed by another middle delimiter' do
+ expect { parse_text(start_text + '=======' + end_text) }
+ .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter)
+ end
+
+ it 'raises UnexpectedDelimiter when it is followed by a start delimiter' do
+ expect { parse_text(start_text + start_text + end_text) }
+ .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter)
+ end
+
+ it 'does not raise when it is followed by a start delimiter for another path' do
+ expect { parse_text(start_text + '<<<<<<< some-other-path.md' + end_text) }
+ .not_to raise_error
+ end
+ end
+
+ it 'raises MissingEndDelimiter when there is no end delimiter at the end' do
+ start_text = "<<<<<<< README.md\n=======\n"
+
+ expect { parse_text(start_text) }
+ .to raise_error(Gitlab::Git::Conflict::Parser::MissingEndDelimiter)
+
+ expect { parse_text(start_text + '>>>>>>> some-other-path.md') }
+ .to raise_error(Gitlab::Git::Conflict::Parser::MissingEndDelimiter)
+ end
+ end
+
+ context 'other file types' do
+ it 'raises UnmergeableFile when lines is blank, indicating a binary file' do
+ expect { parse_text('') }
+ .to raise_error(Gitlab::Git::Conflict::Parser::UnmergeableFile)
+
+ expect { parse_text(nil) }
+ .to raise_error(Gitlab::Git::Conflict::Parser::UnmergeableFile)
+ end
+
+ it 'raises UnmergeableFile when the file is over 200 KB' do
+ expect { parse_text('a' * 204801) }
+ .to raise_error(Gitlab::Git::Conflict::Parser::UnmergeableFile)
+ end
+
+ # All text from Rugged has an encoding of ASCII_8BIT, so force that in
+ # these strings.
+ context 'when the file contains UTF-8 characters' do
+ it 'does not raise' do
+ expect { parse_text("Espa\xC3\xB1a".force_encoding(Encoding::ASCII_8BIT)) }
+ .not_to raise_error
+ end
+ end
+
+ context 'when the file contains non-UTF-8 characters' do
+ it 'raises UnsupportedEncoding' do
+ expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }
+ .to raise_error(Gitlab::Git::Conflict::Parser::UnsupportedEncoding)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index ee657101f4c..65edc750f39 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -487,6 +487,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
loop do
break if @count.zero?
+
# It is critical to decrement before yielding. We may never reach the lines after 'yield'.
@count -= 1
yield @value
diff --git a/spec/lib/gitlab/git/env_spec.rb b/spec/lib/gitlab/git/env_spec.rb
index d9df99bfe05..03836d49518 100644
--- a/spec/lib/gitlab/git/env_spec.rb
+++ b/spec/lib/gitlab/git/env_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Git::Env do
- describe "#set" do
+ describe ".set" do
context 'with RequestStore.store disabled' do
before do
allow(RequestStore).to receive(:active?).and_return(false)
@@ -34,25 +34,57 @@ describe Gitlab::Git::Env do
end
end
- describe "#all" do
+ describe ".all" do
context 'with RequestStore.store enabled' do
before do
allow(RequestStore).to receive(:active?).and_return(true)
described_class.set(
GIT_OBJECT_DIRECTORY: 'foo',
- GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar')
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: ['bar'])
end
it 'returns an env hash' do
expect(described_class.all).to eq({
'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => ['bar']
})
end
end
end
- describe "#[]" do
+ describe ".to_env_hash" do
+ context 'with RequestStore.store enabled' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:key) { 'GIT_OBJECT_DIRECTORY' }
+ subject { described_class.to_env_hash }
+
+ where(:input, :output) do
+ nil | nil
+ 'foo' | 'foo'
+ [] | ''
+ ['foo'] | 'foo'
+ %w[foo bar] | 'foo:bar'
+ end
+
+ with_them do
+ before do
+ allow(RequestStore).to receive(:active?).and_return(true)
+ described_class.set(key.to_sym => input)
+ end
+
+ it 'puts the right value in the hash' do
+ if output
+ expect(subject.fetch(key)).to eq(output)
+ else
+ expect(subject.has_key?(key)).to eq(false)
+ end
+ end
+ end
+ end
+ end
+
+ describe ".[]" do
context 'with RequestStore.store enabled' do
before do
allow(RequestStore).to receive(:active?).and_return(true)
diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb
index 51e4e3fdad1..3ed3feb4c74 100644
--- a/spec/lib/gitlab/git/hooks_service_spec.rb
+++ b/spec/lib/gitlab/git/hooks_service_spec.rb
@@ -1,24 +1,26 @@
require 'spec_helper'
describe Gitlab::Git::HooksService, seed_helper: true do
- let(:user) { Gitlab::Git::User.new('janedoe', 'Jane Doe', 'janedoe@example.com', 'user-456') }
+ let(:gl_id) { 'user-456' }
+ let(:gl_username) { 'janedoe' }
+ let(:user) { Gitlab::Git::User.new(gl_username, 'Jane Doe', 'janedoe@example.com', gl_id) }
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') }
let(:service) { described_class.new }
-
- before do
- @blankrev = Gitlab::Git::BLANK_SHA
- @oldrev = SeedRepo::Commit::PARENT_ID
- @newrev = SeedRepo::Commit::ID
- @ref = 'refs/heads/feature'
- end
+ let(:blankrev) { Gitlab::Git::BLANK_SHA }
+ let(:oldrev) { SeedRepo::Commit::PARENT_ID }
+ let(:newrev) { SeedRepo::Commit::ID }
+ let(:ref) { 'refs/heads/feature' }
describe '#execute' do
context 'when receive hooks were successful' do
- it 'calls post-receive hook' do
- hook = double(trigger: [true, nil])
+ let(:hook) { double(:hook) }
+
+ it 'calls all three hooks' do
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
+ expect(hook).to receive(:trigger).with(gl_id, gl_username, blankrev, newrev, ref)
+ .exactly(3).times.and_return([true, nil])
- service.execute(user, repository, @blankrev, @newrev, @ref) { }
+ service.execute(user, repository, blankrev, newrev, ref) { }
end
end
@@ -28,7 +30,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
- service.execute(user, repository, @blankrev, @newrev, @ref)
+ service.execute(user, repository, blankrev, newrev, ref)
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
end
@@ -40,7 +42,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
- service.execute(user, repository, @blankrev, @newrev, @ref)
+ service.execute(user, repository, blankrev, newrev, ref)
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
end
diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb
new file mode 100644
index 00000000000..c9007d7d456
--- /dev/null
+++ b/spec/lib/gitlab/git/lfs_changes_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Git::LfsChanges do
+ let(:project) { create(:project, :repository) }
+ let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+ let(:blob_object_id) { '0c304a93cb8430108629bbbcaa27db3343299bc0' }
+
+ subject { described_class.new(project.repository, newrev) }
+
+ describe 'new_pointers' do
+ before do
+ allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects).and_yield([blob_object_id])
+ end
+
+ it 'uses rev-list to find new objects' do
+ rev_list = double
+ allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list)
+
+ expect(rev_list).to receive(:new_objects).and_return([])
+
+ subject.new_pointers
+ end
+
+ it 'filters new objects to find lfs pointers' do
+ expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id])
+
+ subject.new_pointers(object_limit: 1)
+ end
+
+ it 'limits new_objects using object_limit' do
+ expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [])
+
+ subject.new_pointers(object_limit: 0)
+ end
+ end
+
+ describe 'all_pointers' do
+ it 'uses rev-list to find all objects' do
+ rev_list = double
+ allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list)
+ allow(rev_list).to receive(:all_objects).and_yield([blob_object_id])
+
+ expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id])
+
+ subject.all_pointers
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/popen_spec.rb b/spec/lib/gitlab/git/popen_spec.rb
new file mode 100644
index 00000000000..b033ede9062
--- /dev/null
+++ b/spec/lib/gitlab/git/popen_spec.rb
@@ -0,0 +1,149 @@
+require 'spec_helper'
+
+describe 'Gitlab::Git::Popen' do
+ let(:path) { Rails.root.join('tmp').to_s }
+
+ let(:klass) do
+ Class.new(Object) do
+ include Gitlab::Git::Popen
+ end
+ end
+
+ context 'popen' do
+ context 'zero status' do
+ let(:result) { klass.new.popen(%w(ls), path) }
+ let(:output) { result.first }
+ let(:status) { result.last }
+
+ it { expect(status).to be_zero }
+ it { expect(output).to include('tests') }
+ end
+
+ context 'non-zero status' do
+ let(:result) { klass.new.popen(%w(cat NOTHING), path) }
+ let(:output) { result.first }
+ let(:status) { result.last }
+
+ it { expect(status).to eq(1) }
+ it { expect(output).to include('No such file or directory') }
+ end
+
+ context 'unsafe string command' do
+ it 'raises an error when it gets called with a string argument' do
+ expect { klass.new.popen('ls', path) }.to raise_error(RuntimeError)
+ end
+ end
+
+ context 'with custom options' do
+ let(:vars) { { 'foobar' => 123, 'PWD' => path } }
+ let(:options) { { chdir: path } }
+
+ it 'calls popen3 with the provided environment variables' do
+ expect(Open3).to receive(:popen3).with(vars, 'ls', options)
+
+ klass.new.popen(%w(ls), path, { 'foobar' => 123 })
+ end
+ end
+
+ context 'use stdin' do
+ let(:result) { klass.new.popen(%w[cat], path) { |stdin| stdin.write 'hello' } }
+ let(:output) { result.first }
+ let(:status) { result.last }
+
+ it { expect(status).to be_zero }
+ it { expect(output).to eq('hello') }
+ end
+
+ context 'with lazy block' do
+ it 'yields a lazy io' do
+ expect_lazy_io = lambda do |io|
+ expect(io).to be_a Enumerator::Lazy
+ expect(io.inspect).to include('#<IO:fd')
+ end
+
+ klass.new.popen(%w[ls], path, lazy_block: expect_lazy_io)
+ end
+
+ it "doesn't wait for process exit" do
+ Timeout.timeout(2) do
+ klass.new.popen(%w[yes], path, lazy_block: ->(io) {})
+ end
+ end
+ end
+ end
+
+ context 'popen_with_timeout' do
+ let(:timeout) { 1.second }
+
+ context 'no timeout' do
+ context 'zero status' do
+ let(:result) { klass.new.popen_with_timeout(%w(ls), timeout, path) }
+ let(:output) { result.first }
+ let(:status) { result.last }
+
+ it { expect(status).to be_zero }
+ it { expect(output).to include('tests') }
+ end
+
+ context 'non-zero status' do
+ let(:result) { klass.new.popen_with_timeout(%w(cat NOTHING), timeout, path) }
+ let(:output) { result.first }
+ let(:status) { result.last }
+
+ it { expect(status).to eq(1) }
+ it { expect(output).to include('No such file or directory') }
+ end
+
+ context 'unsafe string command' do
+ it 'raises an error when it gets called with a string argument' do
+ expect { klass.new.popen_with_timeout('ls', timeout, path) }.to raise_error(RuntimeError)
+ end
+ end
+ end
+
+ context 'timeout' do
+ context 'timeout' do
+ it "raises a Timeout::Error" do
+ expect { klass.new.popen_with_timeout(%w(sleep 1000), timeout, path) }.to raise_error(Timeout::Error)
+ end
+
+ it "handles processes that do not shutdown correctly" do
+ expect { klass.new.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error)
+ end
+ end
+
+ context 'timeout period' do
+ let(:time_taken) do
+ begin
+ start = Time.now
+ klass.new.popen_with_timeout(%w(sleep 1000), timeout, path)
+ rescue
+ Time.now - start
+ end
+ end
+
+ it { expect(time_taken).to be >= timeout }
+ end
+
+ context 'clean up' do
+ let(:instance) { klass.new }
+
+ it 'kills the child process' do
+ expect(instance).to receive(:kill_process_group_for_pid).and_wrap_original do |m, *args|
+ # is the PID, and it should not be running at this point
+ m.call(*args)
+
+ pid = args.first
+ begin
+ Process.getpgid(pid)
+ raise "The child process should have been killed"
+ rescue Errno::ESRCH
+ end
+ end
+
+ expect { instance.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/remote_repository_spec.rb b/spec/lib/gitlab/git/remote_repository_spec.rb
new file mode 100644
index 00000000000..0506210887c
--- /dev/null
+++ b/spec/lib/gitlab/git/remote_repository_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+describe Gitlab::Git::RemoteRepository, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ subject { described_class.new(repository) }
+
+ describe '#empty_repo?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:repository, :result) do
+ Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') | false
+ Gitlab::Git::Repository.new('default', 'does-not-exist.git', '') | true
+ end
+
+ with_them do
+ it { expect(subject.empty_repo?).to eq(result) }
+ end
+ end
+
+ describe '#commit_id' do
+ it 'returns an OID if the revision exists' do
+ expect(subject.commit_id('v1.0.0')).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ end
+
+ it 'is nil when the revision does not exist' do
+ expect(subject.commit_id('does-not-exist')).to be_nil
+ end
+ end
+
+ describe '#branch_exists?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:branch, :result) do
+ 'master' | true
+ 'does-not-exist' | false
+ end
+
+ with_them do
+ it { expect(subject.branch_exists?(branch)).to eq(result) }
+ end
+ end
+
+ describe '#same_repository?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:other_repository, :result) do
+ repository | true
+ Gitlab::Git::Repository.new(repository.storage, repository.relative_path, '') | true
+ Gitlab::Git::Repository.new('broken', TEST_REPO_PATH, '') | false
+ Gitlab::Git::Repository.new(repository.storage, 'wrong/relative-path.git', '') | false
+ Gitlab::Git::Repository.new('broken', 'wrong/relative-path.git', '') | false
+ end
+
+ with_them do
+ it { expect(subject.same_repository?(other_repository)).to eq(result) }
+ end
+ end
+
+ describe '#fetch_env' do
+ let(:remote_repository) { described_class.new(repository) }
+
+ let(:gitaly_client) { double(:gitaly_client) }
+ let(:address) { 'fake-address' }
+ let(:token) { 'fake-token' }
+
+ subject { remote_repository.fetch_env }
+
+ before do
+ allow(remote_repository).to receive(:gitaly_client).and_return(gitaly_client)
+
+ expect(gitaly_client).to receive(:address).with(repository.storage).and_return(address)
+ expect(gitaly_client).to receive(:token).with(repository.storage).and_return(token)
+ end
+
+ it { expect(subject).to be_a(Hash) }
+ it { expect(subject['GITALY_ADDRESS']).to eq(address) }
+ it { expect(subject['GITALY_TOKEN']).to eq(token) }
+ it { expect(subject['GITALY_WD']).to eq(Dir.pwd) }
+
+ it 'creates a plausible GIT_SSH_COMMAND' do
+ git_ssh_command = subject['GIT_SSH_COMMAND']
+
+ expect(git_ssh_command).to start_with('/')
+ expect(git_ssh_command).to end_with('/gitaly-ssh upload-pack')
+ end
+
+ it 'creates a plausible GITALY_PAYLOAD' do
+ req = Gitaly::SSHUploadPackRequest.decode_json(subject['GITALY_PAYLOAD'])
+
+ expect(remote_repository.gitaly_repository).to eq(req.repository)
+ end
+
+ context 'when the token is blank' do
+ let(:token) { '' }
+
+ it { expect(subject.keys).not_to include('GITALY_TOKEN') }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 5f12125beb2..2f49bd1bcf2 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -54,7 +54,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe "#rugged" do
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'raises a storage exception when storage is not available' do
broken_repo = described_class.new('broken', 'a/path.git', '')
@@ -68,31 +68,52 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Repository::NoRepository)
end
- context 'with no Git env stored' do
- before do
- expect(Gitlab::Git::Env).to receive(:all).and_return({})
- end
+ describe 'alternates keyword argument' do
+ context 'with no Git env stored' do
+ before do
+ allow(Gitlab::Git::Env).to receive(:all).and_return({})
+ end
- it "whitelist some variables and pass them via the alternates keyword argument" do
- expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: [])
+ it "is passed an empty array" do
+ expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: [])
- repository.rugged
+ repository.rugged
+ end
end
- end
- context 'with some Git env stored' do
- before do
- expect(Gitlab::Git::Env).to receive(:all).and_return({
- 'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar',
- 'GIT_OTHER' => 'another_env'
- })
+ context 'with absolute and relative Git object dir envvars stored' do
+ before do
+ allow(Gitlab::Git::Env).to receive(:all).and_return({
+ 'GIT_OBJECT_DIRECTORY_RELATIVE' => './objects/foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['./objects/bar', './objects/baz'],
+ 'GIT_OBJECT_DIRECTORY' => 'ignored',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => %w[ignored ignored],
+ 'GIT_OTHER' => 'another_env'
+ })
+ end
+
+ it "is passed the relative object dir envvars after being converted to absolute ones" do
+ alternates = %w[foo bar baz].map { |d| File.join(repository.path, './objects', d) }
+ expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: alternates)
+
+ repository.rugged
+ end
end
- it "whitelist some variables and pass them via the alternates keyword argument" do
- expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: %w[foo bar])
+ context 'with only absolute Git object dir envvars stored' do
+ before do
+ allow(Gitlab::Git::Env).to receive(:all).and_return({
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => %w[bar baz],
+ 'GIT_OTHER' => 'another_env'
+ })
+ end
+
+ it "is passed the absolute object dir envvars as is" do
+ expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: %w[foo bar baz])
- repository.rugged
+ repository.rugged
+ end
end
end
end
@@ -384,7 +405,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- context 'when Gitaly commit_count feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly commit_count feature is disabled', :skip_gitaly_mock do
it_behaves_like 'simple commit counting'
end
end
@@ -418,7 +439,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'check for local branches'
end
- context 'without gitaly', skip_gitaly_mock: true do
+ context 'without gitaly', :skip_gitaly_mock do
it_behaves_like 'check for local branches'
end
end
@@ -428,7 +449,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -453,7 +473,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like "deleting a branch"
end
- context "when Gitaly delete_branch is disabled", skip_gitaly_mock: true do
+ context "when Gitaly delete_branch is disabled", :skip_gitaly_mock do
it_behaves_like "deleting a branch"
end
end
@@ -463,7 +483,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -489,7 +508,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'creating a branch'
end
- context 'when Gitaly create_branch feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly create_branch feature is disabled', :skip_gitaly_mock do
it_behaves_like 'creating a branch'
end
end
@@ -523,7 +542,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
@@ -538,10 +556,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe "#remote_delete" do
+ describe "#remove_remote" do
before(:all) do
@repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
- @repo.remote_delete("expendable")
+ @repo.remove_remote("expendable")
end
it "should remove the remote" do
@@ -549,42 +567,107 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
- describe "#remote_add" do
+ describe "#remote_update" do
before(:all) do
@repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
- @repo.remote_add("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL)
+ @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH)
end
it "should add the remote" do
- expect(@repo.rugged.remotes.each_name.to_a).to include("new_remote")
+ expect(@repo.rugged.remotes["expendable"].url).to(
+ eq(TEST_NORMAL_REPO_PATH)
+ )
end
after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
end
- describe "#remote_update" do
+ describe '#fetch_as_mirror_without_shell' do
+ let(:new_repository) do
+ Gitlab::Git::Repository.new('default', 'my_project.git', '')
+ end
+
+ subject { new_repository.fetch_as_mirror_without_shell(repository.path) }
+
+ before do
+ Gitlab::Shell.new.add_repository('default', 'my_project')
+ end
+
+ after do
+ Gitlab::Shell.new.remove_repository(TestEnv.repos_path, 'my_project')
+ end
+
+ it 'fetches a url as a mirror remote' do
+ subject
+
+ expect(refs(new_repository.path)).to eq(refs(repository.path))
+ end
+
+ context 'with keep-around refs' do
+ let(:sha) { SeedRepo::Commit::ID }
+ let(:keep_around_ref) { "refs/keep-around/#{sha}" }
+ let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" }
+
+ before do
+ repository.rugged.references.create(keep_around_ref, sha, force: true)
+ repository.rugged.references.create(tmp_ref, sha, force: true)
+ end
+
+ it 'includes the temporary and keep-around refs' do
+ subject
+
+ expect(refs(new_repository.path)).to include(keep_around_ref)
+ expect(refs(new_repository.path)).to include(tmp_ref)
+ end
+ end
+ end
+
+ describe '#remote_tags' do
+ let(:remote_name) { 'upstream' }
+ let(:target_commit_id) { SeedRepo::Commit::ID }
+ let(:user) { create(:user) }
+ let(:tag_name) { 'v0.0.1' }
+ let(:tag_message) { 'My tag' }
+ let(:remote_repository) do
+ Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
+ end
+
+ subject { repository.remote_tags(remote_name) }
+
+ before do
+ repository.add_remote(remote_name, remote_repository.path)
+ remote_repository.add_tag(tag_name, user: user, target: target_commit_id)
+ end
+
+ after do
+ ensure_seeds
+ end
+
+ it 'gets the remote tags' do
+ expect(subject.first).to be_an_instance_of(Gitlab::Git::Tag)
+ expect(subject.first.name).to eq(tag_name)
+ expect(subject.first.dereferenced_target.id).to eq(target_commit_id)
+ end
+ end
+
+ describe '#remote_exists?' do
before(:all) do
@repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
- @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH)
+ @repo.add_remote("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL)
end
- it "should add the remote" do
- expect(@repo.rugged.remotes["expendable"].url).to(
- eq(TEST_NORMAL_REPO_PATH)
- )
+ it 'returns true for an existing remote' do
+ expect(@repo.remote_exists?('new_remote')).to eq(true)
end
- after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
- ensure_seeds
+ it 'returns false for a non-existing remote' do
+ expect(@repo.remote_exists?('foo')).to eq(false)
end
end
@@ -929,7 +1012,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'extended commit counting'
end
- context 'when Gitaly count_commits feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly count_commits feature is disabled', :skip_gitaly_mock do
it_behaves_like 'extended commit counting'
end
end
@@ -996,7 +1079,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'finding a branch'
end
- context 'when Gitaly find_branch feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly find_branch feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding a branch'
it 'should reload Rugged::Repository and return master' do
@@ -1047,7 +1130,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -1094,7 +1176,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -1114,8 +1195,44 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#merged_branch_names' do
+ context 'when branch names are passed' do
+ it 'only returns the names we are asking' do
+ names = repository.merged_branch_names(%w[merge-test])
+
+ expect(names).to contain_exactly('merge-test')
+ end
+
+ it 'does not return unmerged branch names' do
+ names = repository.merged_branch_names(%w[feature])
+
+ expect(names).to be_empty
+ end
+ end
+
+ context 'when no branch names are specified' do
+ before do
+ repository.create_branch('identical', 'master')
+ end
+
+ after do
+ ensure_seeds
+ end
+
+ it 'returns all merged branch names except for identical one' do
+ names = repository.merged_branch_names
+
+ expect(names).to include('merge-test')
+ expect(names).to include('fix-mode')
+ expect(names).not_to include('feature')
+ expect(names).not_to include('identical')
+ end
+ end
+ end
+
describe "#ls_files" do
let(:master_file_paths) { repository.ls_files("master") }
+ let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") }
let(:not_existed_branch) { repository.ls_files("not_existed_branch") }
it "read every file paths of master branch" do
@@ -1137,6 +1254,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
it "returns empty array when not existed branch" do
expect(not_existed_branch.length).to equal(0)
end
+
+ it "returns valid utf-8 data" do
+ expect(utf8_file_paths.map { |file| file.force_encoding('utf-8') }).to all(be_valid_encoding)
+ end
end
describe "#copy_gitattributes" do
@@ -1238,7 +1359,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'checks the existence of refs'
end
- context 'when Gitaly ref_exists feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly ref_exists feature is disabled', :skip_gitaly_mock do
it_behaves_like 'checks the existence of refs'
end
end
@@ -1260,7 +1381,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'checks the existence of tags'
end
- context 'when Gitaly ref_exists_tags feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly ref_exists_tags feature is disabled', :skip_gitaly_mock do
it_behaves_like 'checks the existence of tags'
end
end
@@ -1284,18 +1405,35 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'checks the existence of branches'
end
- context 'when Gitaly ref_exists_branches feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly ref_exists_branches feature is disabled', :skip_gitaly_mock do
it_behaves_like 'checks the existence of branches'
end
end
+ describe '#batch_existence' do
+ let(:refs) { ['deadbeef', SeedRepo::RubyBlob::ID, '909e6157199'] }
+
+ it 'returns existing refs back' do
+ result = repository.batch_existence(refs)
+
+ expect(result).to eq([SeedRepo::RubyBlob::ID])
+ end
+
+ context 'existing: true' do
+ it 'inverts meaning and returns non-existing refs' do
+ result = repository.batch_existence(refs, existing: false)
+
+ expect(result).to eq(%w(deadbeef 909e6157199))
+ end
+ end
+ end
+
describe '#local_branches' do
before(:all) do
@repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
ensure_seeds
end
@@ -1361,7 +1499,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'languages'
- context 'with rugged', skip_gitaly_mock: true do
+ context 'with rugged', :skip_gitaly_mock do
it_behaves_like 'languages'
end
end
@@ -1412,36 +1550,61 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe '#fetch_source_branch' do
- let(:local_ref) { 'refs/merge-requests/1/head' }
+ describe '#fetch_source_branch!' do
+ shared_examples '#fetch_source_branch!' do
+ let(:local_ref) { 'refs/merge-requests/1/head' }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:source_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
- context 'when the branch exists' do
- let(:source_branch) { 'master' }
+ after do
+ ensure_seeds
+ end
- it 'writes the ref' do
- expect(repository).to receive(:write_ref).with(local_ref, /\h{40}/)
+ context 'when the branch exists' do
+ context 'when the commit does not exist locally' do
+ let(:source_branch) { 'new-branch-for-fetch-source-branch' }
+ let(:source_rugged) { source_repository.rugged }
+ let(:new_oid) { new_commit_edit_old_file(source_rugged).oid }
- repository.fetch_source_branch(repository, source_branch, local_ref)
- end
+ before do
+ source_rugged.branches.create(source_branch, new_oid)
+ end
- it 'returns true' do
- expect(repository.fetch_source_branch(repository, source_branch, local_ref)).to eq(true)
- end
- end
+ it 'writes the ref' do
+ expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true)
+ expect(repository.commit(local_ref).sha).to eq(new_oid)
+ end
+ end
- context 'when the branch does not exist' do
- let(:source_branch) { 'definitely-not-master' }
+ context 'when the commit exists locally' do
+ let(:source_branch) { 'master' }
+ let(:expected_oid) { SeedRepo::LastCommit::ID }
- it 'does not write the ref' do
- expect(repository).not_to receive(:write_ref)
+ it 'writes the ref' do
+ # Sanity check: the commit should already exist
+ expect(repository.commit(expected_oid)).not_to be_nil
- repository.fetch_source_branch(repository, source_branch, local_ref)
+ expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true)
+ expect(repository.commit(local_ref).sha).to eq(expected_oid)
+ end
+ end
end
- it 'returns false' do
- expect(repository.fetch_source_branch(repository, source_branch, local_ref)).to eq(false)
+ context 'when the branch does not exist' do
+ let(:source_branch) { 'definitely-not-master' }
+
+ it 'does not write the ref' do
+ expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(false)
+ expect(repository.commit(local_ref)).to be_nil
+ end
end
end
+
+ it_behaves_like '#fetch_source_branch!'
+
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like '#fetch_source_branch!'
+ end
end
describe '#rm_branch' do
@@ -1467,7 +1630,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like "user deleting a branch"
end
- context "when Gitaly user_delete_branch is disabled", skip_gitaly_mock: true do
+ context "when Gitaly user_delete_branch is disabled", :skip_gitaly_mock do
it_behaves_like "user deleting a branch"
end
end
@@ -1489,6 +1652,157 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#fetch_remote_without_shell' do
+ let(:git_path) { Gitlab.config.git.bin_path }
+ let(:remote_name) { 'my_remote' }
+
+ subject { repository.fetch_remote_without_shell(remote_name) }
+
+ it 'fetches the remote and returns true if the command was successful' do
+ expect(repository).to receive(:popen)
+ .with(%W(#{git_path} fetch #{remote_name}), repository.path, {})
+ .and_return(['', 0])
+
+ expect(subject).to be(true)
+ end
+ end
+
+ describe '#merge' do
+ let(:repository) do
+ Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
+ end
+ let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' }
+ let(:user) { build(:user) }
+ let(:target_branch) { 'test-merge-target-branch' }
+
+ before do
+ repository.create_branch(target_branch, '6d394385cf567f80a8fd85055db1ab4c5295806f')
+ end
+
+ after do
+ ensure_seeds
+ end
+
+ shared_examples '#merge' do
+ it 'can perform a merge' do
+ merge_commit_id = nil
+ result = repository.merge(user, source_sha, target_branch, 'Test merge') do |commit_id|
+ merge_commit_id = commit_id
+ end
+
+ expect(result.newrev).to eq(merge_commit_id)
+ expect(result.repo_created).to eq(false)
+ expect(result.branch_created).to eq(false)
+ end
+ end
+
+ context 'with gitaly' do
+ it_behaves_like '#merge'
+ end
+
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like '#merge'
+ end
+ end
+
+ describe '#ff_merge' do
+ let(:repository) do
+ Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
+ end
+ let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
+ let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
+ let(:user) { build(:user) }
+ let(:target_branch) { 'test-ff-target-branch' }
+
+ before do
+ repository.create_branch(target_branch, branch_head)
+ end
+
+ after do
+ ensure_seeds
+ end
+
+ subject { repository.ff_merge(user, source_sha, target_branch) }
+
+ shared_examples '#ff_merge' do
+ it 'performs a ff_merge' do
+ expect(subject.newrev).to eq(source_sha)
+ expect(subject.repo_created).to be(false)
+ expect(subject.branch_created).to be(false)
+
+ expect(repository.commit(target_branch).id).to eq(source_sha)
+ end
+
+ context 'with a non-existing target branch' do
+ subject { repository.ff_merge(user, source_sha, 'this-isnt-real') }
+
+ it 'throws an ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'with a non-existing source commit' do
+ let(:source_sha) { 'f001' }
+
+ it 'throws an ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when the source sha is not a descendant of the branch head' do
+ let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
+
+ it "doesn't perform the ff_merge" do
+ expect { subject }.to raise_error(Gitlab::Git::CommitError)
+
+ expect(repository.commit(target_branch).id).to eq(branch_head)
+ end
+ end
+ end
+
+ context 'with gitaly' do
+ it "calls Gitaly's OperationService" do
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService)
+ .to receive(:user_ff_branch).with(user, source_sha, target_branch)
+ .and_return(nil)
+
+ subject
+ end
+
+ it_behaves_like '#ff_merge'
+ end
+
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like '#ff_merge'
+ end
+ end
+
+ describe '#delete_all_refs_except' do
+ let(:repository) do
+ Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
+ end
+
+ before do
+ repository.write_ref("refs/delete/a", "0b4bc9a49b562e85de7cc9e834518ea6828729b9")
+ repository.write_ref("refs/also-delete/b", "12d65c8dd2b2676fa3ac47d955accc085a37a9c1")
+ repository.write_ref("refs/keep/c", "6473c90867124755509e100d0d35ebdc85a0b6ae")
+ repository.write_ref("refs/also-keep/d", "0b4bc9a49b562e85de7cc9e834518ea6828729b9")
+ end
+
+ after do
+ ensure_seeds
+ end
+
+ it 'deletes all refs except those with the specified prefixes' do
+ repository.delete_all_refs_except(%w(refs/keep refs/also-keep refs/heads))
+ expect(repository.ref_exists?("refs/delete/a")).to be(false)
+ expect(repository.ref_exists?("refs/also-delete/b")).to be(false)
+ expect(repository.ref_exists?("refs/keep/c")).to be(true)
+ expect(repository.ref_exists?("refs/also-keep/d")).to be(true)
+ expect(repository.ref_exists?("refs/heads/master")).to be(true)
+ end
+ end
+
def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged
@@ -1564,4 +1878,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
sha = Rugged::Commit.create(repo, options)
repo.lookup(sha)
end
+
+ def refs(dir)
+ IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line|
+ line.split("\t").last
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index c0eac98d718..eaf74951b0e 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -2,53 +2,112 @@ require 'spec_helper'
describe Gitlab::Git::RevList do
let(:project) { create(:project, :repository) }
+ let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
+ let(:env_hash) do
+ {
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ }
+ end
before do
- expect(Gitlab::Git::Env).to receive(:all).and_return({
- GIT_OBJECT_DIRECTORY: 'foo',
- GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar'
- })
+ allow(Gitlab::Git::Env).to receive(:all).and_return(env_hash.symbolize_keys)
end
- context "#new_refs" do
- let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
+ def args_for_popen(args_list)
+ [
+ Gitlab.config.git.bin_path,
+ "--git-dir=#{project.repository.path_to_repo}",
+ 'rev-list',
+ *args_list
+ ]
+ end
- it 'calls out to `popen`' do
- expect(rev_list).to receive(:popen).with([
- Gitlab.config.git.bin_path,
- "--git-dir=#{project.repository.path_to_repo}",
- 'rev-list',
- 'newrev',
- '--not',
- '--all'
- ],
+ def stub_popen_rev_list(*additional_args, output:)
+ args = args_for_popen(additional_args)
+
+ expect(rev_list).to receive(:popen).with(args, nil, env_hash)
+ .and_return([output, 0])
+ end
+
+ def stub_lazy_popen_rev_list(*additional_args, output:)
+ params = [
+ args_for_popen(additional_args),
nil,
- {
- 'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
- }).and_return(["sha1\nsha2", 0])
+ env_hash,
+ hash_including(lazy_block: anything)
+ ]
+
+ expect(rev_list).to receive(:popen).with(*params) do |*_, lazy_block:|
+ lazy_block.call(output.split("\n").lazy)
+ end
+ end
+
+ context "#new_refs" do
+ it 'calls out to `popen`' do
+ stub_popen_rev_list('newrev', '--not', '--all', output: "sha1\nsha2")
expect(rev_list.new_refs).to eq(%w[sha1 sha2])
end
end
+ context '#new_objects' do
+ it 'fetches list of newly pushed objects using rev-list' do
+ stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
+
+ expect(rev_list.new_objects).to eq(%w[sha1 sha2])
+ end
+
+ it 'can skip pathless objects' do
+ stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2 path/to/file")
+
+ expect(rev_list.new_objects(require_path: true)).to eq(%w[sha2])
+ end
+
+ it 'can yield a lazy enumerator' do
+ stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
+
+ rev_list.new_objects do |object_ids|
+ expect(object_ids).to be_a Enumerator::Lazy
+ end
+ end
+
+ it 'returns the result of the block when given' do
+ stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
+
+ objects = rev_list.new_objects do |object_ids|
+ object_ids.first
+ end
+
+ expect(objects).to eq 'sha1'
+ end
+
+ it 'can accept list of references to exclude' do
+ stub_popen_rev_list('newrev', '--not', 'master', '--objects', output: "sha1\nsha2")
+
+ expect(rev_list.new_objects(not_in: ['master'])).to eq(%w[sha1 sha2])
+ end
+
+ it 'handles empty list of references to exclude as listing all known objects' do
+ stub_popen_rev_list('newrev', '--objects', output: "sha1\nsha2")
+
+ expect(rev_list.new_objects(not_in: [])).to eq(%w[sha1 sha2])
+ end
+ end
+
+ context '#all_objects' do
+ it 'fetches list of all pushed objects using rev-list' do
+ stub_popen_rev_list('--all', '--objects', output: "sha1\nsha2")
+
+ expect(rev_list.all_objects).to eq(%w[sha1 sha2])
+ end
+ end
+
context "#missed_ref" do
let(:rev_list) { described_class.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
it 'calls out to `popen`' do
- expect(rev_list).to receive(:popen).with([
- Gitlab.config.git.bin_path,
- "--git-dir=#{project.repository.path_to_repo}",
- 'rev-list',
- '--max-count=1',
- 'oldrev',
- '^newrev'
- ],
- nil,
- {
- 'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
- }).and_return(["sha1\nsha2", 0])
+ stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', output: "sha1\nsha2")
expect(rev_list.missed_ref).to eq(%w[sha1 sha2])
end
diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
index 98cf7966dad..72dabca793a 100644
--- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
+++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
@@ -10,18 +10,10 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
# Override test-settings for the circuitbreaker with something more realistic
# for these specs.
stub_storage_settings('default' => {
- 'path' => TestEnv.repos_path,
- 'failure_count_threshold' => 10,
- 'failure_wait_time' => 30,
- 'failure_reset_time' => 1800,
- 'storage_timeout' => 5
+ 'path' => TestEnv.repos_path
},
'broken' => {
- 'path' => 'tmp/tests/non-existent-repositories',
- 'failure_count_threshold' => 10,
- 'failure_wait_time' => 30,
- 'failure_reset_time' => 1800,
- 'storage_timeout' => 5
+ 'path' => 'tmp/tests/non-existent-repositories'
},
'nopath' => { 'path' => nil }
)
@@ -49,6 +41,10 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
expect(key_exists).to be_falsey
end
+
+ it 'does not break when there are no keys in redis' do
+ expect { described_class.reset_all! }.not_to raise_error
+ end
end
describe '.for_storage' do
@@ -75,19 +71,79 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
expect(circuit_breaker.hostname).to eq(hostname)
expect(circuit_breaker.storage).to eq('default')
expect(circuit_breaker.storage_path).to eq(TestEnv.repos_path)
- expect(circuit_breaker.failure_count_threshold).to eq(10)
- expect(circuit_breaker.failure_wait_time).to eq(30)
- expect(circuit_breaker.failure_reset_time).to eq(1800)
- expect(circuit_breaker.storage_timeout).to eq(5)
+ end
+ end
+
+ context 'circuitbreaker settings' do
+ before do
+ stub_application_setting(circuitbreaker_failure_count_threshold: 0,
+ circuitbreaker_failure_wait_time: 1,
+ circuitbreaker_failure_reset_time: 2,
+ circuitbreaker_storage_timeout: 3,
+ circuitbreaker_access_retries: 4,
+ circuitbreaker_backoff_threshold: 5)
+ end
+
+ describe '#failure_count_threshold' do
+ it 'reads the value from settings' do
+ expect(circuit_breaker.failure_count_threshold).to eq(0)
+ end
+ end
+
+ describe '#failure_wait_time' do
+ it 'reads the value from settings' do
+ expect(circuit_breaker.failure_wait_time).to eq(1)
+ end
+ end
+
+ describe '#failure_reset_time' do
+ it 'reads the value from settings' do
+ expect(circuit_breaker.failure_reset_time).to eq(2)
+ end
+ end
+
+ describe '#storage_timeout' do
+ it 'reads the value from settings' do
+ expect(circuit_breaker.storage_timeout).to eq(3)
+ end
+ end
+
+ describe '#access_retries' do
+ it 'reads the value from settings' do
+ expect(circuit_breaker.access_retries).to eq(4)
+ end
+ end
+
+ describe '#backoff_threshold' do
+ it 'reads the value from settings' do
+ expect(circuit_breaker.backoff_threshold).to eq(5)
+ end
end
end
describe '#perform' do
- it 'raises an exception with retry time when the circuit is open' do
- allow(circuit_breaker).to receive(:circuit_broken?).and_return(true)
+ it 'raises the correct exception when the circuit is open' do
+ set_in_redis(:last_failure, 1.day.ago.to_f)
+ set_in_redis(:failure_count, 999)
expect { |b| circuit_breaker.perform(&b) }
- .to raise_error(Gitlab::Git::Storage::CircuitOpen)
+ .to raise_error do |exception|
+ expect(exception).to be_kind_of(Gitlab::Git::Storage::CircuitOpen)
+ expect(exception.retry_after).to eq(1800)
+ end
+ end
+
+ it 'raises the correct exception when backing off' do
+ Timecop.freeze do
+ set_in_redis(:last_failure, 1.second.ago.to_f)
+ set_in_redis(:failure_count, 90)
+
+ expect { |b| circuit_breaker.perform(&b) }
+ .to raise_error do |exception|
+ expect(exception).to be_kind_of(Gitlab::Git::Storage::Failing)
+ expect(exception.retry_after).to eq(30)
+ end
+ end
end
it 'yields the block' do
@@ -97,6 +153,7 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
it 'checks if the storage is available' do
expect(circuit_breaker).to receive(:check_storage_accessible!)
+ .and_call_original
circuit_breaker.perform { 'hello world' }
end
@@ -112,204 +169,124 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
.to raise_error(Rugged::OSError)
end
- context 'with the feature disabled' do
- it 'returns the block without checking accessibility' do
- stub_feature_flags(git_storage_circuit_breaker: false)
-
- expect(circuit_breaker).not_to receive(:circuit_broken?)
+ it 'tracks that the storage was accessible' do
+ set_in_redis(:failure_count, 10)
+ set_in_redis(:last_failure, Time.now.to_f)
- result = circuit_breaker.perform { 'hello' }
+ circuit_breaker.perform { '' }
- expect(result).to eq('hello')
- end
+ expect(value_from_redis(:failure_count).to_i).to eq(0)
+ expect(value_from_redis(:last_failure)).to be_empty
+ expect(circuit_breaker.failure_count).to eq(0)
+ expect(circuit_breaker.last_failure).to be_nil
end
- end
- describe '#circuit_broken?' do
- it 'is working when there is no last failure' do
- set_in_redis(:last_failure, nil)
- set_in_redis(:failure_count, 0)
+ it 'only performs the accessibility check once' do
+ expect(Gitlab::Git::Storage::ForkedStorageCheck)
+ .to receive(:storage_available?).once.and_call_original
- expect(circuit_breaker.circuit_broken?).to be_falsey
+ 2.times { circuit_breaker.perform { '' } }
end
- it 'is broken when there was a recent failure' do
- Timecop.freeze do
- set_in_redis(:last_failure, 1.second.ago.to_f)
- set_in_redis(:failure_count, 1)
-
- expect(circuit_breaker.circuit_broken?).to be_truthy
- end
- end
+ it 'calls the check with the correct arguments' do
+ stub_application_setting(circuitbreaker_storage_timeout: 30,
+ circuitbreaker_access_retries: 3)
- it 'is broken when there are too many failures' do
- set_in_redis(:last_failure, 1.day.ago.to_f)
- set_in_redis(:failure_count, 200)
+ expect(Gitlab::Git::Storage::ForkedStorageCheck)
+ .to receive(:storage_available?).with(TestEnv.repos_path, 30, 3)
+ .and_call_original
- expect(circuit_breaker.circuit_broken?).to be_truthy
+ circuit_breaker.perform { '' }
end
- context 'the `failure_wait_time` is set to 0' do
+ context 'with the feature disabled' do
before do
- stub_storage_settings('default' => {
- 'failure_wait_time' => 0,
- 'path' => TestEnv.repos_path
- })
- end
-
- it 'is working even when there is a recent failure' do
- Timecop.freeze do
- set_in_redis(:last_failure, 0.seconds.ago.to_f)
- set_in_redis(:failure_count, 1)
-
- expect(circuit_breaker.circuit_broken?).to be_falsey
- end
+ stub_feature_flags(git_storage_circuit_breaker: false)
end
- end
- end
-
- describe "storage_available?" do
- context 'the storage is available' do
- it 'tracks that the storage was accessible an raises the error' do
- expect(circuit_breaker).to receive(:track_storage_accessible)
- circuit_breaker.storage_available?
- end
+ it 'returns the block without checking accessibility' do
+ expect(circuit_breaker).not_to receive(:check_storage_accessible!)
- it 'only performs the check once' do
- expect(Gitlab::Git::Storage::ForkedStorageCheck)
- .to receive(:storage_available?).once.and_call_original
+ result = circuit_breaker.perform { 'hello' }
- 2.times { circuit_breaker.storage_available? }
+ expect(result).to eq('hello')
end
- end
-
- context 'storage is not available' do
- let(:storage_name) { 'broken' }
- it 'tracks that the storage was inaccessible' do
- expect(circuit_breaker).to receive(:track_storage_inaccessible)
+ it 'allows enabling the feature using an ENV var' do
+ stub_env('GIT_STORAGE_CIRCUIT_BREAKER', 'true')
+ expect(circuit_breaker).to receive(:check_storage_accessible!)
- circuit_breaker.storage_available?
- end
- end
- end
-
- describe '#check_storage_accessible!' do
- it 'raises an exception with retry time when the circuit is open' do
- allow(circuit_breaker).to receive(:circuit_broken?).and_return(true)
+ result = circuit_breaker.perform { 'hello' }
- expect { circuit_breaker.check_storage_accessible! }
- .to raise_error do |exception|
- expect(exception).to be_kind_of(Gitlab::Git::Storage::CircuitOpen)
- expect(exception.retry_after).to eq(30)
+ expect(result).to eq('hello')
end
end
context 'the storage is not available' do
let(:storage_name) { 'broken' }
- it 'raises an error' do
+ it 'raises the correct exception' do
expect(circuit_breaker).to receive(:track_storage_inaccessible)
- expect { circuit_breaker.check_storage_accessible! }
+ expect { circuit_breaker.perform { '' } }
.to raise_error do |exception|
expect(exception).to be_kind_of(Gitlab::Git::Storage::Inaccessible)
expect(exception.retry_after).to eq(30)
end
end
- end
- end
-
- describe '#track_storage_inaccessible' do
- around do |example|
- Timecop.freeze { example.run }
- end
- it 'records the failure time in redis' do
- circuit_breaker.track_storage_inaccessible
-
- failure_time = value_from_redis(:last_failure)
-
- expect(Time.at(failure_time.to_i)).to be_within(1.second).of(Time.now)
- end
-
- it 'sets the failure time on the breaker without reloading' do
- circuit_breaker.track_storage_inaccessible
-
- expect(circuit_breaker).not_to receive(:get_failure_info)
- expect(circuit_breaker.last_failure).to eq(Time.now)
- end
-
- it 'increments the failure count in redis' do
- set_in_redis(:failure_count, 10)
-
- circuit_breaker.track_storage_inaccessible
-
- expect(value_from_redis(:failure_count).to_i).to be(11)
- end
-
- it 'increments the failure count on the breaker without reloading' do
- set_in_redis(:failure_count, 10)
-
- circuit_breaker.track_storage_inaccessible
+ it 'tracks that the storage was inaccessible' do
+ Timecop.freeze do
+ expect { circuit_breaker.perform { '' } }.to raise_error(Gitlab::Git::Storage::Inaccessible)
- expect(circuit_breaker).not_to receive(:get_failure_info)
- expect(circuit_breaker.failure_count).to eq(11)
+ expect(value_from_redis(:failure_count).to_i).to eq(1)
+ expect(value_from_redis(:last_failure)).not_to be_empty
+ expect(circuit_breaker.failure_count).to eq(1)
+ expect(circuit_breaker.last_failure).to be_within(1.second).of(Time.now)
+ end
+ end
end
end
- describe '#track_storage_accessible' do
- it 'sets the failure count to zero in redis' do
- set_in_redis(:failure_count, 10)
-
- circuit_breaker.track_storage_accessible
-
- expect(value_from_redis(:failure_count).to_i).to be(0)
- end
-
- it 'sets the failure count to zero on the breaker without reloading' do
- set_in_redis(:failure_count, 10)
-
- circuit_breaker.track_storage_accessible
+ describe '#circuit_broken?' do
+ it 'is working when there is no last failure' do
+ set_in_redis(:last_failure, nil)
+ set_in_redis(:failure_count, 0)
- expect(circuit_breaker).not_to receive(:get_failure_info)
- expect(circuit_breaker.failure_count).to eq(0)
+ expect(circuit_breaker.circuit_broken?).to be_falsey
end
- it 'removes the last failure time from redis' do
- set_in_redis(:last_failure, Time.now.to_i)
-
- circuit_breaker.track_storage_accessible
+ it 'is broken when there are too many failures' do
+ set_in_redis(:last_failure, 1.day.ago.to_f)
+ set_in_redis(:failure_count, 200)
- expect(circuit_breaker).not_to receive(:get_failure_info)
- expect(circuit_breaker.last_failure).to be_nil
+ expect(circuit_breaker.circuit_broken?).to be_truthy
end
+ end
- it 'removes the last failure time from the breaker without reloading' do
- set_in_redis(:last_failure, Time.now.to_i)
-
- circuit_breaker.track_storage_accessible
+ describe '#backing_off?' do
+ it 'is true when there was a recent failure' do
+ Timecop.freeze do
+ set_in_redis(:last_failure, 1.second.ago.to_f)
+ set_in_redis(:failure_count, 90)
- expect(value_from_redis(:last_failure)).to be_empty
+ expect(circuit_breaker.backing_off?).to be_truthy
+ end
end
- it 'wont connect to redis when there are no failures' do
- expect(Gitlab::Git::Storage.redis).to receive(:with).once
- .and_call_original
- expect(circuit_breaker).to receive(:track_storage_accessible)
- .and_call_original
-
- circuit_breaker.track_storage_accessible
- end
- end
+ context 'the `failure_wait_time` is set to 0' do
+ before do
+ stub_application_setting(circuitbreaker_failure_wait_time: 0)
+ end
- describe '#no_failures?' do
- it 'is false when a failure was tracked' do
- set_in_redis(:last_failure, Time.now.to_i)
- set_in_redis(:failure_count, 1)
+ it 'is working even when there are failures' do
+ Timecop.freeze do
+ set_in_redis(:last_failure, 0.seconds.ago.to_f)
+ set_in_redis(:failure_count, 90)
- expect(circuit_breaker.no_failures?).to be_falsey
+ expect(circuit_breaker.backing_off?).to be_falsey
+ end
+ end
end
end
@@ -329,10 +306,4 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
expect(circuit_breaker.failure_count).to eq(7)
end
end
-
- describe '#cache_key' do
- it 'includes storage and host' do
- expect(circuit_breaker.cache_key).to eq(cache_key)
- end
- end
end
diff --git a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb
index c708b15853a..39a5d020bb4 100644
--- a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb
+++ b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb
@@ -33,6 +33,21 @@ describe Gitlab::Git::Storage::ForkedStorageCheck, broken_storage: true, skip_da
expect(runtime).to be < 1.0
end
+ it 'will try the specified amount of times before failing' do
+ allow(described_class).to receive(:check_filesystem_in_process) do
+ Process.spawn("sleep 10")
+ end
+
+ expect(Process).to receive(:spawn).with('sleep 10').twice
+ .and_call_original
+
+ runtime = Benchmark.realtime do
+ described_class.storage_available?(existing_path, 0.5, 2)
+ end
+
+ expect(runtime).to be < 1.0
+ end
+
describe 'when using paths with spaces' do
let(:test_dir) { Rails.root.join('tmp', 'tests', 'storage_check') }
let(:path_with_spaces) { File.join(test_dir, 'path with spaces') }
diff --git a/spec/lib/gitlab/git/storage/health_spec.rb b/spec/lib/gitlab/git/storage/health_spec.rb
index 2d3af387971..4a14a5201d1 100644
--- a/spec/lib/gitlab/git/storage/health_spec.rb
+++ b/spec/lib/gitlab/git/storage/health_spec.rb
@@ -20,36 +20,6 @@ describe Gitlab::Git::Storage::Health, clean_gitlab_redis_shared_state: true, br
end
end
- describe '.load_for_keys' do
- let(:subject) do
- results = Gitlab::Git::Storage.redis.with do |redis|
- fake_future = double
- allow(fake_future).to receive(:value).and_return([host1_key])
- described_class.load_for_keys({ 'broken' => fake_future }, redis)
- end
-
- # Make sure the `Redis#future is loaded
- results.inject({}) do |result, (name, info)|
- info.each { |i| i[:failure_count] = i[:failure_count].value.to_i }
-
- result[name] = info
-
- result
- end
- end
-
- it 'loads when there is no info in redis' do
- expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 0 }])
- end
-
- it 'reads the correct values for a storage from redis' do
- set_in_redis(host1_key, 5)
- set_in_redis(host2_key, 7)
-
- expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 5 }])
- end
- end
-
describe '.for_all_storages' do
it 'loads health status for all configured storages' do
healths = described_class.for_all_storages
diff --git a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb
index 0e645008c88..5db37f55e03 100644
--- a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb
+++ b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb
@@ -54,6 +54,10 @@ describe Gitlab::Git::Storage::NullCircuitBreaker do
end
describe '#failure_count_threshold' do
+ before do
+ stub_application_setting(circuitbreaker_failure_count_threshold: 1)
+ end
+
it { expect(breaker.failure_count_threshold).to eq(1) }
end
@@ -61,17 +65,6 @@ describe Gitlab::Git::Storage::NullCircuitBreaker do
ours = described_class.public_instance_methods
theirs = Gitlab::Git::Storage::CircuitBreaker.public_instance_methods
- # These methods are not part of the public API, but are public to allow the
- # CircuitBreaker specs to operate. They should be made private over time.
- exceptions = %i[
- cache_key
- check_storage_accessible!
- no_failures?
- storage_available?
- track_storage_accessible
- track_storage_inaccessible
- ]
-
- expect(theirs - ours).to contain_exactly(*exceptions)
+ expect(theirs - ours).to be_empty
end
end
diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb
index cc10679ef1e..6c4f538bf01 100644
--- a/spec/lib/gitlab/git/tag_spec.rb
+++ b/spec/lib/gitlab/git/tag_spec.rb
@@ -29,7 +29,7 @@ describe Gitlab::Git::Tag, seed_helper: true do
it_behaves_like 'Gitlab::Git::Repository#tags'
end
- context 'when Gitaly tags feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly tags feature is disabled', :skip_gitaly_mock do
it_behaves_like 'Gitlab::Git::Repository#tags'
end
end
diff --git a/spec/lib/gitlab/git/user_spec.rb b/spec/lib/gitlab/git/user_spec.rb
index 31d5f59a562..99d850e1df9 100644
--- a/spec/lib/gitlab/git/user_spec.rb
+++ b/spec/lib/gitlab/git/user_spec.rb
@@ -1,18 +1,24 @@
require 'spec_helper'
describe Gitlab::Git::User do
- let(:username) { 'janedo' }
- let(:name) { 'Jane Doe' }
- let(:email) { 'janedoe@example.com' }
+ let(:username) { 'janedoe' }
+ let(:name) { 'Jane Doé' }
+ let(:email) { 'janedoé@example.com' }
let(:gl_id) { 'user-123' }
+ let(:user) do
+ described_class.new(username, name, email, gl_id)
+ end
subject { described_class.new(username, name, email, gl_id) }
describe '.from_gitaly' do
- let(:gitaly_user) { Gitaly::User.new(name: name, email: email, gl_id: gl_id) }
+ let(:gitaly_user) do
+ Gitaly::User.new(gl_username: username, name: name.b, email: email.b, gl_id: gl_id)
+ end
+
subject { described_class.from_gitaly(gitaly_user) }
- it { expect(subject).to eq(described_class.new('', name, email, gl_id)) }
+ it { expect(subject).to eq(user) }
end
describe '.from_gitlab' do
@@ -35,4 +41,21 @@ describe Gitlab::Git::User do
it { expect(subject).not_to eq_other(username, name, email + 'x', gl_id) }
it { expect(subject).not_to eq_other(username, name, email, gl_id + 'x') }
end
+
+ describe '#to_gitaly' do
+ subject { user.to_gitaly }
+
+ it 'creates a Gitaly::User with the correct data' do
+ expect(subject).to be_a(Gitaly::User)
+ expect(subject.gl_username).to eq(username)
+
+ expect(subject.name).to eq(name.b)
+ expect(subject.name).to be_a_binary_string
+
+ expect(subject.email).to eq(email.b)
+ expect(subject.email).to be_a_binary_string
+
+ expect(subject.gl_id).to eq(gl_id)
+ end
+ end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index c54327bd2e4..c9643c5da47 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -165,7 +165,7 @@ describe Gitlab::GitAccess do
stub_application_setting(rsa_key_restriction: 4096)
end
- it 'does not allow keys which are too small', aggregate_failures: true do
+ it 'does not allow keys which are too small', :aggregate_failures do
expect(actor).not_to be_valid
expect { pull_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.')
expect { push_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.')
@@ -177,7 +177,7 @@ describe Gitlab::GitAccess do
stub_application_setting(rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE)
end
- it 'does not allow keys which are too small', aggregate_failures: true do
+ it 'does not allow keys which are too small', :aggregate_failures do
expect(actor).not_to be_valid
expect { pull_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/)
expect { push_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/)
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index 7bd6a7fa842..d9ec28ab02e 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::GitalyClient::OperationService do
let(:repository) { project.repository.raw }
let(:client) { described_class.new(repository) }
let(:user) { create(:user) }
- let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) }
+ let(:gitaly_user) { Gitlab::Git::User.from_gitlab(user).to_gitaly }
describe '#user_create_branch' do
let(:branch_name) { 'new' }
@@ -89,4 +89,38 @@ describe Gitlab::GitalyClient::OperationService do
end
end
end
+
+ describe '#user_ff_branch' do
+ let(:target_branch) { 'my-branch' }
+ let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
+ let(:request) do
+ Gitaly::UserFFBranchRequest.new(
+ repository: repository.gitaly_repository,
+ branch: target_branch,
+ commit_id: source_sha,
+ user: gitaly_user
+ )
+ end
+ let(:branch_update) do
+ Gitaly::OperationBranchUpdate.new(
+ commit_id: source_sha,
+ repo_created: false,
+ branch_created: false
+ )
+ end
+ let(:response) { Gitaly::UserFFBranchResponse.new(branch_update: branch_update) }
+
+ subject { client.user_ff_branch(user, source_sha, target_branch) }
+
+ it 'sends a user_ff_branch message and returns a BranchUpdate object' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_ff_branch).with(request, kind_of(Hash))
+ .and_return(response)
+
+ expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate)
+ expect(subject.newrev).to eq(source_sha)
+ expect(subject.repo_created).to be(false)
+ expect(subject.branch_created).to be(false)
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index 6f59750b4da..951e146a30a 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -84,14 +84,14 @@ describe Gitlab::GitalyClient::RefService do
end
end
- describe '#find_ref_name', seed_helper: true do
+ describe '#find_ref_name', :seed_helper do
subject { client.find_ref_name(SeedRepo::Commit::ID, 'refs/heads/master') }
it { is_expected.to be_utf8 }
it { is_expected.to eq('refs/heads/master') }
end
- describe '#ref_exists?', seed_helper: true do
+ describe '#ref_exists?', :seed_helper do
it 'finds the master branch ref' do
expect(client.ref_exists?('refs/heads/master')).to eq(true)
end
@@ -104,4 +104,17 @@ describe Gitlab::GitalyClient::RefService do
expect { client.ref_exists?('reXXXXX') }.to raise_error(ArgumentError)
end
end
+
+ describe '#delete_refs' do
+ let(:prefixes) { %w(refs/heads refs/keep-around) }
+
+ it 'sends a delete_refs message' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:delete_refs)
+ .with(gitaly_request_with_params(except_with_prefix: prefixes), kind_of(Hash))
+ .and_return(double('delete_refs_response'))
+
+ client.delete_refs(except_with_prefixes: prefixes)
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index fd5f984601e..cbc7ce1c1b0 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -73,4 +73,15 @@ describe Gitlab::GitalyClient::RepositoryService do
client.apply_gitattributes(revision)
end
end
+
+ describe '#has_local_branches?' do
+ it 'sends a has_local_branches message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:has_local_branches)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double(value: true))
+
+ expect(client.has_local_branches?).to be(true)
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/util_spec.rb b/spec/lib/gitlab/gitaly_client/util_spec.rb
index 498f6886bee..d1e0136f8c1 100644
--- a/spec/lib/gitlab/gitaly_client/util_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/util_spec.rb
@@ -6,16 +6,16 @@ describe Gitlab::GitalyClient::Util do
let(:relative_path) { 'my/repo.git' }
let(:gl_repository) { 'project-1' }
let(:git_object_directory) { '.git/objects' }
- let(:git_alternate_object_directory) { '/dir/one:/dir/two' }
+ let(:git_alternate_object_directory) { ['/dir/one', '/dir/two'] }
subject do
described_class.repository(repository_storage, relative_path, gl_repository)
end
it 'creates a Gitaly::Repository with the given data' do
- expect(Gitlab::Git::Env).to receive(:[]).with('GIT_OBJECT_DIRECTORY')
+ allow(Gitlab::Git::Env).to receive(:[]).with('GIT_OBJECT_DIRECTORY_RELATIVE')
.and_return(git_object_directory)
- expect(Gitlab::Git::Env).to receive(:[]).with('GIT_ALTERNATE_OBJECT_DIRECTORIES')
+ allow(Gitlab::Git::Env).to receive(:[]).with('GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE')
.and_return(git_alternate_object_directory)
expect(subject).to be_a(Gitaly::Repository)
@@ -23,21 +23,7 @@ describe Gitlab::GitalyClient::Util do
expect(subject.relative_path).to eq(relative_path)
expect(subject.gl_repository).to eq(gl_repository)
expect(subject.git_object_directory).to eq(git_object_directory)
- expect(subject.git_alternate_object_directories).to eq([git_alternate_object_directory])
- end
- end
-
- describe '.gitaly_user' do
- let(:user) { create(:user) }
- let(:gl_id) { Gitlab::GlId.gl_id(user) }
-
- subject { described_class.gitaly_user(user) }
-
- it 'creates a Gitaly::User from a GitLab user' do
- expect(subject).to be_a(Gitaly::User)
- expect(subject.name).to eq(user.name)
- expect(subject.email).to eq(user.email)
- expect(subject.gl_id).to eq(gl_id)
+ expect(subject.git_alternate_object_directories).to eq(git_alternate_object_directory)
end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
new file mode 100644
index 00000000000..6ad9f5ef766
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
@@ -0,0 +1,88 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::WikiService do
+ let(:project) { create(:project) }
+ let(:storage_name) { project.repository_storage }
+ let(:relative_path) { project.disk_path + '.git' }
+ let(:client) { described_class.new(project.repository) }
+ let(:commit) { create(:gitaly_commit) }
+ let(:page_version) { Gitaly::WikiPageVersion.new(format: 'markdown', commit: commit) }
+ let(:page_info) { { title: 'My Page', raw_data: 'a', version: page_version } }
+
+ describe '#find_page' do
+ let(:response) do
+ [
+ Gitaly::WikiFindPageResponse.new(page: Gitaly::WikiPage.new(page_info)),
+ Gitaly::WikiFindPageResponse.new(page: Gitaly::WikiPage.new(raw_data: 'b'))
+ ]
+ end
+ let(:wiki_page) { subject.first }
+ let(:wiki_page_version) { subject.last }
+
+ subject { client.find_page(title: 'My Page', version: 'master', dir: '') }
+
+ it 'sends a wiki_find_page message' do
+ expect_any_instance_of(Gitaly::WikiService::Stub)
+ .to receive(:wiki_find_page)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return([].each)
+
+ subject
+ end
+
+ it 'concatenates the raw data and returns a pair of WikiPage and WikiPageVersion' do
+ expect_any_instance_of(Gitaly::WikiService::Stub)
+ .to receive(:wiki_find_page)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(response.each)
+
+ expect(wiki_page.title).to eq('My Page')
+ expect(wiki_page.raw_data).to eq('ab')
+ expect(wiki_page_version.format).to eq('markdown')
+ end
+ end
+
+ describe '#get_all_pages' do
+ let(:page_2_info) { { title: 'My Page 2', raw_data: 'c', version: page_version } }
+ let(:response) do
+ [
+ Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(page_info)),
+ Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(raw_data: 'b')),
+ Gitaly::WikiGetAllPagesResponse.new(end_of_page: true),
+ Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(page_2_info)),
+ Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(raw_data: 'd')),
+ Gitaly::WikiGetAllPagesResponse.new(end_of_page: true)
+ ]
+ end
+ let(:wiki_page_1) { subject[0].first }
+ let(:wiki_page_1_version) { subject[0].last }
+ let(:wiki_page_2) { subject[1].first }
+ let(:wiki_page_2_version) { subject[1].last }
+
+ subject { client.get_all_pages }
+
+ it 'sends a wiki_get_all_pages message' do
+ expect_any_instance_of(Gitaly::WikiService::Stub)
+ .to receive(:wiki_get_all_pages)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return([].each)
+
+ subject
+ end
+
+ it 'concatenates the raw data and returns a pair of WikiPage and WikiPageVersion for each page' do
+ expect_any_instance_of(Gitaly::WikiService::Stub)
+ .to receive(:wiki_get_all_pages)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(response.each)
+
+ expect(subject.size).to be(2)
+ expect(wiki_page_1.title).to eq('My Page')
+ expect(wiki_page_1.raw_data).to eq('ab')
+ expect(wiki_page_1_version.format).to eq('markdown')
+ expect(wiki_page_2.title).to eq('My Page 2')
+ expect(wiki_page_2.raw_data).to eq('cd')
+ expect(wiki_page_2_version.format).to eq('markdown')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index a1f4e65b8d4..a871ed0df0e 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -278,4 +278,20 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do
end
end
end
+
+ describe 'timeouts' do
+ context 'with default values' do
+ before do
+ stub_application_setting(gitaly_timeout_default: 55)
+ stub_application_setting(gitaly_timeout_medium: 30)
+ stub_application_setting(gitaly_timeout_fast: 10)
+ end
+
+ it 'returns expected values' do
+ expect(described_class.default_timeout).to be(55)
+ expect(described_class.medium_timeout).to be(30)
+ expect(described_class.fast_timeout).to be(10)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
deleted file mode 100644
index 426b43f8b51..00000000000
--- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::BranchFormatter do
- let(:project) { create(:project, :repository) }
- let(:commit) { create(:commit, project: project) }
- let(:repo) { double }
- let(:raw) do
- {
- ref: 'branch-merged',
- repo: repo,
- sha: commit.id
- }
- end
-
- describe '#exists?' do
- it 'returns true when branch exists and commit is part of the branch' do
- branch = described_class.new(project, double(raw))
-
- expect(branch.exists?).to eq true
- end
-
- it 'returns false when branch exists and commit is not part of the branch' do
- branch = described_class.new(project, double(raw.merge(ref: 'feature')))
-
- expect(branch.exists?).to eq false
- end
-
- it 'returns false when branch does not exist' do
- branch = described_class.new(project, double(raw.merge(ref: 'removed-branch')))
-
- expect(branch.exists?).to eq false
- end
- end
-
- describe '#repo' do
- it 'returns raw repo' do
- branch = described_class.new(project, double(raw))
-
- expect(branch.repo).to eq repo
- end
- end
-
- describe '#sha' do
- it 'returns raw sha' do
- branch = described_class.new(project, double(raw))
-
- expect(branch.sha).to eq commit.id
- end
- end
-
- describe '#valid?' do
- it 'returns true when raw sha and ref are present' do
- branch = described_class.new(project, double(raw))
-
- expect(branch.valid?).to eq true
- end
-
- it 'returns false when raw sha is blank' do
- branch = described_class.new(project, double(raw.merge(sha: nil)))
-
- expect(branch.valid?).to eq false
- end
-
- it 'returns false when raw ref is blank' do
- branch = described_class.new(project, double(raw.merge(ref: nil)))
-
- expect(branch.valid?).to eq false
- end
- end
-end
diff --git a/spec/lib/gitlab/github_import/bulk_importing_spec.rb b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
new file mode 100644
index 00000000000..91229d9c7d4
--- /dev/null
+++ b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::BulkImporting do
+ let(:importer) do
+ Class.new { include(Gitlab::GithubImport::BulkImporting) }.new
+ end
+
+ describe '#build_database_rows' do
+ it 'returns an Array containing the rows to insert' do
+ object = double(:object, title: 'Foo')
+
+ expect(importer)
+ .to receive(:build)
+ .with(object)
+ .and_return({ title: 'Foo' })
+
+ expect(importer)
+ .to receive(:already_imported?)
+ .with(object)
+ .and_return(false)
+
+ enum = [[object, 1]].to_enum
+
+ expect(importer.build_database_rows(enum)).to eq([{ title: 'Foo' }])
+ end
+
+ it 'does not import objects that have already been imported' do
+ object = double(:object, title: 'Foo')
+
+ expect(importer)
+ .not_to receive(:build)
+
+ expect(importer)
+ .to receive(:already_imported?)
+ .with(object)
+ .and_return(true)
+
+ enum = [[object, 1]].to_enum
+
+ expect(importer.build_database_rows(enum)).to be_empty
+ end
+ end
+
+ describe '#bulk_insert' do
+ it 'bulk inserts rows into the database' do
+ rows = [{ title: 'Foo' }] * 10
+ model = double(:model, table_name: 'kittens')
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .ordered
+ .with('kittens', rows.first(5))
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .ordered
+ .with('kittens', rows.last(5))
+
+ importer.bulk_insert(model, rows, batch_size: 5)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/caching_spec.rb b/spec/lib/gitlab/github_import/caching_spec.rb
new file mode 100644
index 00000000000..70ecdc16da1
--- /dev/null
+++ b/spec/lib/gitlab/github_import/caching_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Caching, :clean_gitlab_redis_cache do
+ describe '.read' do
+ it 'reads a value from the cache' do
+ described_class.write('foo', 'bar')
+
+ expect(described_class.read('foo')).to eq('bar')
+ end
+
+ it 'returns nil if the cache key does not exist' do
+ expect(described_class.read('foo')).to be_nil
+ end
+
+ it 'refreshes the cache key if a value is present' do
+ described_class.write('foo', 'bar')
+
+ redis = double(:redis)
+
+ expect(redis).to receive(:get).with(/foo/).and_return('bar')
+ expect(redis).to receive(:expire).with(/foo/, described_class::TIMEOUT)
+ expect(Gitlab::Redis::Cache).to receive(:with).twice.and_yield(redis)
+
+ described_class.read('foo')
+ end
+
+ it 'does not refresh the cache key if a value is empty' do
+ described_class.write('foo', nil)
+
+ redis = double(:redis)
+
+ expect(redis).to receive(:get).with(/foo/).and_return('')
+ expect(redis).not_to receive(:expire)
+ expect(Gitlab::Redis::Cache).to receive(:with).and_yield(redis)
+
+ described_class.read('foo')
+ end
+ end
+
+ describe '.read_integer' do
+ it 'returns an Integer' do
+ described_class.write('foo', '10')
+
+ expect(described_class.read_integer('foo')).to eq(10)
+ end
+
+ it 'returns nil if no value was found' do
+ expect(described_class.read_integer('foo')).to be_nil
+ end
+ end
+
+ describe '.write' do
+ it 'writes a value to the cache and returns the written value' do
+ expect(described_class.write('foo', 10)).to eq(10)
+ expect(described_class.read('foo')).to eq('10')
+ end
+ end
+
+ describe '.set_add' do
+ it 'adds a value to a set' do
+ described_class.set_add('foo', 10)
+ described_class.set_add('foo', 10)
+
+ key = described_class.cache_key_for('foo')
+ values = Gitlab::Redis::Cache.with { |r| r.smembers(key) }
+
+ expect(values).to eq(['10'])
+ end
+ end
+
+ describe '.set_includes?' do
+ it 'returns false when the key does not exist' do
+ expect(described_class.set_includes?('foo', 10)).to eq(false)
+ end
+
+ it 'returns false when the value is not present in the set' do
+ described_class.set_add('foo', 10)
+
+ expect(described_class.set_includes?('foo', 20)).to eq(false)
+ end
+
+ it 'returns true when the set includes the given value' do
+ described_class.set_add('foo', 10)
+
+ expect(described_class.set_includes?('foo', 10)).to eq(true)
+ end
+ end
+
+ describe '.write_multiple' do
+ it 'sets multiple keys' do
+ mapping = { 'foo' => 10, 'bar' => 20 }
+
+ described_class.write_multiple(mapping)
+
+ mapping.each do |key, value|
+ full_key = described_class.cache_key_for(key)
+ found = Gitlab::Redis::Cache.with { |r| r.get(full_key) }
+
+ expect(found).to eq(value.to_s)
+ end
+ end
+ end
+
+ describe '.expire' do
+ it 'sets the expiration time of a key' do
+ timeout = 1.hour.to_i
+
+ described_class.write('foo', 'bar', timeout: 2.hours.to_i)
+ described_class.expire('foo', timeout)
+
+ key = described_class.cache_key_for('foo')
+ found_ttl = Gitlab::Redis::Cache.with { |r| r.ttl(key) }
+
+ expect(found_ttl).to be <= timeout
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 66273255b6f..5b2642d9473 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -1,97 +1,392 @@
require 'spec_helper'
describe Gitlab::GithubImport::Client do
- let(:token) { '123456' }
- let(:github_provider) { Settingslogic.new('app_id' => 'asd123', 'app_secret' => 'asd123', 'name' => 'github', 'args' => { 'client_options' => {} }) }
+ describe '#parallel?' do
+ it 'returns true when the client is running in parallel mode' do
+ client = described_class.new('foo', parallel: true)
- subject(:client) { described_class.new(token) }
+ expect(client).to be_parallel
+ end
+
+ it 'returns false when the client is running in sequential mode' do
+ client = described_class.new('foo', parallel: false)
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([github_provider])
+ expect(client).not_to be_parallel
+ end
end
- it 'convert OAuth2 client options to symbols' do
- client.client.options.keys.each do |key|
- expect(key).to be_kind_of(Symbol)
+ describe '#user' do
+ it 'returns the details for the given username' do
+ client = described_class.new('foo')
+
+ expect(client.octokit).to receive(:user).with('foo')
+ expect(client).to receive(:with_rate_limit).and_yield
+
+ client.user('foo')
end
end
- it 'does not crash (e.g. Settingslogic::MissingSetting) when verify_ssl config is not present' do
- expect { client.api }.not_to raise_error
+ describe '#repository' do
+ it 'returns the details of a repository' do
+ client = described_class.new('foo')
+
+ expect(client.octokit).to receive(:repo).with('foo/bar')
+ expect(client).to receive(:with_rate_limit).and_yield
+
+ client.repository('foo/bar')
+ end
end
- context 'when config is missing' do
- before do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
+ describe '#labels' do
+ it 'returns the labels' do
+ client = described_class.new('foo')
+
+ expect(client)
+ .to receive(:each_object)
+ .with(:labels, 'foo/bar')
+
+ client.labels('foo/bar')
end
+ end
- it 'is still possible to get an Octokit client' do
- expect { client.api }.not_to raise_error
+ describe '#milestones' do
+ it 'returns the milestones' do
+ client = described_class.new('foo')
+
+ expect(client)
+ .to receive(:each_object)
+ .with(:milestones, 'foo/bar')
+
+ client.milestones('foo/bar')
end
+ end
- it 'is not be possible to get an OAuth2 client' do
- expect { client.client }.to raise_error(Projects::ImportService::Error)
+ describe '#releases' do
+ it 'returns the releases' do
+ client = described_class.new('foo')
+
+ expect(client)
+ .to receive(:each_object)
+ .with(:releases, 'foo/bar')
+
+ client.releases('foo/bar')
end
end
- context 'allow SSL verification to be configurable on API' do
+ describe '#each_page' do
+ let(:client) { described_class.new('foo') }
+ let(:object1) { double(:object1) }
+ let(:object2) { double(:object2) }
+
before do
- github_provider['verify_ssl'] = false
+ allow(client)
+ .to receive(:with_rate_limit)
+ .and_yield
+
+ allow(client.octokit)
+ .to receive(:public_send)
+ .and_return([object1])
+
+ response = double(:response, data: [object2], rels: { next: nil })
+ next_page = double(:next_page, get: response)
+
+ allow(client.octokit)
+ .to receive(:last_response)
+ .and_return(double(:last_response, rels: { next: next_page }))
+ end
+
+ context 'without a block' do
+ it 'returns an Enumerator' do
+ expect(client.each_page(:foo)).to be_an_instance_of(Enumerator)
+ end
+
+ it 'the returned Enumerator returns Page objects' do
+ enum = client.each_page(:foo)
+
+ page1 = enum.next
+ page2 = enum.next
+
+ expect(page1).to be_an_instance_of(described_class::Page)
+ expect(page2).to be_an_instance_of(described_class::Page)
+
+ expect(page1.objects).to eq([object1])
+ expect(page1.number).to eq(1)
+
+ expect(page2.objects).to eq([object2])
+ expect(page2.number).to eq(2)
+ end
+ end
+
+ context 'with a block' do
+ it 'yields every retrieved page to the supplied block' do
+ pages = []
+
+ client.each_page(:foo) { |page| pages << page }
+
+ expect(pages[0]).to be_an_instance_of(described_class::Page)
+ expect(pages[1]).to be_an_instance_of(described_class::Page)
+
+ expect(pages[0].objects).to eq([object1])
+ expect(pages[0].number).to eq(1)
+
+ expect(pages[1].objects).to eq([object2])
+ expect(pages[1].number).to eq(2)
+ end
+
+ it 'starts at the given page' do
+ pages = []
+
+ client.each_page(:foo, page: 2) { |page| pages << page }
+
+ expect(pages[0].number).to eq(2)
+ expect(pages[1].number).to eq(3)
+ end
+ end
+ end
+
+ describe '#with_rate_limit' do
+ let(:client) { described_class.new('foo') }
+
+ it 'yields the supplied block when enough requests remain' do
+ expect(client).to receive(:requests_remaining?).and_return(true)
+
+ expect { |b| client.with_rate_limit(&b) }.to yield_control
+ end
+
+ it 'waits before yielding if not enough requests remain' do
+ expect(client).to receive(:requests_remaining?).and_return(false)
+ expect(client).to receive(:raise_or_wait_for_rate_limit)
+
+ expect { |b| client.with_rate_limit(&b) }.to yield_control
+ end
+
+ it 'waits and retries the operation if all requests were consumed in the supplied block' do
+ retries = 0
+
+ expect(client).to receive(:requests_remaining?).and_return(true)
+ expect(client).to receive(:raise_or_wait_for_rate_limit)
+
+ client.with_rate_limit do
+ if retries.zero?
+ retries += 1
+ raise(Octokit::TooManyRequests)
+ end
+ end
+
+ expect(retries).to eq(1)
+ end
+
+ it 'increments the request count counter' do
+ expect(client.request_count_counter)
+ .to receive(:increment)
+ .and_call_original
+
+ expect(client).to receive(:requests_remaining?).and_return(true)
+
+ client.with_rate_limit { }
+ end
+
+ it 'ignores rate limiting when disabled' do
+ expect(client)
+ .to receive(:rate_limiting_enabled?)
+ .and_return(false)
+
+ expect(client)
+ .not_to receive(:requests_remaining?)
+
+ expect(client.with_rate_limit { 10 }).to eq(10)
+ end
+ end
+
+ describe '#requests_remaining?' do
+ let(:client) { described_class.new('foo') }
+
+ it 'returns true if enough requests remain' do
+ expect(client).to receive(:remaining_requests).and_return(9000)
+
+ expect(client.requests_remaining?).to eq(true)
+ end
+
+ it 'returns false if not enough requests remain' do
+ expect(client).to receive(:remaining_requests).and_return(1)
+
+ expect(client.requests_remaining?).to eq(false)
+ end
+ end
+
+ describe '#raise_or_wait_for_rate_limit' do
+ it 'raises RateLimitError when running in parallel mode' do
+ client = described_class.new('foo', parallel: true)
+
+ expect { client.raise_or_wait_for_rate_limit }
+ .to raise_error(Gitlab::GithubImport::RateLimitError)
end
- it 'uses supplied value' do
- expect(client.client.options[:connection_opts][:ssl]).to eq({ verify: false })
- expect(client.api.connection_options[:ssl]).to eq({ verify: false })
+ it 'sleeps when running in sequential mode' do
+ client = described_class.new('foo', parallel: false)
+
+ expect(client).to receive(:rate_limit_resets_in).and_return(1)
+ expect(client).to receive(:sleep).with(1)
+
+ client.raise_or_wait_for_rate_limit
+ end
+
+ it 'increments the rate limit counter' do
+ client = described_class.new('foo', parallel: false)
+
+ expect(client)
+ .to receive(:rate_limit_resets_in)
+ .and_return(1)
+
+ expect(client)
+ .to receive(:sleep)
+ .with(1)
+
+ expect(client.rate_limit_counter)
+ .to receive(:increment)
+ .and_call_original
+
+ client.raise_or_wait_for_rate_limit
+ end
+ end
+
+ describe '#remaining_requests' do
+ it 'returns the number of remaining requests' do
+ client = described_class.new('foo')
+ rate_limit = double(remaining: 1)
+
+ expect(client.octokit).to receive(:rate_limit).and_return(rate_limit)
+ expect(client.remaining_requests).to eq(1)
+ end
+ end
+
+ describe '#rate_limit_resets_in' do
+ it 'returns the number of seconds after which the rate limit is reset' do
+ client = described_class.new('foo')
+ rate_limit = double(resets_in: 1)
+
+ expect(client.octokit).to receive(:rate_limit).and_return(rate_limit)
+
+ expect(client.rate_limit_resets_in).to eq(6)
end
end
describe '#api_endpoint' do
- context 'when provider does not specity an API endpoint' do
- it 'uses GitHub root API endpoint' do
- expect(client.api.api_endpoint).to eq 'https://api.github.com/'
+ let(:client) { described_class.new('foo') }
+
+ context 'without a custom endpoint configured in Omniauth' do
+ it 'returns the default API endpoint' do
+ expect(client)
+ .to receive(:custom_api_endpoint)
+ .and_return(nil)
+
+ expect(client.api_endpoint).to eq('https://api.github.com')
end
end
- context 'when provider specify a custom API endpoint' do
- before do
- github_provider['args']['client_options']['site'] = 'https://github.company.com/'
+ context 'with a custom endpoint configured in Omniauth' do
+ it 'returns the custom endpoint' do
+ endpoint = 'https://github.kittens.com'
+
+ expect(client)
+ .to receive(:custom_api_endpoint)
+ .and_return(endpoint)
+
+ expect(client.api_endpoint).to eq(endpoint)
end
+ end
+ end
+
+ describe '#custom_api_endpoint' do
+ let(:client) { described_class.new('foo') }
+
+ context 'without a custom endpoint' do
+ it 'returns nil' do
+ expect(client)
+ .to receive(:github_omniauth_provider)
+ .and_return({})
+
+ expect(client.custom_api_endpoint).to be_nil
+ end
+ end
+
+ context 'with a custom endpoint' do
+ it 'returns the API endpoint' do
+ endpoint = 'https://github.kittens.com'
+
+ expect(client)
+ .to receive(:github_omniauth_provider)
+ .and_return({ 'args' => { 'client_options' => { 'site' => endpoint } } })
- it 'uses the custom API endpoint' do
- expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options)
- expect(client.api.api_endpoint).to eq 'https://github.company.com/'
+ expect(client.custom_api_endpoint).to eq(endpoint)
end
end
+ end
+
+ describe '#default_api_endpoint' do
+ it 'returns the default API endpoint' do
+ client = described_class.new('foo')
+
+ expect(client.default_api_endpoint).to eq('https://api.github.com')
+ end
+ end
+
+ describe '#verify_ssl' do
+ let(:client) { described_class.new('foo') }
- context 'when given a host' do
- subject(:client) { described_class.new(token, host: 'https://try.gitea.io/') }
+ context 'without a custom configuration' do
+ it 'returns true' do
+ expect(client)
+ .to receive(:github_omniauth_provider)
+ .and_return({})
- it 'builds a endpoint with the given host and the default API version' do
- expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
+ expect(client.verify_ssl).to eq(true)
end
end
- context 'when given an API version' do
- subject(:client) { described_class.new(token, api_version: 'v3') }
+ context 'with a custom configuration' do
+ it 'returns the configured value' do
+ expect(client.verify_ssl).to eq(false)
+ end
+ end
+ end
+
+ describe '#github_omniauth_provider' do
+ let(:client) { described_class.new('foo') }
- it 'does not use the API version without a host' do
- expect(client.api.api_endpoint).to eq 'https://api.github.com/'
+ context 'without a configured provider' do
+ it 'returns an empty Hash' do
+ expect(Gitlab.config.omniauth)
+ .to receive(:providers)
+ .and_return([])
+
+ expect(client.github_omniauth_provider).to eq({})
end
end
- context 'when given a host and version' do
- subject(:client) { described_class.new(token, host: 'https://try.gitea.io/', api_version: 'v3') }
+ context 'with a configured provider' do
+ it 'returns the provider details as a Hash' do
+ hash = client.github_omniauth_provider
- it 'builds a endpoint with the given options' do
- expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
+ expect(hash['name']).to eq('github')
+ expect(hash['url']).to eq('https://github.com/')
end
end
end
- it 'does not raise error when rate limit is disabled' do
- stub_request(:get, /api.github.com/)
- allow(client.api).to receive(:rate_limit!).and_raise(Octokit::NotFound)
+ describe '#rate_limiting_enabled?' do
+ let(:client) { described_class.new('foo') }
- expect { client.issues {} }.not_to raise_error
+ it 'returns true when using GitHub.com' do
+ expect(client.rate_limiting_enabled?).to eq(true)
+ end
+
+ it 'returns false for GitHub enterprise installations' do
+ expect(client)
+ .to receive(:api_endpoint)
+ .and_return('https://github.kittens.com/')
+
+ expect(client.rate_limiting_enabled?).to eq(false)
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
deleted file mode 100644
index 035ac8c7c1f..00000000000
--- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::CommentFormatter do
- let(:client) { double }
- let(:project) { create(:project) }
- let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
- let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') }
- let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') }
- let(:base) do
- {
- body: "I'm having a problem with this.",
- user: octocat,
- commit_id: nil,
- diff_hunk: nil,
- created_at: created_at,
- updated_at: updated_at
- }
- end
-
- subject(:comment) { described_class.new(project, raw, client) }
-
- before do
- allow(client).to receive(:user).and_return(octocat)
- end
-
- describe '#attributes' do
- context 'when do not reference a portion of the diff' do
- let(:raw) { double(base) }
-
- it 'returns formatted attributes' do
- expected = {
- project: project,
- note: "*Created by: octocat*\n\nI'm having a problem with this.",
- commit_id: nil,
- line_code: nil,
- author_id: project.creator_id,
- type: nil,
- created_at: created_at,
- updated_at: updated_at
- }
-
- expect(comment.attributes).to eq(expected)
- end
- end
-
- context 'when on a portion of the diff' do
- let(:diff) do
- {
- body: 'Great stuff',
- commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
- diff_hunk: "@@ -1,5 +1,9 @@\n class User\n def name\n- 'John Doe'\n+ 'Jane Doe'",
- path: 'file1.txt'
- }
- end
-
- let(:raw) { double(base.merge(diff)) }
-
- it 'returns formatted attributes' do
- expected = {
- project: project,
- note: "*Created by: octocat*\n\nGreat stuff",
- commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
- line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_4_3',
- author_id: project.creator_id,
- type: 'LegacyDiffNote',
- created_at: created_at,
- updated_at: updated_at
- }
-
- expect(comment.attributes).to eq(expected)
- end
- end
-
- context 'when author is a GitLab user' do
- let(:raw) { double(base.merge(user: octocat)) }
-
- it 'returns GitLab user id associated with GitHub id as author_id' do
- gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
-
- expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
- end
-
- it 'returns GitLab user id associated with GitHub email as author_id' do
- gl_user = create(:user, email: octocat.email)
-
- expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
- end
-
- it 'returns note without created at tag line' do
- create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
-
- expect(comment.attributes.fetch(:note)).to eq("I'm having a problem with this.")
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
new file mode 100644
index 00000000000..1568c657a1e
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
@@ -0,0 +1,152 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::DiffNoteImporter do
+ let(:project) { create(:project) }
+ let(:client) { double(:client) }
+ let(:user) { create(:user) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ let(:hunk) do
+ '@@ -1 +1 @@
+ -Hello
+ +Hello world'
+ end
+
+ let(:note) do
+ Gitlab::GithubImport::Representation::DiffNote.new(
+ noteable_type: 'MergeRequest',
+ noteable_id: 1,
+ commit_id: '123abc',
+ file_path: 'README.md',
+ diff_hunk: hunk,
+ author: Gitlab::GithubImport::Representation::User
+ .new(id: user.id, login: user.username),
+ note: 'Hello',
+ created_at: created_at,
+ updated_at: updated_at,
+ github_id: 1
+ )
+ end
+
+ let(:importer) { described_class.new(note, project, client) }
+
+ describe '#execute' do
+ context 'when the merge request no longer exists' do
+ it 'does not import anything' do
+ expect(Gitlab::Database).not_to receive(:bulk_insert)
+
+ importer.execute
+ end
+ end
+
+ context 'when the merge request exists' do
+ let!(:merge_request) do
+ create(:merge_request, source_project: project, target_project: project)
+ end
+
+ before do
+ allow(importer)
+ .to receive(:find_merge_request_id)
+ .and_return(merge_request.id)
+ end
+
+ it 'imports the note' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .and_return([user.id, true])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(
+ LegacyDiffNote.table_name,
+ [
+ {
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ project_id: project.id,
+ author_id: user.id,
+ note: 'Hello',
+ system: false,
+ commit_id: '123abc',
+ line_code: note.line_code,
+ type: 'LegacyDiffNote',
+ created_at: created_at,
+ updated_at: updated_at,
+ st_diff: note.diff_hash.to_yaml
+ }
+ ]
+ )
+ .and_call_original
+
+ importer.execute
+ end
+
+ it 'imports the note when the author could not be found' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .and_return([project.creator_id, false])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(
+ LegacyDiffNote.table_name,
+ [
+ {
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ project_id: project.id,
+ author_id: project.creator_id,
+ note: "*Created by: #{user.username}*\n\nHello",
+ system: false,
+ commit_id: '123abc',
+ line_code: note.line_code,
+ type: 'LegacyDiffNote',
+ created_at: created_at,
+ updated_at: updated_at,
+ st_diff: note.diff_hash.to_yaml
+ }
+ ]
+ )
+ .and_call_original
+
+ importer.execute
+ end
+
+ it 'produces a valid LegacyDiffNote' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .and_return([user.id, true])
+
+ importer.execute
+
+ note = project.notes.diff_notes.take
+
+ expect(note).to be_valid
+ expect(note.diff).to be_an_instance_of(Gitlab::Git::Diff)
+ end
+
+ it 'does not import the note when a foreign key error is raised' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .and_return([project.creator_id, false])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
+
+ expect { importer.execute }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#find_merge_request_id' do
+ it 'returns a merge request ID' do
+ expect_any_instance_of(Gitlab::GithubImport::IssuableFinder)
+ .to receive(:database_id)
+ .and_return(10)
+
+ expect(importer.find_merge_request_id).to eq(10)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
new file mode 100644
index 00000000000..4713c6795bb
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::DiffNotesImporter do
+ let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+
+ let(:github_comment) do
+ double(
+ :response,
+ html_url: 'https://github.com/foo/bar/pull/42',
+ path: 'README.md',
+ commit_id: '123abc',
+ diff_hunk: "@@ -1 +1 @@\n-Hello\n+Hello world",
+ user: double(:user, id: 4, login: 'alice'),
+ body: 'Hello world',
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now,
+ id: 1
+ )
+ end
+
+ describe '#parallel?' do
+ it 'returns true when running in parallel mode' do
+ importer = described_class.new(project, client)
+ expect(importer).to be_parallel
+ end
+
+ it 'returns false when running in sequential mode' do
+ importer = described_class.new(project, client, parallel: false)
+ expect(importer).not_to be_parallel
+ end
+ end
+
+ describe '#execute' do
+ context 'when running in parallel mode' do
+ it 'imports diff notes in parallel' do
+ importer = described_class.new(project, client)
+
+ expect(importer).to receive(:parallel_import)
+
+ importer.execute
+ end
+ end
+
+ context 'when running in sequential mode' do
+ it 'imports diff notes in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+
+ expect(importer).to receive(:sequential_import)
+
+ importer.execute
+ end
+ end
+ end
+
+ describe '#sequential_import' do
+ it 'imports each diff note in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+ diff_note_importer = double(:diff_note_importer)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_comment)
+
+ expect(Gitlab::GithubImport::Importer::DiffNoteImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::DiffNote),
+ project,
+ client
+ )
+ .and_return(diff_note_importer)
+
+ expect(diff_note_importer).to receive(:execute)
+
+ importer.sequential_import
+ end
+ end
+
+ describe '#parallel_import' do
+ it 'imports each diff note in parallel' do
+ importer = described_class.new(project, client)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_comment)
+
+ expect(Gitlab::GithubImport::ImportDiffNoteWorker)
+ .to receive(:perform_async)
+ .with(project.id, an_instance_of(Hash), an_instance_of(String))
+
+ waiter = importer.parallel_import
+
+ expect(waiter).to be_an_instance_of(Gitlab::JobWaiter)
+ expect(waiter.jobs_remaining).to eq(1)
+ end
+ end
+
+ describe '#id_for_already_imported_cache' do
+ it 'returns the ID of the given note' do
+ importer = described_class.new(project, client)
+
+ expect(importer.id_for_already_imported_cache(github_comment))
+ .to eq(1)
+ end
+ end
+
+ describe '#collection_options' do
+ it 'returns an empty Hash' do
+ # For large projects (e.g. kubernetes/kubernetes) GitHub's API may produce
+ # HTTP 500 errors when using explicit sorting options, regardless of what
+ # order you sort in. Not using any sorting options at all allows us to
+ # work around this.
+ importer = described_class.new(project, client)
+
+ expect(importer.collection_options).to eq({})
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb
new file mode 100644
index 00000000000..665b31ef244
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::IssueAndLabelLinksImporter do
+ describe '#execute' do
+ it 'imports an issue and its labels' do
+ issue = double(:issue)
+ project = double(:project)
+ client = double(:client)
+ label_links_instance = double(:label_links_importer)
+ importer = described_class.new(issue, project, client)
+
+ expect(Gitlab::GithubImport::Importer::IssueImporter)
+ .to receive(:import_if_issue)
+ .with(issue, project, client)
+
+ expect(Gitlab::GithubImport::Importer::LabelLinksImporter)
+ .to receive(:new)
+ .with(issue, project, client)
+ .and_return(label_links_instance)
+
+ expect(label_links_instance)
+ .to receive(:execute)
+
+ importer.execute
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
new file mode 100644
index 00000000000..d34ca0b76b8
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
@@ -0,0 +1,201 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cache do
+ let(:project) { create(:project) }
+ let(:client) { double(:client) }
+ let(:user) { create(:user) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ let(:issue) do
+ Gitlab::GithubImport::Representation::Issue.new(
+ iid: 42,
+ title: 'My Issue',
+ description: 'This is my issue',
+ milestone_number: 1,
+ state: :opened,
+ assignees: [
+ Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice'),
+ Gitlab::GithubImport::Representation::User.new(id: 5, login: 'bob')
+ ],
+ label_names: %w[bug],
+ author: Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice'),
+ created_at: created_at,
+ updated_at: updated_at,
+ pull_request: false
+ )
+ end
+
+ describe '.import_if_issue' do
+ it 'imports an issuable if it is a regular issue' do
+ importer = double(:importer)
+
+ expect(described_class)
+ .to receive(:new)
+ .with(issue, project, client)
+ .and_return(importer)
+
+ expect(importer).to receive(:execute)
+
+ described_class.import_if_issue(issue, project, client)
+ end
+
+ it 'does not import the issuable if it is a pull request' do
+ expect(issue).to receive(:pull_request?).and_return(true)
+
+ expect(described_class).not_to receive(:new)
+
+ described_class.import_if_issue(issue, project, client)
+ end
+ end
+
+ describe '#execute' do
+ let(:importer) { described_class.new(issue, project, client) }
+
+ it 'creates the issue and assignees' do
+ expect(importer)
+ .to receive(:create_issue)
+ .and_return(10)
+
+ expect(importer)
+ .to receive(:create_assignees)
+ .with(10)
+
+ expect(importer.issuable_finder)
+ .to receive(:cache_database_id)
+ .with(10)
+
+ importer.execute
+ end
+ end
+
+ describe '#create_issue' do
+ let(:importer) { described_class.new(issue, project, client) }
+
+ before do
+ allow(importer.milestone_finder)
+ .to receive(:id_for)
+ .with(issue)
+ .and_return(milestone.id)
+ end
+
+ context 'when the issue author could be found' do
+ it 'creates the issue with the found author as the issue author' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(issue)
+ .and_return([user.id, true])
+
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .with(
+ {
+ iid: 42,
+ title: 'My Issue',
+ author_id: user.id,
+ project_id: project.id,
+ description: 'This is my issue',
+ milestone_id: milestone.id,
+ state: :opened,
+ created_at: created_at,
+ updated_at: updated_at
+ },
+ project.issues
+ )
+ .and_call_original
+
+ importer.create_issue
+ end
+ end
+
+ context 'when the issue author could not be found' do
+ it 'creates the issue with the project creator as the issue author' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(issue)
+ .and_return([project.creator_id, false])
+
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .with(
+ {
+ iid: 42,
+ title: 'My Issue',
+ author_id: project.creator_id,
+ project_id: project.id,
+ description: "*Created by: alice*\n\nThis is my issue",
+ milestone_id: milestone.id,
+ state: :opened,
+ created_at: created_at,
+ updated_at: updated_at
+ },
+ project.issues
+ )
+ .and_call_original
+
+ importer.create_issue
+ end
+ end
+
+ context 'when the import fails due to a foreign key error' do
+ it 'does not raise any errors' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(issue)
+ .and_return([user.id, true])
+
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
+
+ expect { importer.create_issue }.not_to raise_error
+ end
+ end
+
+ it 'produces a valid Issue' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(issue)
+ .and_return([user.id, true])
+
+ importer.create_issue
+
+ expect(project.issues.take).to be_valid
+ end
+
+ it 'returns the ID of the created issue' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(issue)
+ .and_return([user.id, true])
+
+ expect(importer.create_issue).to be_a_kind_of(Numeric)
+ end
+ end
+
+ describe '#create_assignees' do
+ it 'inserts the issue assignees in bulk' do
+ importer = described_class.new(issue, project, client)
+
+ allow(importer.user_finder)
+ .to receive(:user_id_for)
+ .ordered.with(issue.assignees[0])
+ .and_return(4)
+
+ allow(importer.user_finder)
+ .to receive(:user_id_for)
+ .ordered.with(issue.assignees[1])
+ .and_return(5)
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(
+ IssueAssignee.table_name,
+ [{ issue_id: 1, user_id: 4 }, { issue_id: 1, user_id: 5 }]
+ )
+
+ importer.create_assignees(1)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
new file mode 100644
index 00000000000..e237e79e94b
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
@@ -0,0 +1,111 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::IssuesImporter do
+ let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ let(:github_issue) do
+ double(
+ :response,
+ number: 42,
+ title: 'My Issue',
+ body: 'This is my issue',
+ milestone: double(:milestone, number: 4),
+ state: 'open',
+ assignees: [double(:user, id: 4, login: 'alice')],
+ labels: [double(:label, name: 'bug')],
+ user: double(:user, id: 4, login: 'alice'),
+ created_at: created_at,
+ updated_at: updated_at,
+ pull_request: false
+ )
+ end
+
+ describe '#parallel?' do
+ it 'returns true when running in parallel mode' do
+ importer = described_class.new(project, client)
+ expect(importer).to be_parallel
+ end
+
+ it 'returns false when running in sequential mode' do
+ importer = described_class.new(project, client, parallel: false)
+ expect(importer).not_to be_parallel
+ end
+ end
+
+ describe '#execute' do
+ context 'when running in parallel mode' do
+ it 'imports issues in parallel' do
+ importer = described_class.new(project, client)
+
+ expect(importer).to receive(:parallel_import)
+
+ importer.execute
+ end
+ end
+
+ context 'when running in sequential mode' do
+ it 'imports issues in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+
+ expect(importer).to receive(:sequential_import)
+
+ importer.execute
+ end
+ end
+ end
+
+ describe '#sequential_import' do
+ it 'imports each issue in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+ issue_importer = double(:importer)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_issue)
+
+ expect(Gitlab::GithubImport::Importer::IssueAndLabelLinksImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::Issue),
+ project,
+ client
+ )
+ .and_return(issue_importer)
+
+ expect(issue_importer).to receive(:execute)
+
+ importer.sequential_import
+ end
+ end
+
+ describe '#parallel_import' do
+ it 'imports each issue in parallel' do
+ importer = described_class.new(project, client)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_issue)
+
+ expect(Gitlab::GithubImport::ImportIssueWorker)
+ .to receive(:perform_async)
+ .with(project.id, an_instance_of(Hash), an_instance_of(String))
+
+ waiter = importer.parallel_import
+
+ expect(waiter).to be_an_instance_of(Gitlab::JobWaiter)
+ expect(waiter.jobs_remaining).to eq(1)
+ end
+ end
+
+ describe '#id_for_already_imported_cache' do
+ it 'returns the issue number of the given issue' do
+ importer = described_class.new(project, client)
+
+ expect(importer.id_for_already_imported_cache(github_issue))
+ .to eq(42)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
new file mode 100644
index 00000000000..e2a71e78574
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::LabelLinksImporter do
+ let(:project) { create(:project) }
+ let(:client) { double(:client) }
+ let(:issue) do
+ double(
+ :issue,
+ iid: 4,
+ label_names: %w[bug],
+ issuable_type: Issue,
+ pull_request?: false
+ )
+ end
+
+ let(:importer) { described_class.new(issue, project, client) }
+
+ describe '#execute' do
+ it 'creates the label links' do
+ importer = described_class.new(issue, project, client)
+
+ expect(importer).to receive(:create_labels)
+
+ importer.execute
+ end
+ end
+
+ describe '#create_labels' do
+ it 'inserts the label links in bulk' do
+ expect(importer.label_finder)
+ .to receive(:id_for)
+ .with('bug')
+ .and_return(2)
+
+ expect(importer)
+ .to receive(:find_target_id)
+ .and_return(1)
+
+ Timecop.freeze do
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(
+ LabelLink.table_name,
+ [
+ {
+ label_id: 2,
+ target_id: 1,
+ target_type: Issue,
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now
+ }
+ ]
+ )
+
+ importer.create_labels
+ end
+ end
+
+ it 'does not insert label links for non-existing labels' do
+ expect(importer.label_finder)
+ .to receive(:id_for)
+ .with('bug')
+ .and_return(nil)
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(LabelLink.table_name, [])
+
+ importer.create_labels
+ end
+ end
+
+ describe '#find_target_id' do
+ it 'returns the ID of the issuable to create the label link for' do
+ expect_any_instance_of(Gitlab::GithubImport::IssuableFinder)
+ .to receive(:database_id)
+ .and_return(10)
+
+ expect(importer.find_target_id).to eq(10)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb
new file mode 100644
index 00000000000..156ef96a0fa
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::LabelsImporter, :clean_gitlab_redis_cache do
+ let(:project) { create(:project, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+ let(:importer) { described_class.new(project, client) }
+
+ describe '#execute' do
+ it 'imports the labels in bulk' do
+ label_hash = { title: 'bug', color: '#fffaaa' }
+
+ expect(importer)
+ .to receive(:build_labels)
+ .and_return([label_hash])
+
+ expect(importer)
+ .to receive(:bulk_insert)
+ .with(Label, [label_hash])
+
+ expect(importer)
+ .to receive(:build_labels_cache)
+
+ importer.execute
+ end
+ end
+
+ describe '#build_labels' do
+ it 'returns an Array containnig label rows' do
+ label = double(:label, name: 'bug', color: 'ffffff')
+
+ expect(importer).to receive(:each_label).and_return([label])
+
+ rows = importer.build_labels
+
+ expect(rows.length).to eq(1)
+ expect(rows[0][:title]).to eq('bug')
+ end
+
+ it 'does not create labels that already exist' do
+ create(:label, project: project, title: 'bug')
+
+ label = double(:label, name: 'bug', color: 'ffffff')
+
+ expect(importer).to receive(:each_label).and_return([label])
+ expect(importer.build_labels).to be_empty
+ end
+ end
+
+ describe '#build_labels_cache' do
+ it 'builds the labels cache' do
+ expect_any_instance_of(Gitlab::GithubImport::LabelFinder)
+ .to receive(:build_cache)
+
+ importer.build_labels_cache
+ end
+ end
+
+ describe '#build' do
+ let(:label_hash) do
+ importer.build(double(:label, name: 'bug', color: 'ffffff'))
+ end
+
+ it 'returns the attributes of the label as a Hash' do
+ expect(label_hash).to be_an_instance_of(Hash)
+ end
+
+ context 'the returned Hash' do
+ it 'includes the label title' do
+ expect(label_hash[:title]).to eq('bug')
+ end
+
+ it 'includes the label color' do
+ expect(label_hash[:color]).to eq('#ffffff')
+ end
+
+ it 'includes the project ID' do
+ expect(label_hash[:project_id]).to eq(project.id)
+ end
+
+ it 'includes the label type' do
+ expect(label_hash[:type]).to eq('ProjectLabel')
+ end
+
+ it 'includes the created timestamp' do
+ Timecop.freeze do
+ expect(label_hash[:created_at]).to eq(Time.zone.now)
+ end
+ end
+
+ it 'includes the updated timestamp' do
+ Timecop.freeze do
+ expect(label_hash[:updated_at]).to eq(Time.zone.now)
+ end
+ end
+ end
+ end
+
+ describe '#each_label' do
+ it 'returns the labels' do
+ expect(client)
+ .to receive(:labels)
+ .with('foo/bar')
+
+ importer.each_label
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
new file mode 100644
index 00000000000..b1cac3b6e46
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis_cache do
+ let(:project) { create(:project, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+ let(:importer) { described_class.new(project, client) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ let(:milestone) do
+ double(
+ :milestone,
+ number: 1,
+ title: '1.0',
+ description: 'The first release',
+ state: 'open',
+ created_at: created_at,
+ updated_at: updated_at
+ )
+ end
+
+ describe '#execute' do
+ it 'imports the milestones in bulk' do
+ milestone_hash = { number: 1, title: '1.0' }
+
+ expect(importer)
+ .to receive(:build_milestones)
+ .and_return([milestone_hash])
+
+ expect(importer)
+ .to receive(:bulk_insert)
+ .with(Milestone, [milestone_hash])
+
+ expect(importer)
+ .to receive(:build_milestones_cache)
+
+ importer.execute
+ end
+ end
+
+ describe '#build_milestones' do
+ it 'returns an Array containnig milestone rows' do
+ expect(importer)
+ .to receive(:each_milestone)
+ .and_return([milestone])
+
+ rows = importer.build_milestones
+
+ expect(rows.length).to eq(1)
+ expect(rows[0][:title]).to eq('1.0')
+ end
+
+ it 'does not create milestones that already exist' do
+ create(:milestone, project: project, title: '1.0', iid: 1)
+
+ expect(importer)
+ .to receive(:each_milestone)
+ .and_return([milestone])
+
+ expect(importer.build_milestones).to be_empty
+ end
+ end
+
+ describe '#build_milestones_cache' do
+ it 'builds the milestones cache' do
+ expect_any_instance_of(Gitlab::GithubImport::MilestoneFinder)
+ .to receive(:build_cache)
+
+ importer.build_milestones_cache
+ end
+ end
+
+ describe '#build' do
+ let(:milestone_hash) { importer.build(milestone) }
+
+ it 'returns the attributes of the milestone as a Hash' do
+ expect(milestone_hash).to be_an_instance_of(Hash)
+ end
+
+ context 'the returned Hash' do
+ it 'includes the milestone number' do
+ expect(milestone_hash[:iid]).to eq(1)
+ end
+
+ it 'includes the milestone title' do
+ expect(milestone_hash[:title]).to eq('1.0')
+ end
+
+ it 'includes the milestone description' do
+ expect(milestone_hash[:description]).to eq('The first release')
+ end
+
+ it 'includes the project ID' do
+ expect(milestone_hash[:project_id]).to eq(project.id)
+ end
+
+ it 'includes the milestone state' do
+ expect(milestone_hash[:state]).to eq(:active)
+ end
+
+ it 'includes the created timestamp' do
+ expect(milestone_hash[:created_at]).to eq(created_at)
+ end
+
+ it 'includes the updated timestamp' do
+ expect(milestone_hash[:updated_at]).to eq(updated_at)
+ end
+ end
+ end
+
+ describe '#each_milestone' do
+ it 'returns the milestones' do
+ expect(client)
+ .to receive(:milestones)
+ .with('foo/bar', state: 'all')
+
+ importer.each_milestone
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
new file mode 100644
index 00000000000..9bdcc42be19
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
@@ -0,0 +1,151 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::NoteImporter do
+ let(:client) { double(:client) }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ let(:github_note) do
+ Gitlab::GithubImport::Representation::Note.new(
+ noteable_id: 1,
+ noteable_type: 'Issue',
+ author: Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice'),
+ note: 'This is my note',
+ created_at: created_at,
+ updated_at: updated_at,
+ github_id: 1
+ )
+ end
+
+ let(:importer) { described_class.new(github_note, project, client) }
+
+ describe '#execute' do
+ context 'when the noteable exists' do
+ let!(:issue_row) { create(:issue, project: project, iid: 1) }
+
+ before do
+ allow(importer)
+ .to receive(:find_noteable_id)
+ .and_return(issue_row.id)
+ end
+
+ context 'when the author could be found' do
+ it 'imports the note with the found author as the note author' do
+ expect(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(github_note)
+ .and_return([user.id, true])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(
+ Note.table_name,
+ [
+ {
+ noteable_type: 'Issue',
+ noteable_id: issue_row.id,
+ project_id: project.id,
+ author_id: user.id,
+ note: 'This is my note',
+ system: false,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+ ]
+ )
+ .and_call_original
+
+ importer.execute
+ end
+ end
+
+ context 'when the note author could not be found' do
+ it 'imports the note with the project creator as the note author' do
+ expect(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(github_note)
+ .and_return([project.creator_id, false])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(
+ Note.table_name,
+ [
+ {
+ noteable_type: 'Issue',
+ noteable_id: issue_row.id,
+ project_id: project.id,
+ author_id: project.creator_id,
+ note: "*Created by: alice*\n\nThis is my note",
+ system: false,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+ ]
+ )
+ .and_call_original
+
+ importer.execute
+ end
+ end
+ end
+
+ context 'when the noteable does not exist' do
+ it 'does not import the note' do
+ expect(Gitlab::Database).not_to receive(:bulk_insert)
+
+ importer.execute
+ end
+ end
+
+ context 'when the import fails due to a foreign key error' do
+ it 'does not raise any errors' do
+ issue_row = create(:issue, project: project, iid: 1)
+
+ allow(importer)
+ .to receive(:find_noteable_id)
+ .and_return(issue_row.id)
+
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(github_note)
+ .and_return([user.id, true])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
+
+ expect { importer.execute }.not_to raise_error
+ end
+ end
+
+ it 'produces a valid Note' do
+ issue_row = create(:issue, project: project, iid: 1)
+
+ allow(importer)
+ .to receive(:find_noteable_id)
+ .and_return(issue_row.id)
+
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(github_note)
+ .and_return([user.id, true])
+
+ importer.execute
+
+ expect(project.notes.take).to be_valid
+ end
+ end
+
+ describe '#find_noteable_id' do
+ it 'returns the ID of the noteable' do
+ expect_any_instance_of(Gitlab::GithubImport::IssuableFinder)
+ .to receive(:database_id)
+ .and_return(10)
+
+ expect(importer.find_noteable_id).to eq(10)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
new file mode 100644
index 00000000000..f046d13f879
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
@@ -0,0 +1,116 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::NotesImporter do
+ let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+
+ let(:github_comment) do
+ double(
+ :response,
+ html_url: 'https://github.com/foo/bar/issues/42',
+ user: double(:user, id: 4, login: 'alice'),
+ body: 'Hello world',
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now,
+ id: 1
+ )
+ end
+
+ describe '#parallel?' do
+ it 'returns true when running in parallel mode' do
+ importer = described_class.new(project, client)
+ expect(importer).to be_parallel
+ end
+
+ it 'returns false when running in sequential mode' do
+ importer = described_class.new(project, client, parallel: false)
+ expect(importer).not_to be_parallel
+ end
+ end
+
+ describe '#execute' do
+ context 'when running in parallel mode' do
+ it 'imports notes in parallel' do
+ importer = described_class.new(project, client)
+
+ expect(importer).to receive(:parallel_import)
+
+ importer.execute
+ end
+ end
+
+ context 'when running in sequential mode' do
+ it 'imports notes in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+
+ expect(importer).to receive(:sequential_import)
+
+ importer.execute
+ end
+ end
+ end
+
+ describe '#sequential_import' do
+ it 'imports each note in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+ note_importer = double(:note_importer)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_comment)
+
+ expect(Gitlab::GithubImport::Importer::NoteImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::Note),
+ project,
+ client
+ )
+ .and_return(note_importer)
+
+ expect(note_importer).to receive(:execute)
+
+ importer.sequential_import
+ end
+ end
+
+ describe '#parallel_import' do
+ it 'imports each note in parallel' do
+ importer = described_class.new(project, client)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(github_comment)
+
+ expect(Gitlab::GithubImport::ImportNoteWorker)
+ .to receive(:perform_async)
+ .with(project.id, an_instance_of(Hash), an_instance_of(String))
+
+ waiter = importer.parallel_import
+
+ expect(waiter).to be_an_instance_of(Gitlab::JobWaiter)
+ expect(waiter.jobs_remaining).to eq(1)
+ end
+ end
+
+ describe '#id_for_already_imported_cache' do
+ it 'returns the ID of the given note' do
+ importer = described_class.new(project, client)
+
+ expect(importer.id_for_already_imported_cache(github_comment))
+ .to eq(1)
+ end
+ end
+
+ describe '#collection_options' do
+ it 'returns an empty Hash' do
+ # For large projects (e.g. kubernetes/kubernetes) GitHub's API may produce
+ # HTTP 500 errors when using explicit sorting options, regardless of what
+ # order you sort in. Not using any sorting options at all allows us to
+ # work around this.
+ importer = described_class.new(project, client)
+
+ expect(importer.collection_options).to eq({})
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
new file mode 100644
index 00000000000..35f3fdf8304
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
@@ -0,0 +1,221 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redis_cache do
+ let(:project) { create(:project, :repository) }
+ let(:client) { double(:client) }
+ let(:user) { create(:user) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+ let(:merged_at) { Time.new(2017, 1, 1, 12, 17) }
+
+ let(:source_commit) { project.repository.commit('feature') }
+ let(:target_commit) { project.repository.commit('master') }
+ let(:milestone) { create(:milestone, project: project) }
+
+ let(:pull_request) do
+ alice = Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice')
+
+ Gitlab::GithubImport::Representation::PullRequest.new(
+ iid: 42,
+ title: 'My Pull Request',
+ description: 'This is my pull request',
+ source_branch: 'feature',
+ source_branch_sha: source_commit.id,
+ target_branch: 'master',
+ target_branch_sha: target_commit.id,
+ source_repository_id: 400,
+ target_repository_id: 200,
+ source_repository_owner: 'alice',
+ state: :closed,
+ milestone_number: milestone.iid,
+ author: alice,
+ assignee: alice,
+ created_at: created_at,
+ updated_at: updated_at,
+ merged_at: merged_at
+ )
+ end
+
+ let(:importer) { described_class.new(pull_request, project, client) }
+
+ describe '#execute' do
+ it 'imports the pull request' do
+ expect(importer)
+ .to receive(:create_merge_request)
+ .and_return(10)
+
+ expect_any_instance_of(Gitlab::GithubImport::IssuableFinder)
+ .to receive(:cache_database_id)
+ .with(10)
+
+ importer.execute
+ end
+ end
+
+ describe '#create_merge_request' do
+ before do
+ allow(importer.milestone_finder)
+ .to receive(:id_for)
+ .with(pull_request)
+ .and_return(milestone.id)
+ end
+
+ context 'when the author could be found' do
+ before do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(pull_request)
+ .and_return([user.id, true])
+
+ allow(importer.user_finder)
+ .to receive(:assignee_id_for)
+ .with(pull_request)
+ .and_return(user.id)
+ end
+
+ it 'imports the pull request with the pull request author as the merge request author' do
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .with(
+ {
+ iid: 42,
+ title: 'My Pull Request',
+ description: 'This is my pull request',
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'alice:feature',
+ target_branch: 'master',
+ state: :merged,
+ milestone_id: milestone.id,
+ author_id: user.id,
+ assignee_id: user.id,
+ created_at: created_at,
+ updated_at: updated_at
+ },
+ project.merge_requests
+ )
+ .and_call_original
+
+ importer.create_merge_request
+ end
+
+ it 'returns the ID of the created merge request' do
+ id = importer.create_merge_request
+
+ expect(id).to be_a_kind_of(Numeric)
+ end
+
+ it 'creates the merge request diffs' do
+ importer.create_merge_request
+
+ mr = project.merge_requests.take
+
+ expect(mr.merge_request_diffs.exists?).to eq(true)
+ end
+ end
+
+ context 'when the author could not be found' do
+ it 'imports the pull request with the project creator as the merge request author' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(pull_request)
+ .and_return([project.creator_id, false])
+
+ allow(importer.user_finder)
+ .to receive(:assignee_id_for)
+ .with(pull_request)
+ .and_return(user.id)
+
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .with(
+ {
+ iid: 42,
+ title: 'My Pull Request',
+ description: "*Created by: alice*\n\nThis is my pull request",
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'alice:feature',
+ target_branch: 'master',
+ state: :merged,
+ milestone_id: milestone.id,
+ author_id: project.creator_id,
+ assignee_id: user.id,
+ created_at: created_at,
+ updated_at: updated_at
+ },
+ project.merge_requests
+ )
+ .and_call_original
+
+ importer.create_merge_request
+ end
+ end
+
+ context 'when the source and target branch are identical' do
+ it 'uses a generated source branch name for the merge request' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(pull_request)
+ .and_return([user.id, true])
+
+ allow(importer.user_finder)
+ .to receive(:assignee_id_for)
+ .with(pull_request)
+ .and_return(user.id)
+
+ allow(pull_request)
+ .to receive(:source_repository_id)
+ .and_return(pull_request.target_repository_id)
+
+ allow(pull_request)
+ .to receive(:source_branch)
+ .and_return('master')
+
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .with(
+ {
+ iid: 42,
+ title: 'My Pull Request',
+ description: 'This is my pull request',
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'master-42',
+ target_branch: 'master',
+ state: :merged,
+ milestone_id: milestone.id,
+ author_id: user.id,
+ assignee_id: user.id,
+ created_at: created_at,
+ updated_at: updated_at
+ },
+ project.merge_requests
+ )
+ .and_call_original
+
+ importer.create_merge_request
+ end
+ end
+
+ context 'when the import fails due to a foreign key error' do
+ it 'does not raise any errors' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(pull_request)
+ .and_return([user.id, true])
+
+ allow(importer.user_finder)
+ .to receive(:assignee_id_for)
+ .with(pull_request)
+ .and_return(user.id)
+
+ expect(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
+
+ expect { importer.create_merge_request }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
new file mode 100644
index 00000000000..d72572cd510
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -0,0 +1,272 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::PullRequestsImporter do
+ let(:project) { create(:project, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+
+ let(:pull_request) do
+ double(
+ :response,
+ number: 42,
+ title: 'My Pull Request',
+ body: 'This is my pull request',
+ state: 'closed',
+ head: double(
+ :head,
+ sha: '123abc',
+ ref: 'my-feature',
+ repo: double(:repo, id: 400),
+ user: double(:user, id: 4, login: 'alice')
+ ),
+ base: double(
+ :base,
+ sha: '456def',
+ ref: 'master',
+ repo: double(:repo, id: 200)
+ ),
+ milestone: double(:milestone, number: 4),
+ user: double(:user, id: 4, login: 'alice'),
+ assignee: double(:user, id: 4, login: 'alice'),
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now,
+ merged_at: Time.zone.now
+ )
+ end
+
+ describe '#parallel?' do
+ it 'returns true when running in parallel mode' do
+ importer = described_class.new(project, client)
+ expect(importer).to be_parallel
+ end
+
+ it 'returns false when running in sequential mode' do
+ importer = described_class.new(project, client, parallel: false)
+ expect(importer).not_to be_parallel
+ end
+ end
+
+ describe '#execute' do
+ context 'when running in parallel mode' do
+ it 'imports pull requests in parallel' do
+ importer = described_class.new(project, client)
+
+ expect(importer).to receive(:parallel_import)
+
+ importer.execute
+ end
+ end
+
+ context 'when running in sequential mode' do
+ it 'imports pull requests in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+
+ expect(importer).to receive(:sequential_import)
+
+ importer.execute
+ end
+ end
+ end
+
+ describe '#sequential_import' do
+ it 'imports each pull request in sequence' do
+ importer = described_class.new(project, client, parallel: false)
+ pull_request_importer = double(:pull_request_importer)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(pull_request)
+
+ expect(Gitlab::GithubImport::Importer::PullRequestImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::PullRequest),
+ project,
+ client
+ )
+ .and_return(pull_request_importer)
+
+ expect(pull_request_importer).to receive(:execute)
+
+ importer.sequential_import
+ end
+ end
+
+ describe '#parallel_import' do
+ it 'imports each note in parallel' do
+ importer = described_class.new(project, client)
+
+ allow(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(pull_request)
+
+ expect(Gitlab::GithubImport::ImportPullRequestWorker)
+ .to receive(:perform_async)
+ .with(project.id, an_instance_of(Hash), an_instance_of(String))
+
+ waiter = importer.parallel_import
+
+ expect(waiter).to be_an_instance_of(Gitlab::JobWaiter)
+ expect(waiter.jobs_remaining).to eq(1)
+ end
+ end
+
+ describe '#each_object_to_import', :clean_gitlab_redis_cache do
+ let(:importer) { described_class.new(project, client) }
+
+ before do
+ page = double(:page, objects: [pull_request], number: 1)
+
+ expect(client)
+ .to receive(:each_page)
+ .with(
+ :pull_requests,
+ 'foo/bar',
+ { state: 'all', sort: 'created', direction: 'asc', page: 1 }
+ )
+ .and_yield(page)
+ end
+
+ it 'yields every pull request to the supplied block' do
+ expect { |b| importer.each_object_to_import(&b) }
+ .to yield_with_args(pull_request)
+ end
+
+ it 'updates the repository if a pull request was updated after the last clone' do
+ expect(importer)
+ .to receive(:update_repository?)
+ .with(pull_request)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:update_repository)
+
+ importer.each_object_to_import { }
+ end
+ end
+
+ describe '#update_repository' do
+ it 'updates the repository' do
+ importer = described_class.new(project, client)
+
+ expect(project.repository)
+ .to receive(:fetch_remote)
+ .with('github', forced: false)
+
+ expect(Rails.logger)
+ .to receive(:info)
+ .with(an_instance_of(String))
+
+ expect(importer.repository_updates_counter)
+ .to receive(:increment)
+ .with(project: project.path_with_namespace)
+ .and_call_original
+
+ Timecop.freeze do
+ importer.update_repository
+
+ expect(project.last_repository_updated_at).to eq(Time.zone.now)
+ end
+ end
+ end
+
+ describe '#update_repository?' do
+ let(:importer) { described_class.new(project, client) }
+
+ context 'when the pull request was updated after the last update' do
+ let(:pr) do
+ double(
+ :pr,
+ updated_at: Time.zone.now,
+ head: double(:head, sha: '123'),
+ base: double(:base, sha: '456')
+ )
+ end
+
+ before do
+ allow(project)
+ .to receive(:last_repository_updated_at)
+ .and_return(1.year.ago)
+ end
+
+ it 'returns true when the head SHA is not present' do
+ expect(importer)
+ .to receive(:commit_exists?)
+ .with(pr.head.sha)
+ .and_return(false)
+
+ expect(importer.update_repository?(pr)).to eq(true)
+ end
+
+ it 'returns true when the base SHA is not present' do
+ expect(importer)
+ .to receive(:commit_exists?)
+ .with(pr.head.sha)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:commit_exists?)
+ .with(pr.base.sha)
+ .and_return(false)
+
+ expect(importer.update_repository?(pr)).to eq(true)
+ end
+
+ it 'returns false if both the head and base SHAs are present' do
+ expect(importer)
+ .to receive(:commit_exists?)
+ .with(pr.head.sha)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:commit_exists?)
+ .with(pr.base.sha)
+ .and_return(true)
+
+ expect(importer.update_repository?(pr)).to eq(false)
+ end
+ end
+
+ context 'when the pull request was updated before the last update' do
+ it 'returns false' do
+ pr = double(:pr, updated_at: 1.year.ago)
+
+ allow(project)
+ .to receive(:last_repository_updated_at)
+ .and_return(Time.zone.now)
+
+ expect(importer.update_repository?(pr)).to eq(false)
+ end
+ end
+ end
+
+ describe '#commit_exists?' do
+ let(:importer) { described_class.new(project, client) }
+
+ it 'returns true when a commit exists' do
+ expect(project.repository)
+ .to receive(:lookup)
+ .with('123')
+ .and_return(double(:commit))
+
+ expect(importer.commit_exists?('123')).to eq(true)
+ end
+
+ it 'returns false when a commit does not exist' do
+ expect(project.repository)
+ .to receive(:lookup)
+ .with('123')
+ .and_raise(Rugged::OdbError)
+
+ expect(importer.commit_exists?('123')).to eq(false)
+ end
+ end
+
+ describe '#id_for_already_imported_cache' do
+ it 'returns the PR number of the given PR' do
+ importer = described_class.new(project, client)
+
+ expect(importer.id_for_already_imported_cache(pull_request))
+ .to eq(42)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
new file mode 100644
index 00000000000..23ae026fb14
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::ReleasesImporter do
+ let(:project) { create(:project) }
+ let(:client) { double(:client) }
+ let(:importer) { described_class.new(project, client) }
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ let(:release) do
+ double(
+ :release,
+ tag_name: '1.0',
+ body: 'This is my release',
+ created_at: created_at,
+ updated_at: updated_at
+ )
+ end
+
+ describe '#execute' do
+ it 'imports the releases in bulk' do
+ release_hash = {
+ tag_name: '1.0',
+ description: 'This is my release',
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(importer).to receive(:build_releases).and_return([release_hash])
+ expect(importer).to receive(:bulk_insert).with(Release, [release_hash])
+
+ importer.execute
+ end
+ end
+
+ describe '#build_releases' do
+ it 'returns an Array containnig release rows' do
+ expect(importer).to receive(:each_release).and_return([release])
+
+ rows = importer.build_releases
+
+ expect(rows.length).to eq(1)
+ expect(rows[0][:tag]).to eq('1.0')
+ end
+
+ it 'does not create releases that already exist' do
+ create(:release, project: project, tag: '1.0', description: '1.0')
+
+ expect(importer).to receive(:each_release).and_return([release])
+ expect(importer.build_releases).to be_empty
+ end
+
+ it 'uses a default release description if none is provided' do
+ expect(release).to receive(:body).and_return('')
+ expect(importer).to receive(:each_release).and_return([release])
+
+ release = importer.build_releases.first
+
+ expect(release[:description]).to eq('Release for tag 1.0')
+ end
+ end
+
+ describe '#build' do
+ let(:release_hash) { importer.build(release) }
+
+ it 'returns the attributes of the release as a Hash' do
+ expect(release_hash).to be_an_instance_of(Hash)
+ end
+
+ context 'the returned Hash' do
+ it 'includes the tag name' do
+ expect(release_hash[:tag]).to eq('1.0')
+ end
+
+ it 'includes the release description' do
+ expect(release_hash[:description]).to eq('This is my release')
+ end
+
+ it 'includes the project ID' do
+ expect(release_hash[:project_id]).to eq(project.id)
+ end
+
+ it 'includes the created timestamp' do
+ expect(release_hash[:created_at]).to eq(created_at)
+ end
+
+ it 'includes the updated timestamp' do
+ expect(release_hash[:updated_at]).to eq(updated_at)
+ end
+ end
+ end
+
+ describe '#each_release' do
+ let(:release) { double(:release) }
+
+ before do
+ allow(project).to receive(:import_source).and_return('foo/bar')
+
+ allow(client)
+ .to receive(:releases)
+ .with('foo/bar')
+ .and_return([release].to_enum)
+ end
+
+ it 'returns an Enumerator' do
+ expect(importer.each_release).to be_an_instance_of(Enumerator)
+ end
+
+ it 'yields every release to the Enumerator' do
+ expect(importer.each_release.next).to eq(release)
+ end
+ end
+
+ describe '#description_for' do
+ it 'returns the description when present' do
+ expect(importer.description_for(release)).to eq(release.body)
+ end
+
+ it 'returns a generated description when one is not present' do
+ allow(release).to receive(:body).and_return('')
+
+ expect(importer.description_for(release)).to eq('Release for tag 1.0')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
new file mode 100644
index 00000000000..168e5d07504
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -0,0 +1,227 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer::RepositoryImporter do
+ let(:repository) { double(:repository) }
+ let(:client) { double(:client) }
+
+ let(:project) do
+ double(
+ :project,
+ import_url: 'foo.git',
+ import_source: 'foo/bar',
+ repository_storage_path: 'foo',
+ disk_path: 'foo',
+ repository: repository
+ )
+ end
+
+ let(:importer) { described_class.new(project, client) }
+ let(:shell_adapter) { Gitlab::Shell.new }
+
+ before do
+ # The method "gitlab_shell" returns a new instance every call, making
+ # it harder to set expectations. To work around this we'll stub the method
+ # and return the same instance on every call.
+ allow(importer).to receive(:gitlab_shell).and_return(shell_adapter)
+ end
+
+ describe '#import_wiki?' do
+ it 'returns true if the wiki should be imported' do
+ repo = double(:repo, has_wiki: true)
+
+ expect(client)
+ .to receive(:repository)
+ .with('foo/bar')
+ .and_return(repo)
+
+ expect(project)
+ .to receive(:wiki_repository_exists?)
+ .and_return(false)
+
+ expect(importer.import_wiki?).to eq(true)
+ end
+
+ it 'returns false if the GitHub wiki is disabled' do
+ repo = double(:repo, has_wiki: false)
+
+ expect(client)
+ .to receive(:repository)
+ .with('foo/bar')
+ .and_return(repo)
+
+ expect(importer.import_wiki?).to eq(false)
+ end
+
+ it 'returns false if the wiki has already been imported' do
+ repo = double(:repo, has_wiki: true)
+
+ expect(client)
+ .to receive(:repository)
+ .with('foo/bar')
+ .and_return(repo)
+
+ expect(project)
+ .to receive(:wiki_repository_exists?)
+ .and_return(true)
+
+ expect(importer.import_wiki?).to eq(false)
+ end
+ end
+
+ describe '#execute' do
+ it 'imports the repository and wiki' do
+ expect(repository)
+ .to receive(:empty_repo?)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:import_wiki?)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:import_repository)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:import_wiki_repository)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:update_clone_time)
+
+ expect(importer.execute).to eq(true)
+ end
+
+ it 'does not import the repository if it already exists' do
+ expect(repository)
+ .to receive(:empty_repo?)
+ .and_return(false)
+
+ expect(importer)
+ .to receive(:import_wiki?)
+ .and_return(true)
+
+ expect(importer)
+ .not_to receive(:import_repository)
+
+ expect(importer)
+ .to receive(:import_wiki_repository)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:update_clone_time)
+
+ expect(importer.execute).to eq(true)
+ end
+
+ it 'does not import the wiki if it is disabled' do
+ expect(repository)
+ .to receive(:empty_repo?)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:import_wiki?)
+ .and_return(false)
+
+ expect(importer)
+ .to receive(:import_repository)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:update_clone_time)
+
+ expect(importer)
+ .not_to receive(:import_wiki_repository)
+
+ expect(importer.execute).to eq(true)
+ end
+
+ it 'does not import the wiki if the repository could not be imported' do
+ expect(repository)
+ .to receive(:empty_repo?)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:import_wiki?)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:import_repository)
+ .and_return(false)
+
+ expect(importer)
+ .not_to receive(:update_clone_time)
+
+ expect(importer)
+ .not_to receive(:import_wiki_repository)
+
+ expect(importer.execute).to eq(false)
+ end
+ end
+
+ describe '#import_repository' do
+ it 'imports the repository' do
+ expect(project)
+ .to receive(:ensure_repository)
+
+ expect(repository)
+ .to receive(:fetch_as_mirror)
+ .with(project.import_url, refmap: Gitlab::GithubImport.refmap, forced: true, remote_name: 'github')
+
+ expect(importer.import_repository).to eq(true)
+ end
+
+ it 'marks the import as failed when an error was raised' do
+ expect(project).to receive(:ensure_repository)
+ .and_raise(Gitlab::Git::Repository::NoRepository)
+
+ expect(importer)
+ .to receive(:fail_import)
+ .and_return(false)
+
+ expect(importer.import_repository).to eq(false)
+ end
+ end
+
+ describe '#import_wiki_repository' do
+ it 'imports the wiki repository' do
+ expect(importer.gitlab_shell)
+ .to receive(:import_repository)
+ .with('foo', 'foo.wiki', 'foo.wiki.git')
+
+ expect(importer.import_wiki_repository).to eq(true)
+ end
+
+ it 'marks the import as failed if an error was raised' do
+ expect(importer.gitlab_shell)
+ .to receive(:import_repository)
+ .and_raise(Gitlab::Shell::Error)
+
+ expect(importer)
+ .to receive(:fail_import)
+ .and_return(false)
+
+ expect(importer.import_wiki_repository).to eq(false)
+ end
+ end
+
+ describe '#fail_import' do
+ it 'marks the import as failed' do
+ expect(project).to receive(:mark_import_as_failed).with('foo')
+
+ expect(importer.fail_import('foo')).to eq(false)
+ end
+ end
+
+ describe '#update_clone_time' do
+ it 'sets the timestamp for when the cloning process finished' do
+ Timecop.freeze do
+ expect(project)
+ .to receive(:update_column)
+ .with(:last_repository_updated_at, Time.zone.now)
+
+ importer.update_clone_time
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
deleted file mode 100644
index d570f34985b..00000000000
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ /dev/null
@@ -1,309 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::Importer do
- shared_examples 'Gitlab::GithubImport::Importer#execute' do
- let(:expected_not_called) { [] }
-
- before do
- allow(project).to receive(:import_data).and_return(double.as_null_object)
- end
-
- it 'calls import methods' do
- importer = described_class.new(project)
-
- expected_called = [
- :import_labels, :import_milestones, :import_pull_requests, :import_issues,
- :import_wiki, :import_releases, :handle_errors
- ]
-
- expected_called -= expected_not_called
-
- aggregate_failures do
- expected_called.each do |method_name|
- expect(importer).to receive(method_name)
- end
-
- expect(importer).to receive(:import_comments).with(:issues)
- expect(importer).to receive(:import_comments).with(:pull_requests)
-
- expected_not_called.each do |method_name|
- expect(importer).not_to receive(method_name)
- end
- end
-
- importer.execute
- end
- end
-
- shared_examples 'Gitlab::GithubImport::Importer#execute an error occurs' do
- before do
- allow(project).to receive(:import_data).and_return(double.as_null_object)
-
- allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
-
- allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound)
- allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
-
- allow_any_instance_of(Octokit::Client).to receive(:user).and_return(octocat)
- allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2])
- allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
- allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
- allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request])
- allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([])
- allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([])
- allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
- allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
- end
-
- let(:label1) do
- double(
- name: 'Bug',
- color: 'ff0000',
- url: "#{api_root}/repos/octocat/Hello-World/labels/bug"
- )
- end
-
- let(:label2) do
- double(
- name: nil,
- color: 'ff0000',
- url: "#{api_root}/repos/octocat/Hello-World/labels/bug"
- )
- end
-
- let(:milestone) do
- double(
- id: 1347, # For Gitea
- number: 1347,
- state: 'open',
- title: '1.0',
- description: 'Version 1.0',
- due_on: nil,
- created_at: created_at,
- updated_at: updated_at,
- closed_at: nil,
- url: "#{api_root}/repos/octocat/Hello-World/milestones/1"
- )
- end
-
- let(:issue1) do
- double(
- number: 1347,
- milestone: nil,
- state: 'open',
- title: 'Found a bug',
- body: "I'm having a problem with this.",
- assignee: nil,
- user: octocat,
- comments: 0,
- pull_request: nil,
- created_at: created_at,
- updated_at: updated_at,
- closed_at: nil,
- url: "#{api_root}/repos/octocat/Hello-World/issues/1347",
- labels: [double(name: 'Label #1')]
- )
- end
-
- let(:issue2) do
- double(
- number: 1348,
- milestone: nil,
- state: 'open',
- title: nil,
- body: "I'm having a problem with this.",
- assignee: nil,
- user: octocat,
- comments: 0,
- pull_request: nil,
- created_at: created_at,
- updated_at: updated_at,
- closed_at: nil,
- url: "#{api_root}/repos/octocat/Hello-World/issues/1348",
- labels: [double(name: 'Label #2')]
- )
- end
-
- let(:release1) do
- double(
- tag_name: 'v1.0.0',
- name: 'First release',
- body: 'Release v1.0.0',
- draft: false,
- created_at: created_at,
- updated_at: updated_at,
- url: "#{api_root}/repos/octocat/Hello-World/releases/1"
- )
- end
-
- let(:release2) do
- double(
- tag_name: 'v2.0.0',
- name: 'Second release',
- body: nil,
- draft: false,
- created_at: created_at,
- updated_at: updated_at,
- url: "#{api_root}/repos/octocat/Hello-World/releases/2"
- )
- end
-
- subject { described_class.new(project) }
-
- it 'returns true' do
- expect(subject.execute).to eq true
- end
-
- it 'does not raise an error' do
- expect { subject.execute }.not_to raise_error
- end
-
- it 'stores error messages' do
- error = {
- message: 'The remote data could not be fully imported.',
- errors: [
- { type: :label, url: "#{api_root}/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
- { type: :issue, url: "#{api_root}/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" },
- { type: :wiki, errors: "Gitlab::Shell::Error" }
- ]
- }
-
- unless project.gitea_import?
- error[:errors] << { type: :release, url: "#{api_root}/repos/octocat/Hello-World/releases/2", errors: "Validation failed: Description can't be blank" }
- end
-
- described_class.new(project).execute
-
- expect(project.import_error).to eq error.to_json
- end
- end
-
- shared_examples 'Gitlab::GithubImport unit-testing' do
- describe '#clean_up_restored_branches' do
- subject { described_class.new(project) }
-
- before do
- allow(gh_pull_request).to receive(:source_branch_exists?).at_least(:once) { false }
- allow(gh_pull_request).to receive(:target_branch_exists?).at_least(:once) { false }
- end
-
- context 'when pull request stills open' do
- let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, pull_request) }
-
- it 'does not remove branches' do
- expect(subject).not_to receive(:remove_branch)
- subject.send(:clean_up_restored_branches, gh_pull_request)
- end
- end
-
- context 'when pull request is closed' do
- let(:gh_pull_request) { Gitlab::GithubImport::PullRequestFormatter.new(project, closed_pull_request) }
-
- it 'does remove branches' do
- expect(subject).to receive(:remove_branch).at_least(2).times
- subject.send(:clean_up_restored_branches, gh_pull_request)
- end
- end
- end
- end
-
- let(:project) { create(:project, :repository, :wiki_disabled, import_url: "#{repo_root}/octocat/Hello-World.git") }
- let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
- let(:credentials) { { user: 'joe' } }
-
- let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
- let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
- let(:repository) { double(id: 1, fork: false) }
- let(:source_sha) { create(:commit, project: project).id }
- let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha, user: octocat) }
- let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
- let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha, user: octocat) }
- let(:pull_request) do
- double(
- number: 1347,
- milestone: nil,
- state: 'open',
- title: 'New feature',
- body: 'Please pull these awesome changes',
- head: source_branch,
- base: target_branch,
- assignee: nil,
- user: octocat,
- created_at: created_at,
- updated_at: updated_at,
- closed_at: nil,
- merged_at: nil,
- url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
- labels: [double(name: 'Label #2')]
- )
- end
- let(:closed_pull_request) do
- double(
- number: 1347,
- milestone: nil,
- state: 'closed',
- title: 'New feature',
- body: 'Please pull these awesome changes',
- head: source_branch,
- base: target_branch,
- assignee: nil,
- user: octocat,
- created_at: created_at,
- updated_at: updated_at,
- closed_at: updated_at,
- merged_at: nil,
- url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
- labels: [double(name: 'Label #2')]
- )
- end
-
- context 'when importing a GitHub project' do
- let(:api_root) { 'https://api.github.com' }
- let(:repo_root) { 'https://github.com' }
- subject { described_class.new(project) }
-
- it_behaves_like 'Gitlab::GithubImport::Importer#execute'
- it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
- it_behaves_like 'Gitlab::GithubImport unit-testing'
-
- describe '#client' do
- it 'instantiates a Client' do
- allow(project).to receive(:import_data).and_return(double(credentials: credentials))
- expect(Gitlab::GithubImport::Client).to receive(:new).with(
- credentials[:user],
- {}
- )
-
- subject.client
- end
- end
- end
-
- context 'when importing a Gitea project' do
- let(:api_root) { 'https://try.gitea.io/api/v1' }
- let(:repo_root) { 'https://try.gitea.io' }
- subject { described_class.new(project) }
-
- before do
- project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git")
- end
-
- it_behaves_like 'Gitlab::GithubImport::Importer#execute' do
- let(:expected_not_called) { [:import_releases] }
- end
- it_behaves_like 'Gitlab::GithubImport::Importer#execute an error occurs'
- it_behaves_like 'Gitlab::GithubImport unit-testing'
-
- describe '#client' do
- it 'instantiates a Client' do
- allow(project).to receive(:import_data).and_return(double(credentials: credentials))
- expect(Gitlab::GithubImport::Client).to receive(:new).with(
- credentials[:user],
- { host: "#{repo_root}:443/foo", api_version: 'v1' }
- )
-
- subject.client
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/github_import/issuable_finder_spec.rb b/spec/lib/gitlab/github_import/issuable_finder_spec.rb
new file mode 100644
index 00000000000..da69911812a
--- /dev/null
+++ b/spec/lib/gitlab/github_import/issuable_finder_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::IssuableFinder, :clean_gitlab_redis_cache do
+ let(:project) { double(:project, id: 4) }
+ let(:issue) do
+ double(:issue, issuable_type: MergeRequest, iid: 1)
+ end
+
+ let(:finder) { described_class.new(project, issue) }
+
+ describe '#database_id' do
+ it 'returns nil when no cache is in place' do
+ expect(finder.database_id).to be_nil
+ end
+
+ it 'returns the ID of an issuable when the cache is in place' do
+ finder.cache_database_id(10)
+
+ expect(finder.database_id).to eq(10)
+ end
+
+ it 'raises TypeError when the object is not supported' do
+ finder = described_class.new(project, double(:issue))
+
+ expect { finder.database_id }.to raise_error(TypeError)
+ end
+ end
+
+ describe '#cache_database_id' do
+ it 'caches the ID of a database row' do
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .with('github-import/issuable-finder/4/MergeRequest/1', 10)
+
+ finder.cache_database_id(10)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/issuable_formatter_spec.rb b/spec/lib/gitlab/github_import/issuable_formatter_spec.rb
deleted file mode 100644
index 05294d227bd..00000000000
--- a/spec/lib/gitlab/github_import/issuable_formatter_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::IssuableFormatter do
- let(:raw_data) do
- double(number: 42)
- end
- let(:project) { double(import_type: 'github') }
- let(:issuable_formatter) { described_class.new(project, raw_data) }
-
- describe '#project_association' do
- it { expect { issuable_formatter.project_association }.to raise_error(NotImplementedError) }
- end
-
- describe '#number' do
- it { expect(issuable_formatter.number).to eq(42) }
- end
-
- describe '#find_condition' do
- it { expect(issuable_formatter.find_condition).to eq({ iid: 42 }) }
- end
-end
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
deleted file mode 100644
index 0fc56d92aa6..00000000000
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ /dev/null
@@ -1,195 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::IssueFormatter do
- let(:client) { double }
- let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
- let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
- let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
- let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
-
- let(:base_data) do
- {
- number: 1347,
- milestone: nil,
- state: 'open',
- title: 'Found a bug',
- body: "I'm having a problem with this.",
- assignee: nil,
- user: octocat,
- comments: 0,
- pull_request: nil,
- created_at: created_at,
- updated_at: updated_at,
- closed_at: nil
- }
- end
-
- subject(:issue) { described_class.new(project, raw_data, client) }
-
- before do
- allow(client).to receive(:user).and_return(octocat)
- end
-
- shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do
- context 'when issue is open' do
- let(:raw_data) { double(base_data.merge(state: 'open')) }
-
- it 'returns formatted attributes' do
- expected = {
- iid: 1347,
- project: project,
- milestone: nil,
- title: 'Found a bug',
- description: "*Created by: octocat*\n\nI'm having a problem with this.",
- state: 'opened',
- author_id: project.creator_id,
- assignee_ids: [],
- created_at: created_at,
- updated_at: updated_at
- }
-
- expect(issue.attributes).to eq(expected)
- end
- end
-
- context 'when issue is closed' do
- let(:raw_data) { double(base_data.merge(state: 'closed')) }
-
- it 'returns formatted attributes' do
- expected = {
- iid: 1347,
- project: project,
- milestone: nil,
- title: 'Found a bug',
- description: "*Created by: octocat*\n\nI'm having a problem with this.",
- state: 'closed',
- author_id: project.creator_id,
- assignee_ids: [],
- created_at: created_at,
- updated_at: updated_at
- }
-
- expect(issue.attributes).to eq(expected)
- end
- end
-
- context 'when it is assigned to someone' do
- let(:raw_data) { double(base_data.merge(assignee: octocat)) }
-
- it 'returns nil as assignee_id when is not a GitLab user' do
- expect(issue.attributes.fetch(:assignee_ids)).to be_empty
- end
-
- it 'returns GitLab user id associated with GitHub id as assignee_id' do
- gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
-
- expect(issue.attributes.fetch(:assignee_ids)).to eq [gl_user.id]
- end
-
- it 'returns GitLab user id associated with GitHub email as assignee_id' do
- gl_user = create(:user, email: octocat.email)
-
- expect(issue.attributes.fetch(:assignee_ids)).to eq [gl_user.id]
- end
- end
-
- context 'when it has a milestone' do
- let(:milestone) { double(id: 42, number: 42) }
- let(:raw_data) { double(base_data.merge(milestone: milestone)) }
-
- it 'returns nil when milestone does not exist' do
- expect(issue.attributes.fetch(:milestone)).to be_nil
- end
-
- it 'returns milestone when it exists' do
- milestone = create(:milestone, project: project, iid: 42)
-
- expect(issue.attributes.fetch(:milestone)).to eq milestone
- end
- end
-
- context 'when author is a GitLab user' do
- let(:raw_data) { double(base_data.merge(user: octocat)) }
-
- it 'returns project creator_id as author_id when is not a GitLab user' do
- expect(issue.attributes.fetch(:author_id)).to eq project.creator_id
- end
-
- it 'returns GitLab user id associated with GitHub id as author_id' do
- gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
-
- expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
- end
-
- it 'returns GitLab user id associated with GitHub email as author_id' do
- gl_user = create(:user, email: octocat.email)
-
- expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
- end
-
- it 'returns description without created at tag line' do
- create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
-
- expect(issue.attributes.fetch(:description)).to eq("I'm having a problem with this.")
- end
- end
- end
-
- shared_examples 'Gitlab::GithubImport::IssueFormatter#number' do
- let(:raw_data) { double(base_data.merge(number: 1347)) }
-
- it 'returns issue number' do
- expect(issue.number).to eq 1347
- end
- end
-
- context 'when importing a GitHub project' do
- it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes'
- it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number'
- end
-
- context 'when importing a Gitea project' do
- before do
- project.update(import_type: 'gitea')
- end
-
- it_behaves_like 'Gitlab::GithubImport::IssueFormatter#attributes'
- it_behaves_like 'Gitlab::GithubImport::IssueFormatter#number'
- end
-
- describe '#has_comments?' do
- context 'when number of comments is greater than zero' do
- let(:raw_data) { double(base_data.merge(comments: 1)) }
-
- it 'returns true' do
- expect(issue.has_comments?).to eq true
- end
- end
-
- context 'when number of comments is equal to zero' do
- let(:raw_data) { double(base_data.merge(comments: 0)) }
-
- it 'returns false' do
- expect(issue.has_comments?).to eq false
- end
- end
- end
-
- describe '#pull_request?' do
- context 'when mention a pull request' do
- let(:raw_data) { double(base_data.merge(pull_request: double)) }
-
- it 'returns true' do
- expect(issue.pull_request?).to eq true
- end
- end
-
- context 'when does not mention a pull request' do
- let(:raw_data) { double(base_data.merge(pull_request: nil)) }
-
- it 'returns false' do
- expect(issue.pull_request?).to eq false
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/github_import/label_finder_spec.rb b/spec/lib/gitlab/github_import/label_finder_spec.rb
new file mode 100644
index 00000000000..8ba766944d6
--- /dev/null
+++ b/spec/lib/gitlab/github_import/label_finder_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::LabelFinder, :clean_gitlab_redis_cache do
+ let(:project) { create(:project) }
+ let(:finder) { described_class.new(project) }
+ let!(:bug) { create(:label, project: project, name: 'Bug') }
+ let!(:feature) { create(:label, project: project, name: 'Feature') }
+
+ describe '#id_for' do
+ context 'with a cache in place' do
+ before do
+ finder.build_cache
+ end
+
+ it 'returns the ID of the given label' do
+ expect(finder.id_for(feature.name)).to eq(feature.id)
+ end
+
+ it 'returns nil for an empty cache key' do
+ key = finder.cache_key_for(bug.name)
+
+ Gitlab::GithubImport::Caching.write(key, '')
+
+ expect(finder.id_for(bug.name)).to be_nil
+ end
+
+ it 'returns nil for a non existing label name' do
+ expect(finder.id_for('kittens')).to be_nil
+ end
+ end
+
+ context 'without a cache in place' do
+ it 'returns nil for a label' do
+ expect(finder.id_for(feature.name)).to be_nil
+ end
+ end
+ end
+
+ describe '#build_cache' do
+ it 'builds the cache of all project labels' do
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write_multiple)
+ .with(
+ {
+ "github-import/label-finder/#{project.id}/Bug" => bug.id,
+ "github-import/label-finder/#{project.id}/Feature" => feature.id
+ }
+ )
+ .and_call_original
+
+ finder.build_cache
+ end
+ end
+
+ describe '#cache_key_for' do
+ it 'returns the cache key for a label name' do
+ expect(finder.cache_key_for('foo'))
+ .to eq("github-import/label-finder/#{project.id}/foo")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/label_formatter_spec.rb b/spec/lib/gitlab/github_import/label_formatter_spec.rb
deleted file mode 100644
index 83fdd2cc415..00000000000
--- a/spec/lib/gitlab/github_import/label_formatter_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::LabelFormatter do
- let(:project) { create(:project) }
- let(:raw) { double(name: 'improvements', color: 'e6e6e6') }
-
- subject { described_class.new(project, raw) }
-
- describe '#attributes' do
- it 'returns formatted attributes' do
- expect(subject.attributes).to eq({
- project: project,
- title: 'improvements',
- color: '#e6e6e6'
- })
- end
- end
-
- describe '#create!' do
- context 'when label does not exist' do
- it 'creates a new label' do
- expect { subject.create! }.to change(Label, :count).by(1)
- end
- end
-
- context 'when label exists' do
- it 'does not create a new label' do
- Labels::CreateService.new(name: raw.name).execute(project: project)
-
- expect { subject.create! }.not_to change(Label, :count)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/github_import/markdown_text_spec.rb b/spec/lib/gitlab/github_import/markdown_text_spec.rb
new file mode 100644
index 00000000000..1ff5b9d66b3
--- /dev/null
+++ b/spec/lib/gitlab/github_import/markdown_text_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::MarkdownText do
+ describe '.format' do
+ it 'formats the text' do
+ author = double(:author, login: 'Alice')
+ text = described_class.format('Hello', author)
+
+ expect(text).to eq("*Created by: Alice*\n\nHello")
+ end
+ end
+
+ describe '#to_s' do
+ it 'returns the text when the author was found' do
+ author = double(:author, login: 'Alice')
+ text = described_class.new('Hello', author, true)
+
+ expect(text.to_s).to eq('Hello')
+ end
+
+ it 'returns the text with an extra header when the author was not found' do
+ author = double(:author, login: 'Alice')
+ text = described_class.new('Hello', author)
+
+ expect(text.to_s).to eq("*Created by: Alice*\n\nHello")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/milestone_finder_spec.rb b/spec/lib/gitlab/github_import/milestone_finder_spec.rb
new file mode 100644
index 00000000000..dff931a2fe8
--- /dev/null
+++ b/spec/lib/gitlab/github_import/milestone_finder_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::MilestoneFinder, :clean_gitlab_redis_cache do
+ let!(:project) { create(:project) }
+ let!(:milestone) { create(:milestone, project: project) }
+ let(:finder) { described_class.new(project) }
+
+ describe '#id_for' do
+ let(:issuable) { double(:issuable, milestone_number: milestone.iid) }
+
+ context 'with a cache in place' do
+ before do
+ finder.build_cache
+ end
+
+ it 'returns the milestone ID of the given issuable' do
+ expect(finder.id_for(issuable)).to eq(milestone.id)
+ end
+
+ it 'returns nil for an empty cache key' do
+ key = finder.cache_key_for(milestone.iid)
+
+ Gitlab::GithubImport::Caching.write(key, '')
+
+ expect(finder.id_for(issuable)).to be_nil
+ end
+
+ it 'returns nil for an issuable with a non-existing milestone' do
+ expect(finder.id_for(double(:issuable, milestone_number: 5))).to be_nil
+ end
+ end
+
+ context 'without a cache in place' do
+ it 'returns nil' do
+ expect(finder.id_for(issuable)).to be_nil
+ end
+ end
+ end
+
+ describe '#build_cache' do
+ it 'builds the cache of all project milestones' do
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write_multiple)
+ .with("github-import/milestone-finder/#{project.id}/1" => milestone.id)
+ .and_call_original
+
+ finder.build_cache
+ end
+ end
+
+ describe '#cache_key_for' do
+ it 'returns the cache key for an IID' do
+ expect(finder.cache_key_for(10))
+ .to eq("github-import/milestone-finder/#{project.id}/10")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
deleted file mode 100644
index 683fa51b78e..00000000000
--- a/spec/lib/gitlab/github_import/milestone_formatter_spec.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::MilestoneFormatter do
- let(:project) { create(:project) }
- let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
- let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
- let(:base_data) do
- {
- state: 'open',
- title: '1.0',
- description: 'Version 1.0',
- due_on: nil,
- created_at: created_at,
- updated_at: updated_at,
- closed_at: nil
- }
- end
- let(:iid_attr) { :number }
-
- subject(:formatter) { described_class.new(project, raw_data) }
-
- shared_examples 'Gitlab::GithubImport::MilestoneFormatter#attributes' do
- let(:data) { base_data.merge(iid_attr => 1347) }
-
- context 'when milestone is open' do
- let(:raw_data) { double(data.merge(state: 'open')) }
-
- it 'returns formatted attributes' do
- expected = {
- iid: 1347,
- project: project,
- title: '1.0',
- description: 'Version 1.0',
- state: 'active',
- due_date: nil,
- created_at: created_at,
- updated_at: updated_at
- }
-
- expect(formatter.attributes).to eq(expected)
- end
- end
-
- context 'when milestone is closed' do
- let(:raw_data) { double(data.merge(state: 'closed')) }
-
- it 'returns formatted attributes' do
- expected = {
- iid: 1347,
- project: project,
- title: '1.0',
- description: 'Version 1.0',
- state: 'closed',
- due_date: nil,
- created_at: created_at,
- updated_at: updated_at
- }
-
- expect(formatter.attributes).to eq(expected)
- end
- end
-
- context 'when milestone has a due date' do
- let(:due_date) { DateTime.strptime('2011-01-28T19:01:12Z') }
- let(:raw_data) { double(data.merge(due_on: due_date)) }
-
- it 'returns formatted attributes' do
- expected = {
- iid: 1347,
- project: project,
- title: '1.0',
- description: 'Version 1.0',
- state: 'active',
- due_date: due_date,
- created_at: created_at,
- updated_at: updated_at
- }
-
- expect(formatter.attributes).to eq(expected)
- end
- end
- end
-
- context 'when importing a GitHub project' do
- it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes'
- end
-
- context 'when importing a Gitea project' do
- let(:iid_attr) { :id }
- before do
- project.update(import_type: 'gitea')
- end
-
- it_behaves_like 'Gitlab::GithubImport::MilestoneFormatter#attributes'
- end
-end
diff --git a/spec/lib/gitlab/github_import/page_counter_spec.rb b/spec/lib/gitlab/github_import/page_counter_spec.rb
new file mode 100644
index 00000000000..c2613a9a415
--- /dev/null
+++ b/spec/lib/gitlab/github_import/page_counter_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::PageCounter, :clean_gitlab_redis_cache do
+ let(:project) { double(:project, id: 1) }
+ let(:counter) { described_class.new(project, :issues) }
+
+ describe '#initialize' do
+ it 'sets the initial page number to 1 when no value is cached' do
+ expect(counter.current).to eq(1)
+ end
+
+ it 'sets the initial page number to the cached value when one is present' do
+ Gitlab::GithubImport::Caching.write(counter.cache_key, 2)
+
+ expect(described_class.new(project, :issues).current).to eq(2)
+ end
+ end
+
+ describe '#set' do
+ it 'overwrites the page number when the given number is greater than the current number' do
+ counter.set(4)
+ expect(counter.current).to eq(4)
+ end
+
+ it 'does not overwrite the page number when the given number is lower than the current number' do
+ counter.set(2)
+ counter.set(1)
+
+ expect(counter.current).to eq(2)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/parallel_importer_spec.rb b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
new file mode 100644
index 00000000000..e2a821d4d5c
--- /dev/null
+++ b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ParallelImporter do
+ describe '.async?' do
+ it 'returns true' do
+ expect(described_class).to be_async
+ end
+ end
+
+ describe '#execute', :clean_gitlab_redis_shared_state do
+ let(:project) { create(:project) }
+ let(:importer) { described_class.new(project) }
+
+ before do
+ expect(Gitlab::GithubImport::Stage::ImportRepositoryWorker)
+ .to receive(:perform_async)
+ .with(project.id)
+ .and_return('123')
+ end
+
+ it 'schedules the importing of the repository' do
+ expect(importer.execute).to eq(true)
+ end
+
+ it 'sets the JID in Redis' do
+ expect(Gitlab::SidekiqStatus)
+ .to receive(:set)
+ .with("github-importer/#{project.id}", StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+ .and_call_original
+
+ importer.execute
+ end
+
+ it 'updates the import JID of the project' do
+ importer.execute
+
+ expect(project.import_jid).to eq("github-importer/#{project.id}")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
new file mode 100644
index 00000000000..98205d3ee25
--- /dev/null
+++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
@@ -0,0 +1,296 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ParallelScheduling do
+ let(:importer_class) do
+ Class.new do
+ include(Gitlab::GithubImport::ParallelScheduling)
+
+ def collection_method
+ :issues
+ end
+ end
+ end
+
+ let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
+ let(:client) { double(:client) }
+
+ describe '#parallel?' do
+ it 'returns true when running in parallel mode' do
+ expect(importer_class.new(project, client)).to be_parallel
+ end
+
+ it 'returns false when running in sequential mode' do
+ importer = importer_class.new(project, client, parallel: false)
+
+ expect(importer).not_to be_parallel
+ end
+ end
+
+ describe '#execute' do
+ it 'imports data in parallel when running in parallel mode' do
+ importer = importer_class.new(project, client)
+ waiter = double(:waiter)
+
+ expect(importer)
+ .to receive(:parallel_import)
+ .and_return(waiter)
+
+ expect(importer.execute)
+ .to eq(waiter)
+ end
+
+ it 'imports data in parallel when running in sequential mode' do
+ importer = importer_class.new(project, client, parallel: false)
+
+ expect(importer)
+ .to receive(:sequential_import)
+ .and_return([])
+
+ expect(importer.execute)
+ .to eq([])
+ end
+
+ it 'expires the cache used for tracking already imported objects' do
+ importer = importer_class.new(project, client)
+
+ expect(importer).to receive(:parallel_import)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:expire)
+ .with(importer.already_imported_cache_key, a_kind_of(Numeric))
+
+ importer.execute
+ end
+ end
+
+ describe '#sequential_import' do
+ let(:importer) { importer_class.new(project, client, parallel: false) }
+
+ it 'imports data in sequence' do
+ repr_class = double(:representation_class)
+ repr_instance = double(:representation_instance)
+ gh_importer = double(:github_importer)
+ gh_importer_instance = double(:github_importer_instance)
+ object = double(:object)
+
+ expect(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(object)
+
+ expect(importer)
+ .to receive(:representation_class)
+ .and_return(repr_class)
+
+ expect(repr_class)
+ .to receive(:from_api_response)
+ .with(object)
+ .and_return(repr_instance)
+
+ expect(importer)
+ .to receive(:importer_class)
+ .and_return(gh_importer)
+
+ expect(gh_importer)
+ .to receive(:new)
+ .with(repr_instance, project, client)
+ .and_return(gh_importer_instance)
+
+ expect(gh_importer_instance)
+ .to receive(:execute)
+
+ importer.sequential_import
+ end
+ end
+
+ describe '#parallel_import' do
+ let(:importer) { importer_class.new(project, client) }
+
+ it 'imports data in parallel' do
+ repr_class = double(:representation)
+ worker_class = double(:worker)
+ object = double(:object)
+
+ expect(importer)
+ .to receive(:each_object_to_import)
+ .and_yield(object)
+
+ expect(importer)
+ .to receive(:representation_class)
+ .and_return(repr_class)
+
+ expect(importer)
+ .to receive(:sidekiq_worker_class)
+ .and_return(worker_class)
+
+ expect(repr_class)
+ .to receive(:from_api_response)
+ .with(object)
+ .and_return({ title: 'Foo' })
+
+ expect(worker_class)
+ .to receive(:perform_async)
+ .with(project.id, { title: 'Foo' }, an_instance_of(String))
+
+ expect(importer.parallel_import)
+ .to be_an_instance_of(Gitlab::JobWaiter)
+ end
+ end
+
+ describe '#each_object_to_import' do
+ let(:importer) { importer_class.new(project, client) }
+ let(:object) { double(:object) }
+
+ before do
+ expect(importer)
+ .to receive(:collection_options)
+ .and_return({ state: 'all' })
+ end
+
+ it 'yields every object to import' do
+ page = double(:page, objects: [object], number: 1)
+
+ expect(client)
+ .to receive(:each_page)
+ .with(:issues, 'foo/bar', { state: 'all', page: 1 })
+ .and_yield(page)
+
+ expect(importer.page_counter)
+ .to receive(:set)
+ .with(1)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:already_imported?)
+ .with(object)
+ .and_return(false)
+
+ expect(importer)
+ .to receive(:mark_as_imported)
+ .with(object)
+
+ expect { |b| importer.each_object_to_import(&b) }
+ .to yield_with_args(object)
+ end
+
+ it 'resumes from the last page' do
+ page = double(:page, objects: [object], number: 2)
+
+ expect(importer.page_counter)
+ .to receive(:current)
+ .and_return(2)
+
+ expect(client)
+ .to receive(:each_page)
+ .with(:issues, 'foo/bar', { state: 'all', page: 2 })
+ .and_yield(page)
+
+ expect(importer.page_counter)
+ .to receive(:set)
+ .with(2)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:already_imported?)
+ .with(object)
+ .and_return(false)
+
+ expect(importer)
+ .to receive(:mark_as_imported)
+ .with(object)
+
+ expect { |b| importer.each_object_to_import(&b) }
+ .to yield_with_args(object)
+ end
+
+ it 'does not yield any objects if the page number was not set' do
+ page = double(:page, objects: [object], number: 1)
+
+ expect(client)
+ .to receive(:each_page)
+ .with(:issues, 'foo/bar', { state: 'all', page: 1 })
+ .and_yield(page)
+
+ expect(importer.page_counter)
+ .to receive(:set)
+ .with(1)
+ .and_return(false)
+
+ expect { |b| importer.each_object_to_import(&b) }
+ .not_to yield_control
+ end
+
+ it 'does not yield the object if it was already imported' do
+ page = double(:page, objects: [object], number: 1)
+
+ expect(client)
+ .to receive(:each_page)
+ .with(:issues, 'foo/bar', { state: 'all', page: 1 })
+ .and_yield(page)
+
+ expect(importer.page_counter)
+ .to receive(:set)
+ .with(1)
+ .and_return(true)
+
+ expect(importer)
+ .to receive(:already_imported?)
+ .with(object)
+ .and_return(true)
+
+ expect(importer)
+ .not_to receive(:mark_as_imported)
+
+ expect { |b| importer.each_object_to_import(&b) }
+ .not_to yield_control
+ end
+ end
+
+ describe '#already_imported?', :clean_gitlab_redis_cache do
+ let(:importer) { importer_class.new(project, client) }
+
+ it 'returns false when an object has not yet been imported' do
+ object = double(:object, id: 10)
+
+ expect(importer)
+ .to receive(:id_for_already_imported_cache)
+ .with(object)
+ .and_return(object.id)
+
+ expect(importer.already_imported?(object))
+ .to eq(false)
+ end
+
+ it 'returns true when an object has already been imported' do
+ object = double(:object, id: 10)
+
+ allow(importer)
+ .to receive(:id_for_already_imported_cache)
+ .with(object)
+ .and_return(object.id)
+
+ importer.mark_as_imported(object)
+
+ expect(importer.already_imported?(object))
+ .to eq(true)
+ end
+ end
+
+ describe '#mark_as_imported', :clean_gitlab_redis_cache do
+ it 'marks an object as already imported' do
+ object = double(:object, id: 10)
+ importer = importer_class.new(project, client)
+
+ expect(importer)
+ .to receive(:id_for_already_imported_cache)
+ .with(object)
+ .and_return(object.id)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:set_add)
+ .with(importer.already_imported_cache_key, object.id)
+ .and_call_original
+
+ importer.mark_as_imported(object)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb
deleted file mode 100644
index 948e7469a18..00000000000
--- a/spec/lib/gitlab/github_import/project_creator_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::ProjectCreator do
- let(:user) { create(:user) }
- let(:namespace) { create(:group, owner: user) }
-
- let(:repo) do
- OpenStruct.new(
- login: 'vim',
- name: 'vim',
- full_name: 'asd/vim',
- clone_url: 'https://gitlab.com/asd/vim.git'
- )
- end
-
- subject(:service) { described_class.new(repo, repo.name, namespace, user, github_access_token: 'asdffg') }
-
- before do
- namespace.add_owner(user)
- allow_any_instance_of(Project).to receive(:add_import_job)
- end
-
- describe '#execute' do
- it 'creates a project' do
- expect { service.execute }.to change(Project, :count).by(1)
- end
-
- it 'handle GitHub credentials' do
- project = service.execute
-
- expect(project.import_url).to eq('https://asdffg@gitlab.com/asd/vim.git')
- expect(project.safe_import_url).to eq('https://*****@gitlab.com/asd/vim.git')
- expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil)
- end
-
- context 'when GitHub project is private' do
- it 'sets project visibility to private' do
- repo.private = true
-
- project = service.execute
-
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
- end
-
- context 'when GitHub project is public' do
- before do
- allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL)
- end
-
- it 'sets project visibility to the default project visibility' do
- repo.private = false
-
- project = service.execute
-
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
- end
- end
-
- context 'when GitHub project has wiki' do
- it 'does not create the wiki repository' do
- allow(repo).to receive(:has_wiki?).and_return(true)
-
- project = service.execute
-
- expect(project.wiki.repository_exists?).to eq false
- end
- end
-
- context 'when GitHub project does not have wiki' do
- it 'creates the wiki repository' do
- allow(repo).to receive(:has_wiki?).and_return(false)
-
- project = service.execute
-
- expect(project.wiki.repository_exists?).to eq true
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
deleted file mode 100644
index 2e42f6239b7..00000000000
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ /dev/null
@@ -1,347 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::PullRequestFormatter do
- let(:client) { double }
- let(:project) { create(:project, :repository) }
- let(:source_sha) { create(:commit, project: project).id }
- let(:target_commit) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit) }
- let(:target_sha) { target_commit.id }
- let(:target_short_sha) { target_commit.id.to_s[0..7] }
- let(:repository) { double(id: 1, fork: false) }
- let(:source_repo) { repository }
- let(:source_branch) { double(ref: 'branch-merged', repo: source_repo, sha: source_sha) }
- let(:forked_source_repo) { double(id: 2, fork: true, name: 'otherproject', full_name: 'company/otherproject') }
- let(:target_repo) { repository }
- let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha, user: octocat) }
- let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) }
- let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) }
- let(:branch_deleted_repo) { double(ref: 'master', repo: nil, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) }
- let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
- let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
- let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
- let(:base_data) do
- {
- number: 1347,
- milestone: nil,
- state: 'open',
- title: 'New feature',
- body: 'Please pull these awesome changes',
- head: source_branch,
- base: target_branch,
- assignee: nil,
- user: octocat,
- created_at: created_at,
- updated_at: updated_at,
- closed_at: nil,
- merged_at: nil,
- url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
- }
- end
-
- subject(:pull_request) { described_class.new(project, raw_data, client) }
-
- before do
- allow(client).to receive(:user).and_return(octocat)
- end
-
- shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do
- context 'when pull request is open' do
- let(:raw_data) { double(base_data.merge(state: 'open')) }
-
- it 'returns formatted attributes' do
- expected = {
- iid: 1347,
- title: 'New feature',
- description: "*Created by: octocat*\n\nPlease pull these awesome changes",
- source_project: project,
- source_branch: 'branch-merged',
- source_branch_sha: source_sha,
- target_project: project,
- target_branch: 'master',
- target_branch_sha: target_sha,
- state: 'opened',
- milestone: nil,
- author_id: project.creator_id,
- assignee_id: nil,
- created_at: created_at,
- updated_at: updated_at,
- imported: true
- }
-
- expect(pull_request.attributes).to eq(expected)
- end
- end
-
- context 'when pull request is closed' do
- let(:raw_data) { double(base_data.merge(state: 'closed')) }
-
- it 'returns formatted attributes' do
- expected = {
- iid: 1347,
- title: 'New feature',
- description: "*Created by: octocat*\n\nPlease pull these awesome changes",
- source_project: project,
- source_branch: 'branch-merged',
- source_branch_sha: source_sha,
- target_project: project,
- target_branch: 'master',
- target_branch_sha: target_sha,
- state: 'closed',
- milestone: nil,
- author_id: project.creator_id,
- assignee_id: nil,
- created_at: created_at,
- updated_at: updated_at,
- imported: true
- }
-
- expect(pull_request.attributes).to eq(expected)
- end
- end
-
- context 'when pull request is merged' do
- let(:merged_at) { DateTime.strptime('2011-01-28T13:01:12Z') }
- let(:raw_data) { double(base_data.merge(state: 'closed', merged_at: merged_at)) }
-
- it 'returns formatted attributes' do
- expected = {
- iid: 1347,
- title: 'New feature',
- description: "*Created by: octocat*\n\nPlease pull these awesome changes",
- source_project: project,
- source_branch: 'branch-merged',
- source_branch_sha: source_sha,
- target_project: project,
- target_branch: 'master',
- target_branch_sha: target_sha,
- state: 'merged',
- milestone: nil,
- author_id: project.creator_id,
- assignee_id: nil,
- created_at: created_at,
- updated_at: updated_at,
- imported: true
- }
-
- expect(pull_request.attributes).to eq(expected)
- end
- end
-
- context 'when it is assigned to someone' do
- let(:raw_data) { double(base_data.merge(assignee: octocat)) }
-
- it 'returns nil as assignee_id when is not a GitLab user' do
- expect(pull_request.attributes.fetch(:assignee_id)).to be_nil
- end
-
- it 'returns GitLab user id associated with GitHub id as assignee_id' do
- gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
-
- expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id
- end
-
- it 'returns GitLab user id associated with GitHub email as assignee_id' do
- gl_user = create(:user, email: octocat.email)
-
- expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id
- end
- end
-
- context 'when author is a GitLab user' do
- let(:raw_data) { double(base_data.merge(user: octocat)) }
-
- it 'returns project creator_id as author_id when is not a GitLab user' do
- expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id
- end
-
- it 'returns GitLab user id associated with GitHub id as author_id' do
- gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
-
- expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
- end
-
- it 'returns GitLab user id associated with GitHub email as author_id' do
- gl_user = create(:user, email: octocat.email)
-
- expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
- end
-
- it 'returns description without created at tag line' do
- create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
-
- expect(pull_request.attributes.fetch(:description)).to eq('Please pull these awesome changes')
- end
- end
-
- context 'when it has a milestone' do
- let(:milestone) { double(id: 42, number: 42) }
- let(:raw_data) { double(base_data.merge(milestone: milestone)) }
-
- it 'returns nil when milestone does not exist' do
- expect(pull_request.attributes.fetch(:milestone)).to be_nil
- end
-
- it 'returns milestone when it exists' do
- milestone = create(:milestone, project: project, iid: 42)
-
- expect(pull_request.attributes.fetch(:milestone)).to eq milestone
- end
- end
- end
-
- shared_examples 'Gitlab::GithubImport::PullRequestFormatter#number' do
- let(:raw_data) { double(base_data) }
-
- it 'returns pull request number' do
- expect(pull_request.number).to eq 1347
- end
- end
-
- shared_examples 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name' do
- context 'when source branch exists' do
- let(:raw_data) { double(base_data) }
-
- it 'returns branch ref' do
- expect(pull_request.source_branch_name).to eq 'branch-merged'
- end
- end
-
- context 'when source branch does not exist' do
- let(:raw_data) { double(base_data.merge(head: removed_branch)) }
-
- it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do
- expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/removed-branch"
- end
- end
-
- context 'when source branch is from a fork' do
- let(:raw_data) { double(base_data.merge(head: forked_branch)) }
-
- it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do
- expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/master"
- end
- end
-
- context 'when source branch is from a deleted fork' do
- let(:raw_data) { double(base_data.merge(head: branch_deleted_repo)) }
-
- it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do
- expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/master"
- end
- end
- end
-
- shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do
- context 'when target branch exists' do
- let(:raw_data) { double(base_data) }
-
- it 'returns branch ref' do
- expect(pull_request.target_branch_name).to eq 'master'
- end
- end
-
- context 'when target branch does not exist' do
- let(:raw_data) { double(base_data.merge(base: removed_branch)) }
-
- it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do
- expect(pull_request.target_branch_name).to eq 'gl-2e5d3239/1347/octocat/removed-branch'
- end
- end
- end
-
- context 'when importing a GitHub project' do
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes'
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number'
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name'
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name'
- end
-
- context 'when importing a Gitea project' do
- before do
- project.update(import_type: 'gitea')
- end
-
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#attributes'
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#number'
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#source_branch_name'
- it_behaves_like 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name'
- end
-
- describe '#valid?' do
- context 'when source, and target repos are not a fork' do
- let(:raw_data) { double(base_data) }
-
- it 'returns true' do
- expect(pull_request.valid?).to eq true
- end
- end
-
- context 'when source repo is a fork' do
- let(:source_repo) { double(id: 2) }
- let(:raw_data) { double(base_data) }
-
- it 'returns true' do
- expect(pull_request.valid?).to eq true
- end
- end
-
- context 'when target repo is a fork' do
- let(:target_repo) { double(id: 2) }
- let(:raw_data) { double(base_data) }
-
- it 'returns true' do
- expect(pull_request.valid?).to eq true
- end
- end
- end
-
- describe '#cross_project?' do
- context 'when source and target repositories are different' do
- let(:raw_data) { double(base_data.merge(head: forked_branch)) }
-
- it 'returns true' do
- expect(pull_request.cross_project?).to eq true
- end
- end
-
- context 'when source repository does not exist anymore' do
- let(:raw_data) { double(base_data.merge(head: branch_deleted_repo)) }
-
- it 'returns true' do
- expect(pull_request.cross_project?).to eq true
- end
- end
-
- context 'when source and target repositories are the same' do
- let(:raw_data) { double(base_data.merge(head: source_branch)) }
-
- it 'returns false' do
- expect(pull_request.cross_project?).to eq false
- end
- end
- end
-
- describe '#source_branch_exists?' do
- let(:raw_data) { double(base_data.merge(head: forked_branch)) }
-
- it 'returns false when is a cross_project' do
- expect(pull_request.source_branch_exists?).to eq false
- end
- end
-
- describe '#url' do
- let(:raw_data) { double(base_data) }
-
- it 'return raw url' do
- expect(pull_request.url).to eq 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
- end
- end
-
- describe '#opened?' do
- let(:raw_data) { double(base_data.merge(state: 'open')) }
-
- it 'returns true when state is "open"' do
- expect(pull_request.opened?).to be_truthy
- end
- end
-end
diff --git a/spec/lib/gitlab/github_import/release_formatter_spec.rb b/spec/lib/gitlab/github_import/release_formatter_spec.rb
deleted file mode 100644
index 926bf725d6a..00000000000
--- a/spec/lib/gitlab/github_import/release_formatter_spec.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::ReleaseFormatter do
- let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
- let(:octocat) { double(id: 123456, login: 'octocat') }
- let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
-
- let(:base_data) do
- {
- tag_name: 'v1.0.0',
- name: 'First release',
- draft: false,
- created_at: created_at,
- published_at: created_at,
- body: 'Release v1.0.0'
- }
- end
-
- subject(:release) { described_class.new(project, raw_data) }
-
- describe '#attributes' do
- let(:raw_data) { double(base_data) }
-
- it 'returns formatted attributes' do
- expected = {
- project: project,
- tag: 'v1.0.0',
- description: 'Release v1.0.0',
- created_at: created_at,
- updated_at: created_at
- }
-
- expect(release.attributes).to eq(expected)
- end
- end
-
- describe '#valid' do
- context 'when release is not a draft' do
- let(:raw_data) { double(base_data) }
-
- it 'returns true' do
- expect(release.valid?).to eq true
- end
- end
-
- context 'when release is draft' do
- let(:raw_data) { double(base_data.merge(draft: true)) }
-
- it 'returns false' do
- expect(release.valid?).to eq false
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
new file mode 100644
index 00000000000..7b0a1ea4948
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
@@ -0,0 +1,164 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::DiffNote do
+ let(:hunk) do
+ '@@ -1 +1 @@
+ -Hello
+ +Hello world'
+ end
+
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ shared_examples 'a DiffNote' do
+ it 'returns an instance of DiffNote' do
+ expect(note).to be_an_instance_of(described_class)
+ end
+
+ context 'the returned DiffNote' do
+ it 'includes the number of the note' do
+ expect(note.noteable_id).to eq(42)
+ end
+
+ it 'includes the file path of the diff' do
+ expect(note.file_path).to eq('README.md')
+ end
+
+ it 'includes the commit ID' do
+ expect(note.commit_id).to eq('123abc')
+ end
+
+ it 'includes the user details' do
+ expect(note.author)
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+
+ expect(note.author.id).to eq(4)
+ expect(note.author.login).to eq('alice')
+ end
+
+ it 'includes the note body' do
+ expect(note.note).to eq('Hello world')
+ end
+
+ it 'includes the created timestamp' do
+ expect(note.created_at).to eq(created_at)
+ end
+
+ it 'includes the updated timestamp' do
+ expect(note.updated_at).to eq(updated_at)
+ end
+
+ it 'includes the GitHub ID' do
+ expect(note.github_id).to eq(1)
+ end
+
+ it 'returns the noteable type' do
+ expect(note.noteable_type).to eq('MergeRequest')
+ end
+ end
+ end
+
+ describe '.from_api_response' do
+ let(:response) do
+ double(
+ :response,
+ html_url: 'https://github.com/foo/bar/pull/42',
+ path: 'README.md',
+ commit_id: '123abc',
+ diff_hunk: hunk,
+ user: double(:user, id: 4, login: 'alice'),
+ body: 'Hello world',
+ created_at: created_at,
+ updated_at: updated_at,
+ id: 1
+ )
+ end
+
+ it_behaves_like 'a DiffNote' do
+ let(:note) { described_class.from_api_response(response) }
+ end
+
+ it 'does not set the user if the response did not include a user' do
+ allow(response)
+ .to receive(:user)
+ .and_return(nil)
+
+ note = described_class.from_api_response(response)
+
+ expect(note.author).to be_nil
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'a DiffNote' do
+ let(:hash) do
+ {
+ 'noteable_type' => 'MergeRequest',
+ 'noteable_id' => 42,
+ 'file_path' => 'README.md',
+ 'commit_id' => '123abc',
+ 'diff_hunk' => hunk,
+ 'author' => { 'id' => 4, 'login' => 'alice' },
+ 'note' => 'Hello world',
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'github_id' => 1
+ }
+ end
+
+ let(:note) { described_class.from_json_hash(hash) }
+ end
+
+ it 'does not convert the author if it was not specified' do
+ hash = {
+ 'noteable_type' => 'MergeRequest',
+ 'noteable_id' => 42,
+ 'file_path' => 'README.md',
+ 'commit_id' => '123abc',
+ 'diff_hunk' => hunk,
+ 'note' => 'Hello world',
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'github_id' => 1
+ }
+
+ note = described_class.from_json_hash(hash)
+
+ expect(note.author).to be_nil
+ end
+ end
+
+ describe '#line_code' do
+ it 'returns a String' do
+ note = described_class.new(diff_hunk: hunk, file_path: 'README.md')
+
+ expect(note.line_code).to be_an_instance_of(String)
+ end
+ end
+
+ describe '#diff_hash' do
+ it 'returns a Hash containing the diff details' do
+ note = described_class.from_json_hash(
+ 'noteable_type' => 'MergeRequest',
+ 'noteable_id' => 42,
+ 'file_path' => 'README.md',
+ 'commit_id' => '123abc',
+ 'diff_hunk' => hunk,
+ 'author' => { 'id' => 4, 'login' => 'alice' },
+ 'note' => 'Hello world',
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'github_id' => 1
+ )
+
+ expect(note.diff_hash).to eq(
+ diff: hunk,
+ new_path: 'README.md',
+ old_path: 'README.md',
+ a_mode: '100644',
+ b_mode: '100644',
+ new_file: false
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb b/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb
new file mode 100644
index 00000000000..15de0fe49ff
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::ExposeAttribute do
+ it 'defines a getter method that returns an attribute value' do
+ klass = Class.new do
+ include Gitlab::GithubImport::Representation::ExposeAttribute
+
+ expose_attribute :number
+
+ attr_reader :attributes
+
+ def initialize
+ @attributes = { number: 42 }
+ end
+ end
+
+ expect(klass.new.number).to eq(42)
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/issue_spec.rb b/spec/lib/gitlab/github_import/representation/issue_spec.rb
new file mode 100644
index 00000000000..99330ce42cb
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/issue_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::Issue do
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ shared_examples 'an Issue' do
+ it 'returns an instance of Issue' do
+ expect(issue).to be_an_instance_of(described_class)
+ end
+
+ context 'the returned Issue' do
+ it 'includes the issue number' do
+ expect(issue.iid).to eq(42)
+ end
+
+ it 'includes the issue title' do
+ expect(issue.title).to eq('My Issue')
+ end
+
+ it 'includes the issue description' do
+ expect(issue.description).to eq('This is my issue')
+ end
+
+ it 'includes the milestone number' do
+ expect(issue.milestone_number).to eq(4)
+ end
+
+ it 'includes the issue state' do
+ expect(issue.state).to eq(:opened)
+ end
+
+ it 'includes the issue assignees' do
+ expect(issue.assignees[0])
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+
+ expect(issue.assignees[0].id).to eq(4)
+ expect(issue.assignees[0].login).to eq('alice')
+ end
+
+ it 'includes the label names' do
+ expect(issue.label_names).to eq(%w[bug])
+ end
+
+ it 'includes the author details' do
+ expect(issue.author)
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+
+ expect(issue.author.id).to eq(4)
+ expect(issue.author.login).to eq('alice')
+ end
+
+ it 'includes the created timestamp' do
+ expect(issue.created_at).to eq(created_at)
+ end
+
+ it 'includes the updated timestamp' do
+ expect(issue.updated_at).to eq(updated_at)
+ end
+
+ it 'is not a pull request' do
+ expect(issue.pull_request?).to eq(false)
+ end
+ end
+ end
+
+ describe '.from_api_response' do
+ let(:response) do
+ double(
+ :response,
+ number: 42,
+ title: 'My Issue',
+ body: 'This is my issue',
+ milestone: double(:milestone, number: 4),
+ state: 'open',
+ assignees: [double(:user, id: 4, login: 'alice')],
+ labels: [double(:label, name: 'bug')],
+ user: double(:user, id: 4, login: 'alice'),
+ created_at: created_at,
+ updated_at: updated_at,
+ pull_request: false
+ )
+ end
+
+ it_behaves_like 'an Issue' do
+ let(:issue) { described_class.from_api_response(response) }
+ end
+
+ it 'does not set the user if the response did not include a user' do
+ allow(response)
+ .to receive(:user)
+ .and_return(nil)
+
+ issue = described_class.from_api_response(response)
+
+ expect(issue.author).to be_nil
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'an Issue' do
+ let(:hash) do
+ {
+ 'iid' => 42,
+ 'title' => 'My Issue',
+ 'description' => 'This is my issue',
+ 'milestone_number' => 4,
+ 'state' => 'opened',
+ 'assignees' => [{ 'id' => 4, 'login' => 'alice' }],
+ 'label_names' => %w[bug],
+ 'author' => { 'id' => 4, 'login' => 'alice' },
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'pull_request' => false
+ }
+ end
+
+ let(:issue) { described_class.from_json_hash(hash) }
+ end
+
+ it 'does not convert the author if it was not specified' do
+ hash = {
+ 'iid' => 42,
+ 'title' => 'My Issue',
+ 'description' => 'This is my issue',
+ 'milestone_number' => 4,
+ 'state' => 'opened',
+ 'assignees' => [{ 'id' => 4, 'login' => 'alice' }],
+ 'label_names' => %w[bug],
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'pull_request' => false
+ }
+
+ issue = described_class.from_json_hash(hash)
+
+ expect(issue.author).to be_nil
+ end
+ end
+
+ describe '#labels?' do
+ it 'returns true when the issue has labels assigned' do
+ issue = described_class.new(label_names: %w[bug])
+
+ expect(issue.labels?).to eq(true)
+ end
+
+ it 'returns false when the issue has no labels assigned' do
+ issue = described_class.new(label_names: [])
+
+ expect(issue.labels?).to eq(false)
+ end
+ end
+
+ describe '#pull_request?' do
+ it 'returns false for an issue' do
+ issue = described_class.new(pull_request: false)
+
+ expect(issue.pull_request?).to eq(false)
+ end
+
+ it 'returns true for a pull request' do
+ issue = described_class.new(pull_request: true)
+
+ expect(issue.pull_request?).to eq(true)
+ end
+ end
+
+ describe '#truncated_title' do
+ it 'truncates the title to 255 characters' do
+ object = described_class.new(title: 'm' * 300)
+
+ expect(object.truncated_title.length).to eq(255)
+ end
+
+ it 'does not truncate the title if it is shorter than 255 characters' do
+ object = described_class.new(title: 'foo')
+
+ expect(object.truncated_title).to eq('foo')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/note_spec.rb b/spec/lib/gitlab/github_import/representation/note_spec.rb
new file mode 100644
index 00000000000..f2c1c66b357
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/note_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::Note do
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+
+ shared_examples 'a Note' do
+ it 'returns an instance of Note' do
+ expect(note).to be_an_instance_of(described_class)
+ end
+
+ context 'the returned Note' do
+ it 'includes the noteable ID' do
+ expect(note.noteable_id).to eq(42)
+ end
+
+ it 'includes the noteable type' do
+ expect(note.noteable_type).to eq('Issue')
+ end
+
+ it 'includes the author details' do
+ expect(note.author)
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+
+ expect(note.author.id).to eq(4)
+ expect(note.author.login).to eq('alice')
+ end
+
+ it 'includes the note body' do
+ expect(note.note).to eq('Hello world')
+ end
+
+ it 'includes the created timestamp' do
+ expect(note.created_at).to eq(created_at)
+ end
+
+ it 'includes the updated timestamp' do
+ expect(note.updated_at).to eq(updated_at)
+ end
+
+ it 'includes the GitHub ID' do
+ expect(note.github_id).to eq(1)
+ end
+ end
+ end
+
+ describe '.from_api_response' do
+ let(:response) do
+ double(
+ :response,
+ html_url: 'https://github.com/foo/bar/issues/42',
+ user: double(:user, id: 4, login: 'alice'),
+ body: 'Hello world',
+ created_at: created_at,
+ updated_at: updated_at,
+ id: 1
+ )
+ end
+
+ it_behaves_like 'a Note' do
+ let(:note) { described_class.from_api_response(response) }
+ end
+
+ it 'does not set the user if the response did not include a user' do
+ allow(response)
+ .to receive(:user)
+ .and_return(nil)
+
+ note = described_class.from_api_response(response)
+
+ expect(note.author).to be_nil
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'a Note' do
+ let(:hash) do
+ {
+ 'noteable_id' => 42,
+ 'noteable_type' => 'Issue',
+ 'author' => { 'id' => 4, 'login' => 'alice' },
+ 'note' => 'Hello world',
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'github_id' => 1
+ }
+ end
+
+ let(:note) { described_class.from_json_hash(hash) }
+ end
+
+ it 'does not convert the author if it was not specified' do
+ hash = {
+ 'noteable_id' => 42,
+ 'noteable_type' => 'Issue',
+ 'note' => 'Hello world',
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'github_id' => 1
+ }
+
+ note = described_class.from_json_hash(hash)
+
+ expect(note.author).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
new file mode 100644
index 00000000000..33f6ff0ae6a
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
@@ -0,0 +1,288 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::PullRequest do
+ let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
+ let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
+ let(:merged_at) { Time.new(2017, 1, 1, 12, 17) }
+
+ shared_examples 'a PullRequest' do
+ it 'returns an instance of PullRequest' do
+ expect(pr).to be_an_instance_of(described_class)
+ end
+
+ context 'the returned PullRequest' do
+ it 'includes the pull request number' do
+ expect(pr.iid).to eq(42)
+ end
+
+ it 'includes the pull request title' do
+ expect(pr.title).to eq('My Pull Request')
+ end
+
+ it 'includes the pull request description' do
+ expect(pr.description).to eq('This is my pull request')
+ end
+
+ it 'includes the source branch name' do
+ expect(pr.source_branch).to eq('my-feature')
+ end
+
+ it 'includes the source branch SHA' do
+ expect(pr.source_branch_sha).to eq('123abc')
+ end
+
+ it 'includes the target branch name' do
+ expect(pr.target_branch).to eq('master')
+ end
+
+ it 'includes the target branch SHA' do
+ expect(pr.target_branch_sha).to eq('456def')
+ end
+
+ it 'includes the milestone number' do
+ expect(pr.milestone_number).to eq(4)
+ end
+
+ it 'includes the user details' do
+ expect(pr.author)
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+
+ expect(pr.author.id).to eq(4)
+ expect(pr.author.login).to eq('alice')
+ end
+
+ it 'includes the assignee details' do
+ expect(pr.assignee)
+ .to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+
+ expect(pr.assignee.id).to eq(4)
+ expect(pr.assignee.login).to eq('alice')
+ end
+
+ it 'includes the created timestamp' do
+ expect(pr.created_at).to eq(created_at)
+ end
+
+ it 'includes the updated timestamp' do
+ expect(pr.updated_at).to eq(updated_at)
+ end
+
+ it 'includes the merged timestamp' do
+ expect(pr.merged_at).to eq(merged_at)
+ end
+
+ it 'includes the source repository ID' do
+ expect(pr.source_repository_id).to eq(400)
+ end
+
+ it 'includes the target repository ID' do
+ expect(pr.target_repository_id).to eq(200)
+ end
+
+ it 'includes the source repository owner name' do
+ expect(pr.source_repository_owner).to eq('alice')
+ end
+
+ it 'includes the pull request state' do
+ expect(pr.state).to eq(:merged)
+ end
+ end
+ end
+
+ describe '.from_api_response' do
+ let(:response) do
+ double(
+ :response,
+ number: 42,
+ title: 'My Pull Request',
+ body: 'This is my pull request',
+ state: 'closed',
+ head: double(
+ :head,
+ sha: '123abc',
+ ref: 'my-feature',
+ repo: double(:repo, id: 400),
+ user: double(:user, id: 4, login: 'alice')
+ ),
+ base: double(
+ :base,
+ sha: '456def',
+ ref: 'master',
+ repo: double(:repo, id: 200)
+ ),
+ milestone: double(:milestone, number: 4),
+ user: double(:user, id: 4, login: 'alice'),
+ assignee: double(:user, id: 4, login: 'alice'),
+ created_at: created_at,
+ updated_at: updated_at,
+ merged_at: merged_at
+ )
+ end
+
+ it_behaves_like 'a PullRequest' do
+ let(:pr) { described_class.from_api_response(response) }
+ end
+
+ it 'does not set the user if the response did not include a user' do
+ allow(response)
+ .to receive(:user)
+ .and_return(nil)
+
+ pr = described_class.from_api_response(response)
+
+ expect(pr.author).to be_nil
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'a PullRequest' do
+ let(:hash) do
+ {
+ 'iid' => 42,
+ 'title' => 'My Pull Request',
+ 'description' => 'This is my pull request',
+ 'source_branch' => 'my-feature',
+ 'source_branch_sha' => '123abc',
+ 'target_branch' => 'master',
+ 'target_branch_sha' => '456def',
+ 'source_repository_id' => 400,
+ 'target_repository_id' => 200,
+ 'source_repository_owner' => 'alice',
+ 'state' => 'closed',
+ 'milestone_number' => 4,
+ 'author' => { 'id' => 4, 'login' => 'alice' },
+ 'assignee' => { 'id' => 4, 'login' => 'alice' },
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'merged_at' => merged_at.to_s
+ }
+ end
+
+ let(:pr) { described_class.from_json_hash(hash) }
+ end
+
+ it 'does not convert the author if it was not specified' do
+ hash = {
+ 'iid' => 42,
+ 'title' => 'My Pull Request',
+ 'description' => 'This is my pull request',
+ 'source_branch' => 'my-feature',
+ 'source_branch_sha' => '123abc',
+ 'target_branch' => 'master',
+ 'target_branch_sha' => '456def',
+ 'source_repository_id' => 400,
+ 'target_repository_id' => 200,
+ 'source_repository_owner' => 'alice',
+ 'state' => 'closed',
+ 'milestone_number' => 4,
+ 'assignee' => { 'id' => 4, 'login' => 'alice' },
+ 'created_at' => created_at.to_s,
+ 'updated_at' => updated_at.to_s,
+ 'merged_at' => merged_at.to_s
+ }
+
+ pr = described_class.from_json_hash(hash)
+
+ expect(pr.author).to be_nil
+ end
+ end
+
+ describe '#state' do
+ it 'returns :opened for an open pull request' do
+ pr = described_class.new(state: :opened)
+
+ expect(pr.state).to eq(:opened)
+ end
+
+ it 'returns :closed for a closed pull request' do
+ pr = described_class.new(state: :closed)
+
+ expect(pr.state).to eq(:closed)
+ end
+
+ it 'returns :merged for a merged pull request' do
+ pr = described_class.new(state: :closed, merged_at: merged_at)
+
+ expect(pr.state).to eq(:merged)
+ end
+ end
+
+ describe '#cross_project?' do
+ it 'returns false for a pull request submitted from the target project' do
+ pr = described_class.new(source_repository_id: 1, target_repository_id: 1)
+
+ expect(pr).not_to be_cross_project
+ end
+
+ it 'returns true for a pull request submitted from a different project' do
+ pr = described_class.new(source_repository_id: 1, target_repository_id: 2)
+
+ expect(pr).to be_cross_project
+ end
+
+ it 'returns true if no source repository is present' do
+ pr = described_class.new(target_repository_id: 2)
+
+ expect(pr).to be_cross_project
+ end
+ end
+
+ describe '#formatted_source_branch' do
+ context 'for a cross-project pull request' do
+ it 'includes the owner name in the branch name' do
+ pr = described_class.new(
+ source_repository_owner: 'foo',
+ source_branch: 'branch',
+ target_branch: 'master',
+ source_repository_id: 1,
+ target_repository_id: 2
+ )
+
+ expect(pr.formatted_source_branch).to eq('foo:branch')
+ end
+ end
+
+ context 'for a regular pull request' do
+ it 'returns the source branch name' do
+ pr = described_class.new(
+ source_repository_owner: 'foo',
+ source_branch: 'branch',
+ target_branch: 'master',
+ source_repository_id: 1,
+ target_repository_id: 1
+ )
+
+ expect(pr.formatted_source_branch).to eq('branch')
+ end
+ end
+
+ context 'for a pull request with the same source and target branches' do
+ it 'returns a generated source branch name' do
+ pr = described_class.new(
+ iid: 1,
+ source_repository_owner: 'foo',
+ source_branch: 'branch',
+ target_branch: 'branch',
+ source_repository_id: 1,
+ target_repository_id: 1
+ )
+
+ expect(pr.formatted_source_branch).to eq('branch-1')
+ end
+ end
+ end
+
+ describe '#truncated_title' do
+ it 'truncates the title to 255 characters' do
+ object = described_class.new(title: 'm' * 300)
+
+ expect(object.truncated_title.length).to eq(255)
+ end
+
+ it 'does not truncate the title if it is shorter than 255 characters' do
+ object = described_class.new(title: 'foo')
+
+ expect(object.truncated_title).to eq('foo')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/to_hash_spec.rb b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
new file mode 100644
index 00000000000..c296aa0a45b
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::ToHash do
+ describe '#to_hash' do
+ let(:user) { double(:user, attributes: { login: 'alice' }) }
+
+ let(:issue) do
+ double(
+ :issue,
+ attributes: { user: user, assignees: [user], number: 42 }
+ )
+ end
+
+ let(:issue_hash) { issue.to_hash }
+
+ before do
+ user.extend(described_class)
+ issue.extend(described_class)
+ end
+
+ it 'converts an object to a Hash' do
+ expect(issue_hash).to be_an_instance_of(Hash)
+ end
+
+ it 'converts nested objects to Hashes' do
+ expect(issue_hash[:user]).to eq({ login: 'alice' })
+ end
+
+ it 'converts Array values to Hashes' do
+ expect(issue_hash[:assignees]).to eq([{ login: 'alice' }])
+ end
+
+ it 'keeps values as-is if they do not respond to #to_hash' do
+ expect(issue_hash[:number]).to eq(42)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/user_spec.rb b/spec/lib/gitlab/github_import/representation/user_spec.rb
new file mode 100644
index 00000000000..4e63e8ea568
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/user_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation::User do
+ shared_examples 'a User' do
+ it 'returns an instance of User' do
+ expect(user).to be_an_instance_of(described_class)
+ end
+
+ context 'the returned User' do
+ it 'includes the user ID' do
+ expect(user.id).to eq(42)
+ end
+
+ it 'includes the username' do
+ expect(user.login).to eq('alice')
+ end
+ end
+ end
+
+ describe '.from_api_response' do
+ it_behaves_like 'a User' do
+ let(:response) { double(:response, id: 42, login: 'alice') }
+ let(:user) { described_class.from_api_response(response) }
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'a User' do
+ let(:hash) { { 'id' => 42, 'login' => 'alice' } }
+ let(:user) { described_class.from_json_hash(hash) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation_spec.rb b/spec/lib/gitlab/github_import/representation_spec.rb
new file mode 100644
index 00000000000..0b0610817b0
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Representation do
+ describe '.symbolize_hash' do
+ it 'returns a Hash with the keys as Symbols' do
+ hash = described_class.symbolize_hash('number' => 10)
+
+ expect(hash).to eq({ number: 10 })
+ end
+
+ it 'parses timestamp fields into Time instances' do
+ hash = described_class.symbolize_hash('created_at' => '2017-01-01 12:00')
+
+ expect(hash[:created_at]).to be_an_instance_of(Time)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/sequential_importer_spec.rb b/spec/lib/gitlab/github_import/sequential_importer_spec.rb
new file mode 100644
index 00000000000..6089b0b751f
--- /dev/null
+++ b/spec/lib/gitlab/github_import/sequential_importer_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::SequentialImporter do
+ describe '#execute' do
+ it 'imports a project in sequence' do
+ repository = double(:repository)
+ project = double(:project, id: 1, repository: repository)
+ importer = described_class.new(project, token: 'foo')
+
+ expect_any_instance_of(Gitlab::GithubImport::Importer::RepositoryImporter)
+ .to receive(:execute)
+
+ described_class::SEQUENTIAL_IMPORTERS.each do |klass|
+ instance = double(:instance)
+
+ expect(klass).to receive(:new)
+ .with(project, importer.client)
+ .and_return(instance)
+
+ expect(instance).to receive(:execute)
+ end
+
+ described_class::PARALLEL_IMPORTERS.each do |klass|
+ instance = double(:instance)
+
+ expect(klass).to receive(:new)
+ .with(project, importer.client, parallel: false)
+ .and_return(instance)
+
+ expect(instance).to receive(:execute)
+ end
+
+ expect(repository).to receive(:after_import)
+ expect(importer.execute).to eq(true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/user_finder_spec.rb b/spec/lib/gitlab/github_import/user_finder_spec.rb
new file mode 100644
index 00000000000..29f4c00d9c7
--- /dev/null
+++ b/spec/lib/gitlab/github_import/user_finder_spec.rb
@@ -0,0 +1,333 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache do
+ let(:project) { create(:project) }
+ let(:client) { double(:client) }
+ let(:finder) { described_class.new(project, client) }
+
+ describe '#author_id_for' do
+ it 'returns the user ID for the author of an object' do
+ user = double(:user, id: 4, login: 'kittens')
+ note = double(:note, author: user)
+
+ expect(finder).to receive(:user_id_for).with(user).and_return(42)
+
+ expect(finder.author_id_for(note)).to eq([42, true])
+ end
+
+ it 'returns the ID of the project creator if no user ID could be found' do
+ user = double(:user, id: 4, login: 'kittens')
+ note = double(:note, author: user)
+
+ expect(finder).to receive(:user_id_for).with(user).and_return(nil)
+
+ expect(finder.author_id_for(note)).to eq([project.creator_id, false])
+ end
+
+ it 'returns the ID of the ghost user when the object has no user' do
+ note = double(:note, author: nil)
+
+ expect(finder.author_id_for(note)).to eq([User.ghost.id, true])
+ end
+
+ it 'returns the ID of the ghost user when the given object is nil' do
+ expect(finder.author_id_for(nil)).to eq([User.ghost.id, true])
+ end
+ end
+
+ describe '#assignee_id_for' do
+ it 'returns the user ID for the assignee of an issuable' do
+ user = double(:user, id: 4, login: 'kittens')
+ issue = double(:issue, assignee: user)
+
+ expect(finder).to receive(:user_id_for).with(user).and_return(42)
+ expect(finder.assignee_id_for(issue)).to eq(42)
+ end
+
+ it 'returns nil if the issuable does not have an assignee' do
+ issue = double(:issue, assignee: nil)
+
+ expect(finder).not_to receive(:user_id_for)
+ expect(finder.assignee_id_for(issue)).to be_nil
+ end
+ end
+
+ describe '#user_id_for' do
+ it 'returns the user ID for the given user' do
+ user = double(:user, id: 4, login: 'kittens')
+
+ expect(finder).to receive(:find).with(user.id, user.login).and_return(42)
+ expect(finder.user_id_for(user)).to eq(42)
+ end
+ end
+
+ describe '#find' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(finder).to receive(:email_for_github_username)
+ .and_return(user.email)
+ end
+
+ context 'without a cache' do
+ before do
+ allow(finder).to receive(:find_from_cache).and_return([false, nil])
+ expect(finder).to receive(:find_id_from_database).and_call_original
+ end
+
+ it 'finds a GitLab user for a GitHub user ID' do
+ user.identities.create!(provider: :github, extern_uid: 42)
+
+ expect(finder.find(42, user.username)).to eq(user.id)
+ end
+
+ it 'finds a GitLab user for a GitHub Email address' do
+ expect(finder.find(42, user.username)).to eq(user.id)
+ end
+ end
+
+ context 'with a cache' do
+ it 'returns the cached user ID' do
+ expect(finder).to receive(:find_from_cache).and_return([true, user.id])
+ expect(finder).not_to receive(:find_id_from_database)
+
+ expect(finder.find(42, user.username)).to eq(user.id)
+ end
+
+ it 'does not query the database if the cache key exists but is empty' do
+ expect(finder).to receive(:find_from_cache).and_return([true, nil])
+ expect(finder).not_to receive(:find_id_from_database)
+
+ expect(finder.find(42, user.username)).to be_nil
+ end
+ end
+ end
+
+ describe '#find_from_cache' do
+ it 'retrieves a GitLab user ID for a GitHub user ID' do
+ expect(finder)
+ .to receive(:cached_id_for_github_id)
+ .with(42)
+ .and_return([true, 4])
+
+ expect(finder.find_from_cache(42)).to eq([true, 4])
+ end
+
+ it 'retrieves a GitLab user ID for a GitHub Email address' do
+ email = 'kittens@example.com'
+
+ expect(finder)
+ .to receive(:cached_id_for_github_id)
+ .with(42)
+ .and_return([false, nil])
+
+ expect(finder)
+ .to receive(:cached_id_for_github_email)
+ .with(email)
+ .and_return([true, 4])
+
+ expect(finder.find_from_cache(42, email)).to eq([true, 4])
+ end
+
+ it 'does not query the cache for an Email address when none is given' do
+ expect(finder)
+ .to receive(:cached_id_for_github_id)
+ .with(42)
+ .and_return([false, nil])
+
+ expect(finder).not_to receive(:cached_id_for_github_id)
+
+ expect(finder.find_from_cache(42)).to eq([false])
+ end
+ end
+
+ describe '#find_id_from_database' do
+ let(:user) { create(:user) }
+
+ it 'returns the GitLab user ID for a GitHub user ID' do
+ user.identities.create!(provider: :github, extern_uid: 42)
+
+ expect(finder.find_id_from_database(42, user.email)).to eq(user.id)
+ end
+
+ it 'returns the GitLab user ID for a GitHub Email address' do
+ expect(finder.find_id_from_database(42, user.email)).to eq(user.id)
+ end
+ end
+
+ describe '#email_for_github_username' do
+ let(:email) { 'kittens@example.com' }
+
+ context 'when an Email address is cached' do
+ it 'reads the Email address from the cache' do
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:read)
+ .and_return(email)
+
+ expect(client).not_to receive(:user)
+ expect(finder.email_for_github_username('kittens')).to eq(email)
+ end
+ end
+
+ context 'when an Email address is not cached' do
+ let(:user) { double(:user, email: email) }
+
+ it 'retrieves the Email address from the GitHub API' do
+ expect(client).to receive(:user).with('kittens').and_return(user)
+ expect(finder.email_for_github_username('kittens')).to eq(email)
+ end
+
+ it 'caches the Email address when an Email address is available' do
+ expect(client).to receive(:user).with('kittens').and_return(user)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .with(an_instance_of(String), email)
+
+ finder.email_for_github_username('kittens')
+ end
+
+ it 'returns nil if the user does not exist' do
+ expect(client)
+ .to receive(:user)
+ .with('kittens')
+ .and_return(nil)
+
+ expect(Gitlab::GithubImport::Caching)
+ .not_to receive(:write)
+
+ expect(finder.email_for_github_username('kittens')).to be_nil
+ end
+ end
+ end
+
+ describe '#cached_id_for_github_id' do
+ let(:id) { 4 }
+
+ it 'reads a user ID from the cache' do
+ Gitlab::GithubImport::Caching
+ .write(described_class::ID_CACHE_KEY % id, 4)
+
+ expect(finder.cached_id_for_github_id(id)).to eq([true, 4])
+ end
+
+ it 'reads a non existing cache key' do
+ expect(finder.cached_id_for_github_id(id)).to eq([false, nil])
+ end
+ end
+
+ describe '#cached_id_for_github_email' do
+ let(:email) { 'kittens@example.com' }
+
+ it 'reads a user ID from the cache' do
+ Gitlab::GithubImport::Caching
+ .write(described_class::ID_FOR_EMAIL_CACHE_KEY % email, 4)
+
+ expect(finder.cached_id_for_github_email(email)).to eq([true, 4])
+ end
+
+ it 'reads a non existing cache key' do
+ expect(finder.cached_id_for_github_email(email)).to eq([false, nil])
+ end
+ end
+
+ describe '#id_for_github_id' do
+ let(:id) { 4 }
+
+ it 'queries and caches the user ID for a given GitHub ID' do
+ expect(finder).to receive(:query_id_for_github_id)
+ .with(id)
+ .and_return(42)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .with(described_class::ID_CACHE_KEY % id, 42)
+
+ finder.id_for_github_id(id)
+ end
+
+ it 'caches a nil value if no ID could be found' do
+ expect(finder).to receive(:query_id_for_github_id)
+ .with(id)
+ .and_return(nil)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .with(described_class::ID_CACHE_KEY % id, nil)
+
+ finder.id_for_github_id(id)
+ end
+ end
+
+ describe '#id_for_github_email' do
+ let(:email) { 'kittens@example.com' }
+
+ it 'queries and caches the user ID for a given Email address' do
+ expect(finder).to receive(:query_id_for_github_email)
+ .with(email)
+ .and_return(42)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .with(described_class::ID_FOR_EMAIL_CACHE_KEY % email, 42)
+
+ finder.id_for_github_email(email)
+ end
+
+ it 'caches a nil value if no ID could be found' do
+ expect(finder).to receive(:query_id_for_github_email)
+ .with(email)
+ .and_return(nil)
+
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .with(described_class::ID_FOR_EMAIL_CACHE_KEY % email, nil)
+
+ finder.id_for_github_email(email)
+ end
+ end
+
+ describe '#query_id_for_github_id' do
+ it 'returns the ID of the user for the given GitHub user ID' do
+ user = create(:user)
+
+ user.identities.create!(provider: :github, extern_uid: '42')
+
+ expect(finder.query_id_for_github_id(42)).to eq(user.id)
+ end
+
+ it 'returns nil when no user ID could be found' do
+ expect(finder.query_id_for_github_id(42)).to be_nil
+ end
+ end
+
+ describe '#query_id_for_github_email' do
+ it 'returns the ID of the user for the given Email address' do
+ user = create(:user, email: 'kittens@example.com')
+
+ expect(finder.query_id_for_github_email(user.email)).to eq(user.id)
+ end
+
+ it 'returns nil if no user ID could be found' do
+ expect(finder.query_id_for_github_email('kittens@example.com')).to be_nil
+ end
+ end
+
+ describe '#read_id_from_cache' do
+ it 'reads an ID from the cache' do
+ Gitlab::GithubImport::Caching.write('foo', 10)
+
+ expect(finder.read_id_from_cache('foo')).to eq([true, 10])
+ end
+
+ it 'reads a cache key with an empty value' do
+ Gitlab::GithubImport::Caching.write('foo', nil)
+
+ expect(finder.read_id_from_cache('foo')).to eq([true, nil])
+ end
+
+ it 'reads a cache key that does not exist' do
+ expect(finder.read_id_from_cache('foo')).to eq([false, nil])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/user_formatter_spec.rb b/spec/lib/gitlab/github_import/user_formatter_spec.rb
deleted file mode 100644
index 98e3a7c28b9..00000000000
--- a/spec/lib/gitlab/github_import/user_formatter_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::UserFormatter do
- let(:client) { double }
- let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
-
- subject(:user) { described_class.new(client, octocat) }
-
- before do
- allow(client).to receive(:user).and_return(octocat)
- end
-
- describe '#gitlab_id' do
- context 'when GitHub user is a GitLab user' do
- it 'return GitLab user id when user associated their account with GitHub' do
- gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
-
- expect(user.gitlab_id).to eq gl_user.id
- end
-
- it 'returns GitLab user id when user primary email matches GitHub email' do
- gl_user = create(:user, email: octocat.email)
-
- expect(user.gitlab_id).to eq gl_user.id
- end
-
- it 'returns GitLab user id when any of user linked emails matches GitHub email' do
- gl_user = create(:user, email: 'johndoe@example.com')
- create(:email, user: gl_user, email: octocat.email)
-
- expect(user.gitlab_id).to eq gl_user.id
- end
- end
-
- it 'returns nil when GitHub user is not a GitLab user' do
- expect(user.gitlab_id).to be_nil
- end
- end
-end
diff --git a/spec/lib/gitlab/github_import/wiki_formatter_spec.rb b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb
deleted file mode 100644
index fcd90fab547..00000000000
--- a/spec/lib/gitlab/github_import/wiki_formatter_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::WikiFormatter do
- let(:project) do
- create(:project,
- namespace: create(:namespace, path: 'gitlabhq'),
- import_url: 'https://xxx@github.com/gitlabhq/sample.gitlabhq.git')
- end
-
- subject(:wiki) { described_class.new(project) }
-
- describe '#disk_path' do
- it 'appends .wiki to project path' do
- expect(wiki.disk_path).to eq project.disk_path + '.wiki'
- end
- end
-
- describe '#import_url' do
- it 'returns URL of the wiki repository' do
- expect(wiki.import_url).to eq 'https://xxx@github.com/gitlabhq/sample.gitlabhq.wiki.git'
- end
- end
-end
diff --git a/spec/lib/gitlab/github_import_spec.rb b/spec/lib/gitlab/github_import_spec.rb
new file mode 100644
index 00000000000..51414800e8c
--- /dev/null
+++ b/spec/lib/gitlab/github_import_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport do
+ let(:project) { double(:project) }
+
+ describe '.new_client_for' do
+ it 'returns a new Client with a custom token' do
+ expect(described_class::Client)
+ .to receive(:new)
+ .with('123', parallel: true)
+
+ described_class.new_client_for(project, token: '123')
+ end
+
+ it 'returns a new Client with a token stored in the import data' do
+ import_data = double(:import_data, credentials: { user: '123' })
+
+ expect(project)
+ .to receive(:import_data)
+ .and_return(import_data)
+
+ expect(described_class::Client)
+ .to receive(:new)
+ .with('123', parallel: true)
+
+ described_class.new_client_for(project)
+ end
+ end
+
+ describe '.insert_and_return_id' do
+ let(:attributes) { { iid: 1, title: 'foo' } }
+ let(:project) { create(:project) }
+
+ context 'on PostgreSQL' do
+ it 'returns the ID returned by the query' do
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(Issue.table_name, [attributes], return_ids: true)
+ .and_return([10])
+
+ id = described_class.insert_and_return_id(attributes, project.issues)
+
+ expect(id).to eq(10)
+ end
+ end
+
+ context 'on MySQL' do
+ it 'uses a separate query to retrieve the ID' do
+ issue = create(:issue, project: project, iid: attributes[:iid])
+
+ expect(Gitlab::Database)
+ .to receive(:bulk_insert)
+ .with(Issue.table_name, [attributes], return_ids: true)
+ .and_return([])
+
+ id = described_class.insert_and_return_id(attributes, project.issues)
+
+ expect(id).to eq(issue.id)
+ end
+ end
+ end
+
+ describe '.ghost_user_id', :clean_gitlab_redis_cache do
+ it 'returns the ID of the ghost user' do
+ expect(described_class.ghost_user_id).to eq(User.ghost.id)
+ end
+
+ it 'caches the ghost user ID' do
+ expect(Gitlab::GithubImport::Caching)
+ .to receive(:write)
+ .once
+ .and_call_original
+
+ 2.times do
+ described_class.ghost_user_id
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb
index 8dc83a6db7f..30686634af4 100644
--- a/spec/lib/gitlab/group_hierarchy_spec.rb
+++ b/spec/lib/gitlab/group_hierarchy_spec.rb
@@ -18,6 +18,12 @@ describe Gitlab::GroupHierarchy, :postgresql do
expect(relation).to include(parent, child1)
end
+ it 'can find ancestors upto a certain level' do
+ relation = described_class.new(Group.where(id: child2)).base_and_ancestors(upto: child1)
+
+ expect(relation).to contain_exactly(child2)
+ end
+
it 'uses ancestors_base #initialize argument' do
relation = described_class.new(Group.where(id: child2.id), Group.none).base_and_ancestors
@@ -55,6 +61,28 @@ describe Gitlab::GroupHierarchy, :postgresql do
end
end
+ describe '#descendants' do
+ it 'includes only the descendants' do
+ relation = described_class.new(Group.where(id: parent)).descendants
+
+ expect(relation).to contain_exactly(child1, child2)
+ end
+ end
+
+ describe '#ancestors' do
+ it 'includes only the ancestors' do
+ relation = described_class.new(Group.where(id: child2)).ancestors
+
+ expect(relation).to contain_exactly(child1, parent)
+ end
+
+ it 'can find ancestors upto a certain level' do
+ relation = described_class.new(Group.where(id: child2)).ancestors(upto: child1)
+
+ expect(relation).to be_empty
+ end
+ end
+
describe '#all_groups' do
let(:relation) do
described_class.new(Group.where(id: child1.id)).all_groups
diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
index 73dd236a5c6..4c1ca4349ea 100644
--- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
describe '#readiness' do
subject { described_class.readiness }
- context 'storage has a tripped circuitbreaker', broken_storage: true do
+ context 'storage has a tripped circuitbreaker', :broken_storage do
let(:repository_storages) { ['broken'] }
let(:storages_paths) do
Gitlab.config.repositories.storages
diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
new file mode 100644
index 00000000000..26529c4759d
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe Gitlab::HookData::IssuableBuilder do
+ set(:user) { create(:user) }
+
+ # This shared example requires a `builder` and `user` variable
+ shared_examples 'issuable hook data' do |kind|
+ let(:data) { builder.build(user: user) }
+
+ include_examples 'project hook data' do
+ let(:project) { builder.issuable.project }
+ end
+ include_examples 'deprecated repository hook data'
+
+ context "with a #{kind}" do
+ it 'contains issuable data' do
+ expect(data[:object_kind]).to eq(kind)
+ expect(data[:user]).to eq(user.hook_attrs)
+ expect(data[:project]).to eq(builder.issuable.project.hook_attrs)
+ expect(data[:object_attributes]).to eq(builder.issuable.hook_attrs)
+ expect(data[:changes]).to eq({})
+ expect(data[:repository]).to eq(builder.issuable.project.hook_attrs.slice(:name, :url, :description, :homepage))
+ end
+
+ it 'does not contain certain keys' do
+ expect(data).not_to have_key(:assignees)
+ expect(data).not_to have_key(:assignee)
+ end
+
+ describe 'changes are given' do
+ let(:changes) do
+ {
+ cached_markdown_version: %w[foo bar],
+ description: ['A description', 'A cool description'],
+ description_html: %w[foo bar],
+ in_progress_merge_commit_sha: %w[foo bar],
+ lock_version: %w[foo bar],
+ merge_jid: %w[foo bar],
+ title: ['A title', 'Hello World'],
+ title_html: %w[foo bar],
+ labels: [
+ [{ id: 1, title: 'foo' }],
+ [{ id: 1, title: 'foo' }, { id: 2, title: 'bar' }]
+ ],
+ total_time_spent: [1, 2]
+ }
+ end
+ let(:data) { builder.build(user: user, changes: changes) }
+
+ it 'populates the :changes hash' do
+ expect(data[:changes]).to match(hash_including({
+ title: { previous: 'A title', current: 'Hello World' },
+ description: { previous: 'A description', current: 'A cool description' },
+ labels: {
+ previous: [{ id: 1, title: 'foo' }],
+ current: [{ id: 1, title: 'foo' }, { id: 2, title: 'bar' }]
+ },
+ total_time_spent: {
+ previous: 1,
+ current: 2
+ }
+ }))
+ end
+
+ it 'does not contain certain keys' do
+ expect(data[:changes]).not_to have_key('cached_markdown_version')
+ expect(data[:changes]).not_to have_key('description_html')
+ expect(data[:changes]).not_to have_key('lock_version')
+ expect(data[:changes]).not_to have_key('title_html')
+ expect(data[:changes]).not_to have_key('in_progress_merge_commit_sha')
+ expect(data[:changes]).not_to have_key('merge_jid')
+ end
+ end
+ end
+ end
+
+ describe '#build' do
+ it_behaves_like 'issuable hook data', 'issue' do
+ let(:issuable) { create(:issue, description: 'A description') }
+ let(:builder) { described_class.new(issuable) }
+ end
+
+ it_behaves_like 'issuable hook data', 'merge_request' do
+ let(:issuable) { create(:merge_request, description: 'A description') }
+ let(:builder) { described_class.new(issuable) }
+ end
+
+ context 'issue is assigned' do
+ let(:issue) { create(:issue, assignees: [user]) }
+ let(:data) { described_class.new(issue).build(user: user) }
+
+ it 'returns correct hook data' do
+ expect(data[:object_attributes]['assignee_id']).to eq(user.id)
+ expect(data[:assignees].first).to eq(user.hook_attrs)
+ expect(data).not_to have_key(:assignee)
+ end
+ end
+
+ context 'merge_request is assigned' do
+ let(:merge_request) { create(:merge_request, assignee: user) }
+ let(:data) { described_class.new(merge_request).build(user: user) }
+
+ it 'returns correct hook data' do
+ expect(data[:object_attributes]['assignee_id']).to eq(user.id)
+ expect(data[:assignee]).to eq(user.hook_attrs)
+ expect(data).not_to have_key(:assignees)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
new file mode 100644
index 00000000000..aeacd577d18
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::HookData::IssueBuilder do
+ set(:issue) { create(:issue) }
+ let(:builder) { described_class.new(issue) }
+
+ describe '#build' do
+ let(:data) { builder.build }
+
+ it 'includes safe attribute' do
+ %w[
+ assignee_id
+ author_id
+ closed_at
+ confidential
+ created_at
+ deleted_at
+ description
+ due_date
+ id
+ iid
+ last_edited_at
+ last_edited_by_id
+ milestone_id
+ moved_to_id
+ project_id
+ relative_position
+ state
+ time_estimate
+ title
+ updated_at
+ updated_by_id
+ ].each do |key|
+ expect(data).to include(key)
+ end
+ end
+
+ it 'includes additional attrs' do
+ expect(data).to include(:total_time_spent)
+ expect(data).to include(:human_time_estimate)
+ expect(data).to include(:human_total_time_spent)
+ expect(data).to include(:assignee_ids)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
new file mode 100644
index 00000000000..78475403f9e
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Gitlab::HookData::MergeRequestBuilder do
+ set(:merge_request) { create(:merge_request) }
+ let(:builder) { described_class.new(merge_request) }
+
+ describe '#build' do
+ let(:data) { builder.build }
+
+ it 'includes safe attribute' do
+ %w[
+ assignee_id
+ author_id
+ created_at
+ deleted_at
+ description
+ head_pipeline_id
+ id
+ iid
+ last_edited_at
+ last_edited_by_id
+ merge_commit_sha
+ merge_error
+ merge_params
+ merge_status
+ merge_user_id
+ merge_when_pipeline_succeeds
+ milestone_id
+ source_branch
+ source_project_id
+ state
+ target_branch
+ target_project_id
+ time_estimate
+ title
+ updated_at
+ updated_by_id
+ ].each do |key|
+ expect(data).to include(key)
+ end
+ end
+
+ %i[source target].each do |key|
+ describe "#{key} key" do
+ include_examples 'project hook data', project_key: key do
+ let(:project) { merge_request.public_send("#{key}_project") }
+ end
+ end
+ end
+
+ it 'includes additional attrs' do
+ expect(data).to include(:source)
+ expect(data).to include(:target)
+ expect(data).to include(:last_commit)
+ expect(data).to include(:work_in_progress)
+ expect(data).to include(:total_time_spent)
+ expect(data).to include(:human_time_estimate)
+ expect(data).to include(:human_total_time_spent)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 29baa70d5ae..0ecb50f7110 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -88,6 +88,7 @@ merge_requests:
- metrics
- timelogs
- head_pipeline
+- latest_merge_request_diff
merge_request_diff:
- merge_request
- merge_request_diff_commits
@@ -147,10 +148,6 @@ deploy_keys:
- user
- deploy_keys_projects
- projects
-cluster:
-- project
-- user
-- service
services:
- project
- service_hook
@@ -182,6 +179,8 @@ project:
- tags
- chat_services
- cluster
+- clusters
+- cluster_project
- creator
- group
- namespace
@@ -195,6 +194,7 @@ project:
- mattermost_slash_commands_service
- slack_slash_commands_service
- irker_service
+- packagist_service
- pivotaltracker_service
- prometheus_service
- hipchat_service
@@ -275,6 +275,7 @@ project:
- root_of_fork_network
- fork_network_member
- fork_network
+- custom_attributes
award_emoji:
- awardable
- user
@@ -286,3 +287,6 @@ timelogs:
- user
push_event_payload:
- event
+issue_assignees:
+- issue
+- assignee
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
index dd0ce0dae41..cfb15ee7e8b 100644
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -46,7 +46,7 @@ describe 'forked project import' do
end
it 'can access the MR' do
- project.merge_requests.first.ensure_ref_fetched
+ project.merge_requests.first.fetch_ref!
expect(project.repository.ref_exists?('refs/merge-requests/1/head')).to be_truthy
end
diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
index 473ba40fae7..b793636c4d6 100644
--- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
+++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::ImportExport::MergeRequestParser do
let(:parsed_merge_request) do
described_class.new(project,
- merge_request.diff_head_sha,
+ 'abcd',
merge_request,
merge_request.as_json).parse!
end
@@ -29,4 +29,14 @@ describe Gitlab::ImportExport::MergeRequestParser do
it 'has a target branch' do
expect(project.repository.branch_exists?(parsed_merge_request.target_branch)).to be true
end
+
+ it 'parses a MR that has no source branch' do
+ allow_any_instance_of(described_class).to receive(:branch_exists?).and_call_original
+ allow_any_instance_of(described_class).to receive(:branch_exists?).with(merge_request.source_branch).and_return(false)
+ allow_any_instance_of(described_class).to receive(:fork_merge_request?).and_return(true)
+ allow(Gitlab::GitalyClient).to receive(:migrate).and_call_original
+ allow(Gitlab::GitalyClient).to receive(:migrate).with(:fetch_ref).and_return([nil, 0])
+
+ expect(parsed_merge_request).to eq(merge_request)
+ end
end
diff --git a/spec/lib/gitlab/import_export/project.group.json b/spec/lib/gitlab/import_export/project.group.json
new file mode 100644
index 00000000000..82a1fbd2fc5
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project.group.json
@@ -0,0 +1,188 @@
+{
+ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
+ "visibility_level": 10,
+ "archived": false,
+ "milestones": [
+ {
+ "id": 1,
+ "title": "Project milestone",
+ "project_id": 8,
+ "description": "Project-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ }
+ ],
+ "labels": [
+ {
+ "id": 2,
+ "title": "project label",
+ "color": "#428bca",
+ "project_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "type": "ProjectLabel",
+ "priorities": [
+ {
+ "id": 1,
+ "project_id": 5,
+ "label_id": 1,
+ "priority": 1,
+ "created_at": "2016-10-18T09:35:43.338Z",
+ "updated_at": "2016-10-18T09:35:43.338Z"
+ }
+ ]
+ }
+ ],
+ "issues": [
+ {
+ "id": 1,
+ "title": "Fugiat est minima quae maxime non similique.",
+ "assignee_id": null,
+ "project_id": 8,
+ "author_id": 1,
+ "created_at": "2017-07-07T18:13:01.138Z",
+ "updated_at": "2017-08-15T18:37:40.807Z",
+ "branch_name": null,
+ "description": "Quam totam fuga numquam in eveniet.",
+ "state": "opened",
+ "iid": 1,
+ "updated_by_id": 1,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "lock_version": null,
+ "time_estimate": 0,
+ "closed_at": null,
+ "last_edited_at": null,
+ "last_edited_by_id": null,
+ "group_milestone_id": null,
+ "milestone": {
+ "id": 1,
+ "title": "Project milestone",
+ "project_id": 8,
+ "description": "Project-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ },
+ "label_links": [
+ {
+ "id": 11,
+ "label_id": 6,
+ "target_id": 1,
+ "target_type": "Issue",
+ "created_at": "2017-08-15T18:37:40.795Z",
+ "updated_at": "2017-08-15T18:37:40.795Z",
+ "label": {
+ "id": 6,
+ "title": "group label",
+ "color": "#A8D695",
+ "project_id": null,
+ "created_at": "2017-08-15T18:37:19.698Z",
+ "updated_at": "2017-08-15T18:37:19.698Z",
+ "template": false,
+ "description": "",
+ "group_id": 5,
+ "type": "GroupLabel",
+ "priorities": []
+ }
+ },
+ {
+ "id": 11,
+ "label_id": 2,
+ "target_id": 1,
+ "target_type": "Issue",
+ "created_at": "2017-08-15T18:37:40.795Z",
+ "updated_at": "2017-08-15T18:37:40.795Z",
+ "label": {
+ "id": 6,
+ "title": "project label",
+ "color": "#A8D695",
+ "project_id": null,
+ "created_at": "2017-08-15T18:37:19.698Z",
+ "updated_at": "2017-08-15T18:37:19.698Z",
+ "template": false,
+ "description": "",
+ "group_id": 5,
+ "type": "ProjectLabel",
+ "priorities": []
+ }
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "title": "Fugiat est minima quae maxime non similique.",
+ "assignee_id": null,
+ "project_id": 8,
+ "author_id": 1,
+ "created_at": "2017-07-07T18:13:01.138Z",
+ "updated_at": "2017-08-15T18:37:40.807Z",
+ "branch_name": null,
+ "description": "Quam totam fuga numquam in eveniet.",
+ "state": "opened",
+ "iid": 2,
+ "updated_by_id": 1,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "lock_version": null,
+ "time_estimate": 0,
+ "closed_at": null,
+ "last_edited_at": null,
+ "last_edited_by_id": null,
+ "group_milestone_id": null,
+ "milestone": {
+ "id": 2,
+ "title": "A group milestone",
+ "description": "Group-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": 100
+ },
+ "label_links": [
+ {
+ "id": 11,
+ "label_id": 2,
+ "target_id": 1,
+ "target_type": "Issue",
+ "created_at": "2017-08-15T18:37:40.795Z",
+ "updated_at": "2017-08-15T18:37:40.795Z",
+ "label": {
+ "id": 2,
+ "title": "project label",
+ "color": "#A8D695",
+ "project_id": null,
+ "created_at": "2017-08-15T18:37:19.698Z",
+ "updated_at": "2017-08-15T18:37:19.698Z",
+ "template": false,
+ "description": "",
+ "group_id": 5,
+ "type": "ProjectLabel",
+ "priorities": []
+ }
+ }
+ ]
+ }
+ ],
+ "snippets": [
+
+ ],
+ "hooks": [
+
+ ]
+}
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 1115fb218d6..f0752649121 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -43,7 +43,7 @@
"issues": [
{
"id": 40,
- "title": "Voluptatem amet doloribus deleniti eos maxime repudiandae molestias.",
+ "title": "Voluptatem",
"assignee_id": 1,
"author_id": 22,
"project_id": 5,
@@ -60,6 +60,12 @@
"due_date": null,
"moved_to_id": null,
"test_ee_field": "test",
+ "issue_assignees": [
+ {
+ "user_id": 1,
+ "issue_id": 1
+ }
+ ],
"milestone": {
"id": 1,
"title": "test milestone",
@@ -3140,13 +3146,12 @@
"merge_request_diff": {
"id": 26,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "0b4bc9a49b562e85de7cc9e834518ea6828729b9",
+ "merge_request_diff_id": 26,
+ "sha": "0b4bc9a49b562e85de7cc9e834518ea6828729b9",
+ "relative_order": 0,
"message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "ae73cb07c9eeaf35924a10f713b364d32b2dd34f"
- ],
"authored_date": "2014-02-27T09:26:01.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3155,9 +3160,11 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n",
+ "merge_request_diff_id": 26,
+ "relative_order": 0,
+ "utf8_diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n",
"new_path": "files/ruby/feature.rb",
"old_path": "files/ruby/feature.rb",
"a_mode": "0",
@@ -3419,13 +3426,12 @@
"merge_request_diff": {
"id": 15,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "94b8d581c48d894b86661718582fecbc5e3ed2eb",
+ "merge_request_diff_id": 15,
+ "relative_order": 0,
+ "sha": "94b8d581c48d894b86661718582fecbc5e3ed2eb",
"message": "fixes #10\n",
- "parent_ids": [
- "be93687618e4b132087f430a4d8fc3a609c9b77c"
- ],
"authored_date": "2016-01-19T13:22:56.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
@@ -3434,9 +3440,11 @@
"committer_email": "james@jameslopez.es"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "--- /dev/null\n+++ b/test\n",
+ "merge_request_diff_id": 15,
+ "relative_order": 0,
+ "utf8_diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
"old_path": "test",
"a_mode": "0",
@@ -3698,13 +3706,12 @@
"merge_request_diff": {
"id": 14,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "ddd4ff416a931589c695eb4f5b23f844426f6928",
+ "merge_request_diff_id": 14,
+ "relative_order": 0,
+ "sha": "ddd4ff416a931589c695eb4f5b23f844426f6928",
"message": "fixes #10\n",
- "parent_ids": [
- "be93687618e4b132087f430a4d8fc3a609c9b77c"
- ],
"authored_date": "2016-01-19T14:14:43.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
@@ -3713,12 +3720,10 @@
"committer_email": "james@jameslopez.es"
},
{
- "id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "merge_request_diff_id": 14,
+ "relative_order": 1,
+ "sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849",
- "048721d90c449b244b7b4c53a9186b04330174ec"
- ],
"authored_date": "2015-12-07T12:52:12.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "marin@gitlab.com",
@@ -3727,11 +3732,10 @@
"committer_email": "marin@gitlab.com"
},
{
- "id": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "merge_request_diff_id": 14,
+ "relative_order": 2,
+ "sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849"
- ],
"authored_date": "2015-12-07T11:54:28.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "maxlazio@gmail.com",
@@ -3740,11 +3744,10 @@
"committer_email": "maxlazio@gmail.com"
},
{
- "id": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "merge_request_diff_id": 14,
+ "relative_order": 3,
+ "sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
- "parent_ids": [
- "d2d430676773caa88cdaf7c55944073b2fd5561a"
- ],
"authored_date": "2015-11-13T16:27:12.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -3753,12 +3756,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "merge_request_diff_id": 14,
+ "relative_order": 4,
+ "sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
- "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
- ],
"authored_date": "2015-11-13T08:50:17.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -3767,11 +3768,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "merge_request_diff_id": 14,
+ "relative_order": 5,
+ "sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8"
- ],
"authored_date": "2015-11-13T08:39:43.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -3780,12 +3780,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "merge_request_diff_id": 14,
+ "relative_order": 6,
+ "sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
- "parent_ids": [
- "19e2e9b4ef76b422ce1154af39a91323ccc57434",
- "66eceea0db202bb39c4e445e8ca28689645366c5"
- ],
"authored_date": "2015-11-13T07:21:40.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -3794,11 +3792,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "66eceea0db202bb39c4e445e8ca28689645366c5",
+ "merge_request_diff_id": 14,
+ "relative_order": 7,
+ "sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
- "parent_ids": [
- "08f22f255f082689c0d7d39d19205085311542bc"
- ],
"authored_date": "2015-11-13T06:01:27.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -3807,11 +3804,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "08f22f255f082689c0d7d39d19205085311542bc",
+ "merge_request_diff_id": 14,
+ "relative_order": 8,
+ "sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
- "parent_ids": [
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -3820,12 +3816,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "merge_request_diff_id": 14,
+ "relative_order": 9,
+ "sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T05:23:14.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -3834,11 +3828,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
+ "merge_request_diff_id": 14,
+ "relative_order": 10,
+ "sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
- "parent_ids": [
- "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0"
- ],
"authored_date": "2015-11-13T05:08:45.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -3847,11 +3840,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
+ "merge_request_diff_id": 14,
+ "relative_order": 11,
+ "sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd"
- ],
"authored_date": "2015-11-13T05:08:04.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -3860,11 +3852,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "merge_request_diff_id": 14,
+ "relative_order": 12,
+ "sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
- "parent_ids": [
- "e56497bb5f03a90a51293fc6d516788730953899"
- ],
"authored_date": "2015-08-25T17:53:12.000+02:00",
"author_name": "Stan Hu",
"author_email": "stanhu@packetzoom.com",
@@ -3873,12 +3864,10 @@
"committer_email": "stanhu@packetzoom.com"
},
{
- "id": "e56497bb5f03a90a51293fc6d516788730953899",
+ "merge_request_diff_id": 14,
+ "relative_order": 13,
+ "sha": "e56497bb5f03a90a51293fc6d516788730953899",
"message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)\n\nSee merge request !2\n",
- "parent_ids": [
- "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
- "4cd80ccab63c82b4bad16faa5193fbd2aa06df40"
- ],
"authored_date": "2015-01-10T22:23:29.000+01:00",
"author_name": "Sytse Sijbrandij",
"author_email": "sytse@gitlab.com",
@@ -3887,11 +3876,10 @@
"committer_email": "sytse@gitlab.com"
},
{
- "id": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
+ "merge_request_diff_id": 14,
+ "relative_order": 14,
+ "sha": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
"message": "add directory structure for tree_helper spec\n",
- "parent_ids": [
- "5937ac0a7beb003549fc5fd26fc247adbce4a52e"
- ],
"authored_date": "2015-01-10T21:28:18.000+01:00",
"author_name": "marmis85",
"author_email": "marmis85@gmail.com",
@@ -3900,11 +3888,10 @@
"committer_email": "marmis85@gmail.com"
},
{
- "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "merge_request_diff_id": 14,
+ "relative_order": 15,
+ "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
"message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
- ],
"authored_date": "2014-02-27T10:01:38.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3913,11 +3900,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
+ "merge_request_diff_id": 14,
+ "relative_order": 16,
+ "sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
- ],
"authored_date": "2014-02-27T09:57:31.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3926,11 +3912,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
+ "merge_request_diff_id": 14,
+ "relative_order": 17,
+ "sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
"message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "d14d6c0abdd253381df51a723d58691b2ee1ab08"
- ],
"authored_date": "2014-02-27T09:54:21.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3939,11 +3924,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
+ "merge_request_diff_id": 14,
+ "relative_order": 18,
+ "sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
"message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "c1acaa58bbcbc3eafe538cb8274ba387047b69f8"
- ],
"authored_date": "2014-02-27T09:49:50.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3952,11 +3936,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
+ "merge_request_diff_id": 14,
+ "relative_order": 19,
+ "sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
"message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "ae73cb07c9eeaf35924a10f713b364d32b2dd34f"
- ],
"authored_date": "2014-02-27T09:48:32.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3965,9 +3948,11 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "Binary files a/.DS_Store and /dev/null differ\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 0,
+ "utf8_diff": "Binary files a/.DS_Store and /dev/null differ\n",
"new_path": ".DS_Store",
"old_path": ".DS_Store",
"a_mode": "100644",
@@ -3978,7 +3963,9 @@
"too_large": false
},
{
- "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 1,
+ "utf8_diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n",
"new_path": ".gitignore",
"old_path": ".gitignore",
"a_mode": "100644",
@@ -3989,7 +3976,9 @@
"too_large": false
},
{
- "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 2,
+ "utf8_diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n",
"new_path": ".gitmodules",
"old_path": ".gitmodules",
"a_mode": "100644",
@@ -4000,7 +3989,9 @@
"too_large": false
},
{
- "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 3,
+ "utf8_diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
"new_path": "CHANGELOG",
"old_path": "CHANGELOG",
"a_mode": "100644",
@@ -4011,7 +4002,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 4,
+ "utf8_diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
"new_path": "encoding/iso8859.txt",
"old_path": "encoding/iso8859.txt",
"a_mode": "0",
@@ -4022,7 +4015,9 @@
"too_large": false
},
{
- "diff": "Binary files a/files/.DS_Store and /dev/null differ\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 5,
+ "utf8_diff": "Binary files a/files/.DS_Store and /dev/null differ\n",
"new_path": "files/.DS_Store",
"old_path": "files/.DS_Store",
"a_mode": "100644",
@@ -4033,7 +4028,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 6,
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -4044,7 +4041,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 7,
+ "utf8_diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
"new_path": "files/lfs/lfs_object.iso",
"old_path": "files/lfs/lfs_object.iso",
"a_mode": "0",
@@ -4055,7 +4054,9 @@
"too_large": false
},
{
- "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 8,
+ "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
"new_path": "files/ruby/popen.rb",
"old_path": "files/ruby/popen.rb",
"a_mode": "100644",
@@ -4066,7 +4067,9 @@
"too_large": false
},
{
- "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 9,
+ "utf8_diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n",
"new_path": "files/ruby/regex.rb",
"old_path": "files/ruby/regex.rb",
"a_mode": "100644",
@@ -4077,7 +4080,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
+ "merge_request_diff_id": 14,
+ "relative_order": 10,
+ "utf8_diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
"new_path": "files/whitespace",
"old_path": "files/whitespace",
"a_mode": "0",
@@ -4088,7 +4093,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 11,
+ "utf8_diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
"new_path": "foo/bar/.gitkeep",
"old_path": "foo/bar/.gitkeep",
"a_mode": "0",
@@ -4099,7 +4106,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 12,
+ "utf8_diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n",
"new_path": "gitlab-grack",
"old_path": "gitlab-grack",
"a_mode": "0",
@@ -4110,7 +4119,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 13,
+ "utf8_diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n",
"new_path": "gitlab-shell",
"old_path": "gitlab-shell",
"a_mode": "0",
@@ -4121,7 +4132,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/test\n",
+ "merge_request_diff_id": 14,
+ "relative_order": 14,
+ "utf8_diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
"old_path": "test",
"a_mode": "0",
@@ -4209,6 +4222,7 @@
},
"events": [
{
+ "merge_request_diff_id": 14,
"id": 529,
"target_type": "Note",
"target_id": 793,
@@ -4392,13 +4406,12 @@
"merge_request_diff": {
"id": 13,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "0bfedc29d30280c7e8564e19f654584b459e5868",
+ "merge_request_diff_id": 13,
+ "relative_order": 0,
+ "sha": "0bfedc29d30280c7e8564e19f654584b459e5868",
"message": "fixes #10\n",
- "parent_ids": [
- "be93687618e4b132087f430a4d8fc3a609c9b77c"
- ],
"authored_date": "2016-01-19T15:25:23.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
@@ -4407,12 +4420,10 @@
"committer_email": "james@jameslopez.es"
},
{
- "id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "merge_request_diff_id": 13,
+ "relative_order": 1,
+ "sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849",
- "048721d90c449b244b7b4c53a9186b04330174ec"
- ],
"authored_date": "2015-12-07T12:52:12.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "marin@gitlab.com",
@@ -4421,11 +4432,10 @@
"committer_email": "marin@gitlab.com"
},
{
- "id": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "merge_request_diff_id": 13,
+ "relative_order": 2,
+ "sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849"
- ],
"authored_date": "2015-12-07T11:54:28.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "maxlazio@gmail.com",
@@ -4434,11 +4444,10 @@
"committer_email": "maxlazio@gmail.com"
},
{
- "id": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "merge_request_diff_id": 13,
+ "relative_order": 3,
+ "sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
- "parent_ids": [
- "d2d430676773caa88cdaf7c55944073b2fd5561a"
- ],
"authored_date": "2015-11-13T16:27:12.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4447,12 +4456,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "merge_request_diff_id": 13,
+ "relative_order": 4,
+ "sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
- "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
- ],
"authored_date": "2015-11-13T08:50:17.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4461,11 +4468,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "merge_request_diff_id": 13,
+ "relative_order": 5,
+ "sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8"
- ],
"authored_date": "2015-11-13T08:39:43.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4474,12 +4480,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "merge_request_diff_id": 13,
+ "relative_order": 6,
+ "sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
- "parent_ids": [
- "19e2e9b4ef76b422ce1154af39a91323ccc57434",
- "66eceea0db202bb39c4e445e8ca28689645366c5"
- ],
"authored_date": "2015-11-13T07:21:40.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4488,11 +4492,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "66eceea0db202bb39c4e445e8ca28689645366c5",
+ "merge_request_diff_id": 13,
+ "relative_order": 7,
+ "sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
- "parent_ids": [
- "08f22f255f082689c0d7d39d19205085311542bc"
- ],
"authored_date": "2015-11-13T06:01:27.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -4501,11 +4504,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "08f22f255f082689c0d7d39d19205085311542bc",
+ "merge_request_diff_id": 13,
+ "relative_order": 8,
+ "sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
- "parent_ids": [
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -4514,12 +4516,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "merge_request_diff_id": 13,
+ "relative_order": 9,
+ "sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T05:23:14.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4528,11 +4528,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
+ "merge_request_diff_id": 13,
+ "relative_order": 10,
+ "sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
- "parent_ids": [
- "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0"
- ],
"authored_date": "2015-11-13T05:08:45.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -4541,11 +4540,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
+ "merge_request_diff_id": 13,
+ "relative_order": 11,
+ "sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd"
- ],
"authored_date": "2015-11-13T05:08:04.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -4554,11 +4552,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "merge_request_diff_id": 13,
+ "relative_order": 12,
+ "sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
- "parent_ids": [
- "e56497bb5f03a90a51293fc6d516788730953899"
- ],
"authored_date": "2015-08-25T17:53:12.000+02:00",
"author_name": "Stan Hu",
"author_email": "stanhu@packetzoom.com",
@@ -4567,12 +4564,10 @@
"committer_email": "stanhu@packetzoom.com"
},
{
- "id": "e56497bb5f03a90a51293fc6d516788730953899",
+ "merge_request_diff_id": 13,
+ "relative_order": 13,
+ "sha": "e56497bb5f03a90a51293fc6d516788730953899",
"message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)\n\nSee merge request !2\n",
- "parent_ids": [
- "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
- "4cd80ccab63c82b4bad16faa5193fbd2aa06df40"
- ],
"authored_date": "2015-01-10T22:23:29.000+01:00",
"author_name": "Sytse Sijbrandij",
"author_email": "sytse@gitlab.com",
@@ -4581,11 +4576,10 @@
"committer_email": "sytse@gitlab.com"
},
{
- "id": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
+ "merge_request_diff_id": 13,
+ "relative_order": 14,
+ "sha": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
"message": "add directory structure for tree_helper spec\n",
- "parent_ids": [
- "5937ac0a7beb003549fc5fd26fc247adbce4a52e"
- ],
"authored_date": "2015-01-10T21:28:18.000+01:00",
"author_name": "marmis85",
"author_email": "marmis85@gmail.com",
@@ -4594,9 +4588,11 @@
"committer_email": "marmis85@gmail.com"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
+ "merge_request_diff_id": 13,
+ "relative_order": 0,
+ "utf8_diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
"new_path": "CHANGELOG",
"old_path": "CHANGELOG",
"a_mode": "100644",
@@ -4607,7 +4603,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
+ "merge_request_diff_id": 13,
+ "relative_order": 1,
+ "utf8_diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
"new_path": "encoding/iso8859.txt",
"old_path": "encoding/iso8859.txt",
"a_mode": "0",
@@ -4618,7 +4616,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "merge_request_diff_id": 13,
+ "relative_order": 2,
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -4629,7 +4629,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
+ "merge_request_diff_id": 13,
+ "relative_order": 3,
+ "utf8_diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
"new_path": "files/lfs/lfs_object.iso",
"old_path": "files/lfs/lfs_object.iso",
"a_mode": "0",
@@ -4640,7 +4642,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
+ "merge_request_diff_id": 13,
+ "relative_order": 4,
+ "utf8_diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
"new_path": "files/whitespace",
"old_path": "files/whitespace",
"a_mode": "0",
@@ -4651,7 +4655,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
+ "merge_request_diff_id": 13,
+ "relative_order": 5,
+ "utf8_diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
"new_path": "foo/bar/.gitkeep",
"old_path": "foo/bar/.gitkeep",
"a_mode": "0",
@@ -4662,7 +4668,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/test\n",
+ "merge_request_diff_id": 13,
+ "relative_order": 6,
+ "utf8_diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
"old_path": "test",
"a_mode": "0",
@@ -4924,13 +4932,12 @@
"merge_request_diff": {
"id": 12,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "97a0df9696e2aebf10c31b3016f40214e0e8f243",
+ "merge_request_diff_id": 12,
+ "relative_order": 0,
+ "sha": "97a0df9696e2aebf10c31b3016f40214e0e8f243",
"message": "fixes #10\n",
- "parent_ids": [
- "be93687618e4b132087f430a4d8fc3a609c9b77c"
- ],
"authored_date": "2016-01-19T14:08:21.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
@@ -4939,12 +4946,10 @@
"committer_email": "james@jameslopez.es"
},
{
- "id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "merge_request_diff_id": 12,
+ "relative_order": 1,
+ "sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849",
- "048721d90c449b244b7b4c53a9186b04330174ec"
- ],
"authored_date": "2015-12-07T12:52:12.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "marin@gitlab.com",
@@ -4953,11 +4958,10 @@
"committer_email": "marin@gitlab.com"
},
{
- "id": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "merge_request_diff_id": 12,
+ "relative_order": 2,
+ "sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849"
- ],
"authored_date": "2015-12-07T11:54:28.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "maxlazio@gmail.com",
@@ -4966,11 +4970,10 @@
"committer_email": "maxlazio@gmail.com"
},
{
- "id": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "merge_request_diff_id": 12,
+ "relative_order": 3,
+ "sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
- "parent_ids": [
- "d2d430676773caa88cdaf7c55944073b2fd5561a"
- ],
"authored_date": "2015-11-13T16:27:12.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4979,12 +4982,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "merge_request_diff_id": 12,
+ "relative_order": 4,
+ "sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
- "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
- ],
"authored_date": "2015-11-13T08:50:17.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -4993,11 +4994,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "merge_request_diff_id": 12,
+ "relative_order": 5,
+ "sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8"
- ],
"authored_date": "2015-11-13T08:39:43.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5006,12 +5006,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "merge_request_diff_id": 12,
+ "relative_order": 6,
+ "sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
- "parent_ids": [
- "19e2e9b4ef76b422ce1154af39a91323ccc57434",
- "66eceea0db202bb39c4e445e8ca28689645366c5"
- ],
"authored_date": "2015-11-13T07:21:40.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5020,11 +5018,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "66eceea0db202bb39c4e445e8ca28689645366c5",
+ "merge_request_diff_id": 12,
+ "relative_order": 7,
+ "sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
- "parent_ids": [
- "08f22f255f082689c0d7d39d19205085311542bc"
- ],
"authored_date": "2015-11-13T06:01:27.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -5033,11 +5030,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "08f22f255f082689c0d7d39d19205085311542bc",
+ "merge_request_diff_id": 12,
+ "relative_order": 8,
+ "sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
- "parent_ids": [
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -5046,12 +5042,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "merge_request_diff_id": 12,
+ "relative_order": 9,
+ "sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T05:23:14.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5060,11 +5054,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
+ "merge_request_diff_id": 12,
+ "relative_order": 10,
+ "sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
- "parent_ids": [
- "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0"
- ],
"authored_date": "2015-11-13T05:08:45.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -5073,11 +5066,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
+ "merge_request_diff_id": 12,
+ "relative_order": 11,
+ "sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd"
- ],
"authored_date": "2015-11-13T05:08:04.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -5086,11 +5078,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "merge_request_diff_id": 12,
+ "relative_order": 12,
+ "sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
- "parent_ids": [
- "e56497bb5f03a90a51293fc6d516788730953899"
- ],
"authored_date": "2015-08-25T17:53:12.000+02:00",
"author_name": "Stan Hu",
"author_email": "stanhu@packetzoom.com",
@@ -5099,9 +5090,11 @@
"committer_email": "stanhu@packetzoom.com"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
+ "merge_request_diff_id": 12,
+ "relative_order": 0,
+ "utf8_diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
"new_path": "CHANGELOG",
"old_path": "CHANGELOG",
"a_mode": "100644",
@@ -5112,7 +5105,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
+ "merge_request_diff_id": 12,
+ "relative_order": 1,
+ "utf8_diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
"new_path": "encoding/iso8859.txt",
"old_path": "encoding/iso8859.txt",
"a_mode": "0",
@@ -5123,7 +5118,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "merge_request_diff_id": 12,
+ "relative_order": 2,
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -5134,7 +5131,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
+ "merge_request_diff_id": 12,
+ "relative_order": 3,
+ "utf8_diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
"new_path": "files/lfs/lfs_object.iso",
"old_path": "files/lfs/lfs_object.iso",
"a_mode": "0",
@@ -5145,7 +5144,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
+ "merge_request_diff_id": 12,
+ "relative_order": 4,
+ "utf8_diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
"new_path": "files/whitespace",
"old_path": "files/whitespace",
"a_mode": "0",
@@ -5156,7 +5157,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/test\n",
+ "merge_request_diff_id": 12,
+ "relative_order": 5,
+ "utf8_diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
"old_path": "test",
"a_mode": "0",
@@ -5418,9 +5421,9 @@
"merge_request_diff": {
"id": 11,
"state": "empty",
- "st_commits": null,
- "utf8_st_diffs": [
-
+ "merge_request_diff_commits": [
+ ],
+ "merge_request_diff_files": [
],
"merge_request_id": 11,
"created_at": "2016-06-14T15:02:23.772Z",
@@ -5673,13 +5676,12 @@
"merge_request_diff": {
"id": 10,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "f998ac87ac9244f15e9c15109a6f4e62a54b779d",
+ "merge_request_diff_id": 10,
+ "relative_order": 0,
+ "sha": "f998ac87ac9244f15e9c15109a6f4e62a54b779d",
"message": "fixes #10\n",
- "parent_ids": [
- "be93687618e4b132087f430a4d8fc3a609c9b77c"
- ],
"authored_date": "2016-01-19T14:43:23.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
@@ -5688,12 +5690,10 @@
"committer_email": "james@jameslopez.es"
},
{
- "id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "merge_request_diff_id": 10,
+ "relative_order": 1,
+ "sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849",
- "048721d90c449b244b7b4c53a9186b04330174ec"
- ],
"authored_date": "2015-12-07T12:52:12.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "marin@gitlab.com",
@@ -5702,11 +5702,10 @@
"committer_email": "marin@gitlab.com"
},
{
- "id": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "merge_request_diff_id": 10,
+ "relative_order": 2,
+ "sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
- "parent_ids": [
- "5f923865dde3436854e9ceb9cdb7815618d4e849"
- ],
"authored_date": "2015-12-07T11:54:28.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "maxlazio@gmail.com",
@@ -5715,11 +5714,10 @@
"committer_email": "maxlazio@gmail.com"
},
{
- "id": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "merge_request_diff_id": 10,
+ "relative_order": 3,
+ "sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
- "parent_ids": [
- "d2d430676773caa88cdaf7c55944073b2fd5561a"
- ],
"authored_date": "2015-11-13T16:27:12.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5728,12 +5726,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "merge_request_diff_id": 10,
+ "relative_order": 4,
+ "sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
- "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
- ],
"authored_date": "2015-11-13T08:50:17.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5742,11 +5738,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "merge_request_diff_id": 10,
+ "relative_order": 5,
+ "sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
- "parent_ids": [
- "59e29889be61e6e0e5e223bfa9ac2721d31605b8"
- ],
"authored_date": "2015-11-13T08:39:43.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5755,12 +5750,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "merge_request_diff_id": 10,
+ "relative_order": 6,
+ "sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
- "parent_ids": [
- "19e2e9b4ef76b422ce1154af39a91323ccc57434",
- "66eceea0db202bb39c4e445e8ca28689645366c5"
- ],
"authored_date": "2015-11-13T07:21:40.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5769,11 +5762,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "66eceea0db202bb39c4e445e8ca28689645366c5",
+ "merge_request_diff_id": 10,
+ "relative_order": 7,
+ "sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
- "parent_ids": [
- "08f22f255f082689c0d7d39d19205085311542bc"
- ],
"authored_date": "2015-11-13T06:01:27.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -5782,11 +5774,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "08f22f255f082689c0d7d39d19205085311542bc",
+ "merge_request_diff_id": 10,
+ "relative_order": 8,
+ "sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
- "parent_ids": [
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -5795,12 +5786,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "merge_request_diff_id": 10,
+ "relative_order": 9,
+ "sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
- "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
- ],
"authored_date": "2015-11-13T05:23:14.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
@@ -5809,11 +5798,10 @@
"committer_email": "stanhu@gmail.com"
},
{
- "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
+ "merge_request_diff_id": 10,
+ "relative_order": 10,
+ "sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
- "parent_ids": [
- "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0"
- ],
"authored_date": "2015-11-13T05:08:45.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -5822,11 +5810,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
+ "merge_request_diff_id": 10,
+ "relative_order": 11,
+ "sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
- "parent_ids": [
- "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd"
- ],
"authored_date": "2015-11-13T05:08:04.000+01:00",
"author_name": "윤민ì‹",
"author_email": "minsik.yoon@samsung.com",
@@ -5835,11 +5822,10 @@
"committer_email": "minsik.yoon@samsung.com"
},
{
- "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "merge_request_diff_id": 10,
+ "relative_order": 12,
+ "sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
- "parent_ids": [
- "e56497bb5f03a90a51293fc6d516788730953899"
- ],
"authored_date": "2015-08-25T17:53:12.000+02:00",
"author_name": "Stan Hu",
"author_email": "stanhu@packetzoom.com",
@@ -5848,12 +5834,10 @@
"committer_email": "stanhu@packetzoom.com"
},
{
- "id": "e56497bb5f03a90a51293fc6d516788730953899",
+ "merge_request_diff_id": 10,
+ "relative_order": 13,
+ "sha": "e56497bb5f03a90a51293fc6d516788730953899",
"message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)\n\nSee merge request !2\n",
- "parent_ids": [
- "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
- "4cd80ccab63c82b4bad16faa5193fbd2aa06df40"
- ],
"authored_date": "2015-01-10T22:23:29.000+01:00",
"author_name": "Sytse Sijbrandij",
"author_email": "sytse@gitlab.com",
@@ -5862,11 +5846,10 @@
"committer_email": "sytse@gitlab.com"
},
{
- "id": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
+ "merge_request_diff_id": 10,
+ "relative_order": 14,
+ "sha": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
"message": "add directory structure for tree_helper spec\n",
- "parent_ids": [
- "5937ac0a7beb003549fc5fd26fc247adbce4a52e"
- ],
"authored_date": "2015-01-10T21:28:18.000+01:00",
"author_name": "marmis85",
"author_email": "marmis85@gmail.com",
@@ -5875,11 +5858,10 @@
"committer_email": "marmis85@gmail.com"
},
{
- "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "merge_request_diff_id": 10,
+ "relative_order": 16,
+ "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
"message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
- ],
"authored_date": "2014-02-27T10:01:38.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5888,11 +5870,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
+ "merge_request_diff_id": 10,
+ "relative_order": 17,
+ "sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
- ],
"authored_date": "2014-02-27T09:57:31.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5901,11 +5882,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
+ "merge_request_diff_id": 10,
+ "relative_order": 18,
+ "sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
"message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "d14d6c0abdd253381df51a723d58691b2ee1ab08"
- ],
"authored_date": "2014-02-27T09:54:21.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5914,11 +5894,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
+ "merge_request_diff_id": 10,
+ "relative_order": 19,
+ "sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
"message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "c1acaa58bbcbc3eafe538cb8274ba387047b69f8"
- ],
"authored_date": "2014-02-27T09:49:50.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5927,11 +5906,10 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
},
{
- "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
+ "merge_request_diff_id": 10,
+ "relative_order": 20,
+ "sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
"message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "parent_ids": [
- "ae73cb07c9eeaf35924a10f713b364d32b2dd34f"
- ],
"authored_date": "2014-02-27T09:48:32.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5940,9 +5918,11 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "Binary files a/.DS_Store and /dev/null differ\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 0,
+ "utf8_diff": "Binary files a/.DS_Store and /dev/null differ\n",
"new_path": ".DS_Store",
"old_path": ".DS_Store",
"a_mode": "100644",
@@ -5953,7 +5933,9 @@
"too_large": false
},
{
- "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 1,
+ "utf8_diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n",
"new_path": ".gitignore",
"old_path": ".gitignore",
"a_mode": "100644",
@@ -5964,7 +5946,9 @@
"too_large": false
},
{
- "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 2,
+ "utf8_diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n",
"new_path": ".gitmodules",
"old_path": ".gitmodules",
"a_mode": "100644",
@@ -5975,7 +5959,9 @@
"too_large": false
},
{
- "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 3,
+ "utf8_diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
"new_path": "CHANGELOG",
"old_path": "CHANGELOG",
"a_mode": "100644",
@@ -5986,7 +5972,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 4,
+ "utf8_diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
"new_path": "encoding/iso8859.txt",
"old_path": "encoding/iso8859.txt",
"a_mode": "0",
@@ -5997,7 +5985,9 @@
"too_large": false
},
{
- "diff": "Binary files a/files/.DS_Store and /dev/null differ\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 5,
+ "utf8_diff": "Binary files a/files/.DS_Store and /dev/null differ\n",
"new_path": "files/.DS_Store",
"old_path": "files/.DS_Store",
"a_mode": "100644",
@@ -6008,7 +5998,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 6,
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -6019,7 +6011,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 7,
+ "utf8_diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
"new_path": "files/lfs/lfs_object.iso",
"old_path": "files/lfs/lfs_object.iso",
"a_mode": "0",
@@ -6030,7 +6024,9 @@
"too_large": false
},
{
- "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 8,
+ "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
"new_path": "files/ruby/popen.rb",
"old_path": "files/ruby/popen.rb",
"a_mode": "100644",
@@ -6041,7 +6037,9 @@
"too_large": false
},
{
- "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 9,
+ "utf8_diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n",
"new_path": "files/ruby/regex.rb",
"old_path": "files/ruby/regex.rb",
"a_mode": "100644",
@@ -6052,7 +6050,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
+ "merge_request_diff_id": 10,
+ "relative_order": 10,
+ "utf8_diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
"new_path": "files/whitespace",
"old_path": "files/whitespace",
"a_mode": "0",
@@ -6063,7 +6063,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 11,
+ "utf8_diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
"new_path": "foo/bar/.gitkeep",
"old_path": "foo/bar/.gitkeep",
"a_mode": "0",
@@ -6074,7 +6076,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 12,
+ "utf8_diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n",
"new_path": "gitlab-grack",
"old_path": "gitlab-grack",
"a_mode": "0",
@@ -6085,7 +6089,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 13,
+ "utf8_diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n",
"new_path": "gitlab-shell",
"old_path": "gitlab-shell",
"a_mode": "0",
@@ -6096,7 +6102,9 @@
"too_large": false
},
{
- "diff": "--- /dev/null\n+++ b/test\n",
+ "merge_request_diff_id": 10,
+ "relative_order": 14,
+ "utf8_diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
"old_path": "test",
"a_mode": "0",
@@ -6358,13 +6366,12 @@
"merge_request_diff": {
"id": 9,
"state": "collected",
- "st_commits": [
+ "merge_request_diff_commits": [
{
- "id": "a4e5dfebf42e34596526acb8611bc7ed80e4eb3f",
+ "merge_request_diff_id": 9,
+ "relative_order": 0,
+ "sha": "a4e5dfebf42e34596526acb8611bc7ed80e4eb3f",
"message": "fixes #10\n",
- "parent_ids": [
- "be93687618e4b132087f430a4d8fc3a609c9b77c"
- ],
"authored_date": "2016-01-19T15:44:02.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
@@ -6373,9 +6380,11 @@
"committer_email": "james@jameslopez.es"
}
],
- "utf8_st_diffs": [
+ "merge_request_diff_files": [
{
- "diff": "--- /dev/null\n+++ b/test\n",
+ "merge_request_diff_id": 9,
+ "relative_order": 0,
+ "utf8_diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
"old_path": "test",
"a_mode": "0",
@@ -7402,5 +7411,23 @@
"snippets_access_level": 20,
"updated_at": "2016-09-23T11:58:28.000Z",
"wiki_access_level": 20
- }
+ },
+ "custom_attributes": [
+ {
+ "id": 1,
+ "created_at": "2017-10-19T15:36:23.466Z",
+ "updated_at": "2017-10-19T15:36:23.466Z",
+ "project_id": 5,
+ "key": "foo",
+ "value": "foo"
+ },
+ {
+ "id": 2,
+ "created_at": "2017-10-19T15:37:21.904Z",
+ "updated_at": "2017-10-19T15:37:21.904Z",
+ "project_id": 5,
+ "key": "bar",
+ "value": "bar"
+ }
+ ]
}
diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json
index 2d8f3d4a566..02450478a77 100644
--- a/spec/lib/gitlab/import_export/project.light.json
+++ b/spec/lib/gitlab/import_export/project.light.json
@@ -5,9 +5,9 @@
"milestones": [
{
"id": 1,
- "title": "test milestone",
+ "title": "Project milestone",
"project_id": 8,
- "description": "test milestone",
+ "description": "Project-level milestone",
"due_date": null,
"created_at": "2016-06-14T15:02:04.415Z",
"updated_at": "2016-06-14T15:02:04.415Z",
@@ -19,7 +19,7 @@
"labels": [
{
"id": 2,
- "title": "test2",
+ "title": "A project label",
"color": "#428bca",
"project_id": 8,
"created_at": "2016-07-22T08:55:44.161Z",
@@ -63,30 +63,21 @@
"last_edited_at": null,
"last_edited_by_id": null,
"group_milestone_id": null,
+ "milestone": {
+ "id": 1,
+ "title": "Project milestone",
+ "project_id": 8,
+ "description": "Project-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ },
"label_links": [
{
"id": 11,
- "label_id": 6,
- "target_id": 1,
- "target_type": "Issue",
- "created_at": "2017-08-15T18:37:40.795Z",
- "updated_at": "2017-08-15T18:37:40.795Z",
- "label": {
- "id": 6,
- "title": "group label",
- "color": "#A8D695",
- "project_id": null,
- "created_at": "2017-08-15T18:37:19.698Z",
- "updated_at": "2017-08-15T18:37:19.698Z",
- "template": false,
- "description": "",
- "group_id": 5,
- "type": "GroupLabel",
- "priorities": []
- }
- },
- {
- "id": 11,
"label_id": 2,
"target_id": 1,
"target_type": "Issue",
@@ -94,14 +85,14 @@
"updated_at": "2017-08-15T18:37:40.795Z",
"label": {
"id": 6,
- "title": "project label",
+ "title": "Another project label",
"color": "#A8D695",
"project_id": null,
"created_at": "2017-08-15T18:37:19.698Z",
"updated_at": "2017-08-15T18:37:19.698Z",
"template": false,
"description": "",
- "group_id": 5,
+ "group_id": null,
"type": "ProjectLabel",
"priorities": []
}
@@ -109,10 +100,6 @@
]
}
],
- "snippets": [
-
- ],
- "hooks": [
-
- ]
+ "snippets": [],
+ "hooks": []
}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index efe11ca794a..0ab3afd0074 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
context 'JSON' do
it 'restores models based on JSON' do
- expect(@restored_project_json).to be true
+ expect(@restored_project_json).to be_truthy
end
it 'restore correct project features' do
@@ -63,6 +63,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
end
+ it 'has issue assignees' do
+ expect(Issue.where(title: 'Voluptatem').first.issue_assignees).not_to be_empty
+ end
+
it 'contains the merge access levels on a protected branch' do
expect(ProtectedBranch.first.merge_access_levels).not_to be_empty
end
@@ -91,26 +95,18 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
end
- it 'has the correct data for merge request st_diffs' do
- # makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+
- # one MergeRequestDiff uses the new format, where st_diffs is expected to be nil
-
- expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(8)
- end
-
it 'has the correct data for merge request diff files' do
- expect(MergeRequestDiffFile.where.not(diff: nil).count).to eq(9)
+ expect(MergeRequestDiffFile.where.not(diff: nil).count).to eq(55)
end
- it 'has the correct data for merge request diff commits in serialised and table formats' do
- expect(MergeRequestDiff.where.not(st_commits: nil).count).to eq(7)
- expect(MergeRequestDiffCommit.count).to eq(6)
+ it 'has the correct data for merge request diff commits' do
+ expect(MergeRequestDiffCommit.count).to eq(77)
end
- it 'has the correct time for merge request st_commits' do
- st_commits = MergeRequestDiff.where.not(st_commits: nil).first.st_commits
-
- expect(st_commits.first[:committed_date]).to be_kind_of(Time)
+ it 'has the correct data for merge request latest_merge_request_diff' do
+ MergeRequest.find_each do |merge_request|
+ expect(merge_request.latest_merge_request_diff_id).to eq(merge_request.merge_request_diffs.maximum(:id))
+ end
end
it 'has labels associated to label links, associated to issues' do
@@ -129,6 +125,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(@project.project_feature).not_to be_nil
end
+ it 'has custom attributes' do
+ expect(@project.custom_attributes.count).to eq(2)
+ end
+
it 'restores the correct service' do
expect(CustomIssueTrackerService.first).not_to be_nil
end
@@ -147,7 +147,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
it 'has no source if source/target differ' do
- expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1)
+ expect(MergeRequest.find_by_title('MR2').source_project_id).to be_nil
end
end
@@ -182,6 +182,53 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
end
+ shared_examples 'restores project successfully' do
+ it 'correctly restores project' do
+ expect(shared.errors).to be_empty
+ expect(restored_project_json).to be_truthy
+ end
+ end
+
+ shared_examples 'restores project correctly' do |**results|
+ it 'has labels' do
+ expect(project.labels.size).to eq(results.fetch(:labels, 0))
+ end
+
+ it 'has label priorities' do
+ expect(project.labels.first.priorities).not_to be_empty
+ end
+
+ it 'has milestones' do
+ expect(project.milestones.size).to eq(results.fetch(:milestones, 0))
+ end
+
+ it 'has issues' do
+ expect(project.issues.size).to eq(results.fetch(:issues, 0))
+ end
+
+ it 'has issue with group label and project label' do
+ labels = project.issues.first.labels
+
+ expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0))
+ end
+ end
+
+ shared_examples 'restores group correctly' do |**results|
+ it 'has group label' do
+ expect(project.group.labels.size).to eq(results.fetch(:labels, 0))
+ end
+
+ it 'has group milestone' do
+ expect(project.group.milestones.size).to eq(results.fetch(:milestones, 0))
+ end
+
+ it 'has issue with group label' do
+ labels = project.issues.first.labels
+
+ expect(labels.where(type: "GroupLabel").count).to eq(results.fetch(:first_issue_labels, 0))
+ end
+ end
+
context 'Light JSON' do
let(:user) { create(:user) }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
@@ -190,33 +237,45 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
let(:restored_project_json) { project_tree_restorer.restore }
before do
- project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
-
allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
end
- context 'project.json file access check' do
- it 'does not read a symlink' do
- Dir.mktmpdir do |tmpdir|
- setup_symlink(tmpdir, 'project.json')
- allow(shared).to receive(:export_path).and_call_original
+ context 'with a simple project' do
+ before do
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
+
+ restored_project_json
+ end
+
+ it_behaves_like 'restores project correctly',
+ issues: 1,
+ labels: 1,
+ milestones: 1,
+ first_issue_labels: 1
- restored_project_json
+ context 'project.json file access check' do
+ it 'does not read a symlink' do
+ Dir.mktmpdir do |tmpdir|
+ setup_symlink(tmpdir, 'project.json')
+ allow(shared).to receive(:export_path).and_call_original
- expect(shared.errors.first).to be_nil
+ restored_project_json
+
+ expect(shared.errors).to be_empty
+ end
end
end
- end
- context 'when there is an existing build with build token' do
- it 'restores project json correctly' do
- create(:ci_build, token: 'abcd')
+ context 'when there is an existing build with build token' do
+ before do
+ create(:ci_build, token: 'abcd')
+ end
- expect(restored_project_json).to be true
+ it_behaves_like 'restores project successfully'
end
end
- context 'with group' do
+ context 'with a project that has a group' do
let!(:project) do
create(:project,
:builds_disabled,
@@ -227,43 +286,22 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
before do
- project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.group.json")
restored_project_json
end
- it 'correctly restores project' do
- expect(restored_project_json).to be_truthy
- expect(shared.errors).to be_empty
- end
-
- it 'has labels' do
- expect(project.labels.count).to eq(2)
- end
-
- it 'creates group label' do
- expect(project.group.labels.count).to eq(1)
- end
-
- it 'has label priorities' do
- expect(project.labels.first.priorities).not_to be_empty
- end
-
- it 'has milestones' do
- expect(project.milestones.count).to eq(1)
- end
+ it_behaves_like 'restores project successfully'
+ it_behaves_like 'restores project correctly',
+ issues: 2,
+ labels: 1,
+ milestones: 1,
+ first_issue_labels: 1
- it 'has issue' do
- expect(project.issues.count).to eq(1)
- expect(project.issues.first.labels.count).to eq(2)
- end
-
- it 'has issue with group label and project label' do
- labels = project.issues.first.labels
-
- expect(labels.where(type: "GroupLabel").count).to eq(1)
- expect(labels.where(type: "ProjectLabel").count).to eq(1)
- end
+ it_behaves_like 'restores group correctly',
+ labels: 1,
+ milestones: 1,
+ first_issue_labels: 1
end
end
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index d9b86e1bf34..6243b6ac9f0 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -77,6 +77,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['issues'].first['notes']).not_to be_empty
end
+ it 'has issue assignees' do
+ expect(saved_project_json['issues'].first['issue_assignees']).not_to be_empty
+ end
+
it 'has author on issue comments' do
expect(saved_project_json['issues'].first['notes'].first['author']).not_to be_empty
end
@@ -89,10 +93,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['merge_requests'].first['merge_request_diff']).not_to be_empty
end
- it 'has merge requests diff st_diffs' do
- expect(saved_project_json['merge_requests'].first['merge_request_diff']['utf8_st_diffs']).not_to be_nil
- end
-
it 'has merge request diff files' do
expect(saved_project_json['merge_requests'].first['merge_request_diff']['merge_request_diff_files']).not_to be_empty
end
@@ -164,10 +164,8 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(project_feature["builds_access_level"]).to eq(ProjectFeature::PRIVATE)
end
- it 'does not complain about non UTF-8 characters in MR diffs' do
- ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
-
- expect(project_tree_saver.save).to be true
+ it 'has custom attributes' do
+ expect(saved_project_json['custom_attributes'].count).to eq(2)
end
it 'does not complain about non UTF-8 characters in MR diff files' do
@@ -275,6 +273,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
create(:event, :created, target: milestone, project: project, author: user)
create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
+ create(:project_custom_attribute, project: project)
+ create(:project_custom_attribute, project: project)
+
project
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 80d92b2e6a3..ec8fa99e0da 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -173,7 +173,6 @@ MergeRequest:
MergeRequestDiff:
- id
- state
-- st_commits
- merge_request_id
- created_at
- updated_at
@@ -313,30 +312,47 @@ Ci::PipelineSchedule:
- deleted_at
- created_at
- updated_at
-Gcp::Cluster:
+Clusters::Cluster:
- id
-- project_id
- user_id
-- service_id
- enabled
+- name
+- provider_type
+- platform_type
+- created_at
+- updated_at
+Clusters::Project:
+- id
+- project_id
+- cluster_id
+- created_at
+- updated_at
+Clusters::Providers::Gcp:
+- id
+- cluster_id
- status
- status_reason
-- project_namespace
+- gcp_project_id
+- zone
+- num_nodes
+- machine_type
+- operation_id
- endpoint
+- encrypted_access_token
+- encrypted_access_token_iv
+- created_at
+- updated_at
+Clusters::Platforms::Kubernetes:
+- id
+- cluster_id
+- api_url
- ca_cert
-- encrypted_kubernetes_token
-- encrypted_kubernetes_token_iv
+- namespace
- username
- encrypted_password
- encrypted_password_iv
-- gcp_project_id
-- gcp_cluster_zone
-- gcp_cluster_name
-- gcp_cluster_size
-- gcp_machine_type
-- gcp_operation_id
-- encrypted_gcp_token
-- encrypted_gcp_token_iv
+- encrypted_token
+- encrypted_token_iv
- created_at
- updated_at
DeployKey:
@@ -496,6 +512,7 @@ Timelog:
- merge_request_id
- issue_id
- user_id
+- spent_at
- created_at
- updated_at
ProjectAutoDevops:
@@ -505,3 +522,13 @@ ProjectAutoDevops:
- project_id
- created_at
- updated_at
+IssueAssignee:
+- user_id
+- issue_id
+ProjectCustomAttribute:
+- id
+- created_at
+- updated_at
+- project_id
+- key
+- value
diff --git a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb
new file mode 100644
index 00000000000..63992ea8ab8
--- /dev/null
+++ b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::UploadsRestorer do
+ describe 'bundle a project Git repo' do
+ let(:export_path) { "#{Dir.tmpdir}/uploads_saver_spec" }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) }
+ let(:uploads_path) { FileUploader.dynamic_path_segment(project) }
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ FileUtils.mkdir_p(File.join(shared.export_path, 'uploads/random'))
+ FileUtils.touch(File.join(shared.export_path, 'uploads/random', "dummy.txt"))
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ describe 'legacy storage' do
+ let(:project) { create(:project) }
+
+ subject(:restorer) { described_class.new(project: project, shared: shared) }
+
+ it 'saves the uploads successfully' do
+ expect(restorer.restore).to be true
+ end
+
+ it 'copies the uploads to the project path' do
+ restorer.restore
+
+ uploads = Dir.glob(File.join(uploads_path, '**/*')).map { |file| File.basename(file) }
+
+ expect(uploads).to include('dummy.txt')
+ end
+ end
+
+ describe 'hashed storage' do
+ let(:project) { create(:project, :hashed) }
+
+ subject(:restorer) { described_class.new(project: project, shared: shared) }
+
+ it 'saves the uploads successfully' do
+ expect(restorer.restore).to be true
+ end
+
+ it 'copies the uploads to the project path' do
+ restorer.restore
+
+ uploads = Dir.glob(File.join(uploads_path, '**/*')).map { |file| File.basename(file) }
+
+ expect(uploads).to include('dummy.txt')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/uploads_saver_spec.rb b/spec/lib/gitlab/import_export/uploads_saver_spec.rb
new file mode 100644
index 00000000000..e8948de1f3a
--- /dev/null
+++ b/spec/lib/gitlab/import_export/uploads_saver_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::UploadsSaver do
+ describe 'bundle a project Git repo' do
+ let(:export_path) { "#{Dir.tmpdir}/uploads_saver_spec" }
+ let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) }
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ describe 'legacy storage' do
+ let(:project) { create(:project) }
+
+ subject(:saver) { described_class.new(shared: shared, project: project) }
+
+ before do
+ UploadService.new(project, file, FileUploader).execute
+ end
+
+ it 'saves the uploads successfully' do
+ expect(saver.save).to be true
+ end
+
+ it 'copies the uploads to the export path' do
+ saver.save
+
+ uploads = Dir.glob(File.join(shared.export_path, 'uploads', '**/*')).map { |file| File.basename(file) }
+
+ expect(uploads).to include('banana_sample.gif')
+ end
+ end
+
+ describe 'hashed storage' do
+ let(:project) { create(:project, :hashed) }
+
+ subject(:saver) { described_class.new(shared: shared, project: project) }
+
+ before do
+ UploadService.new(project, file, FileUploader).execute
+ end
+
+ it 'saves the uploads successfully' do
+ expect(saver.save).to be true
+ end
+
+ it 'copies the uploads to the export path' do
+ saver.save
+
+ uploads = Dir.glob(File.join(shared.export_path, 'uploads', '**/*')).map { |file| File.basename(file) }
+
+ expect(uploads).to include('banana_sample.gif')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index c5725f47453..f2fa315e3ec 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -56,14 +56,14 @@ describe Gitlab::ImportSources do
describe '.importer' do
import_sources = {
- 'github' => Github::Import,
+ 'github' => Gitlab::GithubImport::ParallelImporter,
'bitbucket' => Gitlab::BitbucketImport::Importer,
'gitlab' => Gitlab::GitlabImport::Importer,
'google_code' => Gitlab::GoogleCodeImport::Importer,
'fogbugz' => Gitlab::FogbugzImport::Importer,
'git' => nil,
'gitlab_project' => Gitlab::ImportExport::Importer,
- 'gitea' => Gitlab::GithubImport::Importer
+ 'gitea' => Gitlab::LegacyGithubImport::Importer
}
import_sources.each do |name, klass|
diff --git a/spec/lib/gitlab/issuable_metadata_spec.rb b/spec/lib/gitlab/issuable_metadata_spec.rb
index 2455969a183..42635a68ee1 100644
--- a/spec/lib/gitlab/issuable_metadata_spec.rb
+++ b/spec/lib/gitlab/issuable_metadata_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe Gitlab::IssuableMetadata do
- let(:user) { create(:user) }
- let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
subject { Class.new { include Gitlab::IssuableMetadata }.new }
@@ -10,6 +10,10 @@ describe Gitlab::IssuableMetadata do
expect(subject.issuable_meta_data(Issue.none, 'Issue')).to eq({})
end
+ it 'raises an error when given a collection with no limit' do
+ expect { subject.issuable_meta_data(Issue.all, 'Issue') }.to raise_error(/must have a limit/)
+ end
+
context 'issues' do
let!(:issue) { create(:issue, author: user, project: project) }
let!(:closed_issue) { create(:issue, state: :closed, author: user, project: project) }
@@ -19,7 +23,7 @@ describe Gitlab::IssuableMetadata do
let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) }
it 'aggregates stats on issues' do
- data = subject.issuable_meta_data(Issue.all, 'Issue')
+ data = subject.issuable_meta_data(Issue.all.limit(10), 'Issue')
expect(data.count).to eq(2)
expect(data[issue.id].upvotes).to eq(1)
@@ -42,7 +46,7 @@ describe Gitlab::IssuableMetadata do
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
it 'aggregates stats on merge requests' do
- data = subject.issuable_meta_data(MergeRequest.all, 'MergeRequest')
+ data = subject.issuable_meta_data(MergeRequest.all.limit(10), 'MergeRequest')
expect(data.count).to eq(2)
expect(data[merge_request.id].upvotes).to eq(1)
diff --git a/spec/lib/gitlab/kubernetes/helm_spec.rb b/spec/lib/gitlab/kubernetes/helm_spec.rb
new file mode 100644
index 00000000000..15f99b0401f
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/helm_spec.rb
@@ -0,0 +1,100 @@
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::Helm do
+ let(:client) { double('kubernetes client') }
+ let(:helm) { described_class.new(client) }
+ let(:namespace) { Gitlab::Kubernetes::Namespace.new(described_class::NAMESPACE, client) }
+ let(:install_helm) { true }
+ let(:chart) { 'stable/a_chart' }
+ let(:application_name) { 'app_name' }
+ let(:command) { Gitlab::Kubernetes::Helm::InstallCommand.new(application_name, install_helm, chart) }
+ subject { helm }
+
+ before do
+ allow(Gitlab::Kubernetes::Namespace).to receive(:new).with(described_class::NAMESPACE, client).and_return(namespace)
+ end
+
+ describe '#initialize' do
+ it 'creates a namespace object' do
+ expect(Gitlab::Kubernetes::Namespace).to receive(:new).with(described_class::NAMESPACE, client)
+
+ subject
+ end
+ end
+
+ describe '#install' do
+ before do
+ allow(client).to receive(:create_pod).and_return(nil)
+ allow(namespace).to receive(:ensure_exists!).once
+ end
+
+ it 'ensures the namespace exists before creating the POD' do
+ expect(namespace).to receive(:ensure_exists!).once.ordered
+ expect(client).to receive(:create_pod).once.ordered
+
+ subject.install(command)
+ end
+ end
+
+ describe '#installation_status' do
+ let(:phase) { Gitlab::Kubernetes::Pod::RUNNING }
+ let(:pod) { Kubeclient::Resource.new(status: { phase: phase }) } # partial representation
+
+ it 'fetches POD phase from kubernetes cluster' do
+ expect(client).to receive(:get_pod).with(command.pod_name, described_class::NAMESPACE).once.and_return(pod)
+
+ expect(subject.installation_status(command.pod_name)).to eq(phase)
+ end
+ end
+
+ describe '#installation_log' do
+ let(:log) { 'some output' }
+ let(:response) { RestClient::Response.new(log) }
+
+ it 'fetches POD phase from kubernetes cluster' do
+ expect(client).to receive(:get_pod_log).with(command.pod_name, described_class::NAMESPACE).once.and_return(response)
+
+ expect(subject.installation_log(command.pod_name)).to eq(log)
+ end
+ end
+
+ describe '#delete_installation_pod!' do
+ it 'deletes the POD from kubernetes cluster' do
+ expect(client).to receive(:delete_pod).with(command.pod_name, described_class::NAMESPACE).once
+
+ subject.delete_installation_pod!(command.pod_name)
+ end
+ end
+
+ describe '#helm_init_command' do
+ subject { helm.send(:helm_init_command, command) }
+
+ context 'when command.install_helm is true' do
+ let(:install_helm) { true }
+
+ it { is_expected.to eq('helm init >/dev/null') }
+ end
+
+ context 'when command.install_helm is false' do
+ let(:install_helm) { false }
+
+ it { is_expected.to eq('helm init --client-only >/dev/null') }
+ end
+ end
+
+ describe '#helm_install_command' do
+ subject { helm.send(:helm_install_command, command) }
+
+ context 'when command.chart is nil' do
+ let(:chart) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when command.chart is set' do
+ let(:chart) { 'stable/a_chart' }
+
+ it { is_expected.to eq("helm install #{chart} --name #{application_name} --namespace #{namespace.name} >/dev/null")}
+ end
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/namespace_spec.rb b/spec/lib/gitlab/kubernetes/namespace_spec.rb
new file mode 100644
index 00000000000..b3c987f9344
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/namespace_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::Namespace do
+ let(:name) { 'a_namespace' }
+ let(:client) { double('kubernetes client') }
+ subject { described_class.new(name, client) }
+
+ it { expect(subject.name).to eq(name) }
+
+ describe '#exists?' do
+ context 'when namespace do not exits' do
+ let(:exception) { ::KubeException.new(404, "namespace #{name} not found", nil) }
+
+ it 'returns false' do
+ expect(client).to receive(:get_namespace).with(name).once.and_raise(exception)
+
+ expect(subject.exists?).to be_falsey
+ end
+ end
+
+ context 'when namespace exits' do
+ let(:namespace) { ::Kubeclient::Resource.new(kind: 'Namespace', metadata: { name: name }) } # partial representation
+
+ it 'returns true' do
+ expect(client).to receive(:get_namespace).with(name).once.and_return(namespace)
+
+ expect(subject.exists?).to be_truthy
+ end
+ end
+
+ context 'when cluster cannot be reached' do
+ let(:exception) { Errno::ECONNREFUSED.new }
+
+ it 'raises exception' do
+ expect(client).to receive(:get_namespace).with(name).once.and_raise(exception)
+
+ expect { subject.exists? }.to raise_error(exception)
+ end
+ end
+ end
+
+ describe '#create!' do
+ it 'creates a namespace' do
+ matcher = have_attributes(metadata: have_attributes(name: name))
+ expect(client).to receive(:create_namespace).with(matcher).once
+
+ expect { subject.create! }.not_to raise_error
+ end
+ end
+
+ describe '#ensure_exists!' do
+ it 'checks for existing namespace before creating' do
+ expect(subject).to receive(:exists?).once.ordered.and_return(false)
+ expect(subject).to receive(:create!).once.ordered
+
+ subject.ensure_exists!
+ end
+
+ it 'do not re-create an existing namespace' do
+ expect(subject).to receive(:exists?).once.and_return(true)
+ expect(subject).not_to receive(:create!)
+
+ subject.ensure_exists!
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ldap/authentication_spec.rb b/spec/lib/gitlab/ldap/authentication_spec.rb
index 01b6282af0c..9d57a46c12b 100644
--- a/spec/lib/gitlab/ldap/authentication_spec.rb
+++ b/spec/lib/gitlab/ldap/authentication_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe Gitlab::LDAP::Authentication do
- let(:user) { create(:omniauth_user, extern_uid: dn) }
- let(:dn) { 'uid=john,ou=people,dc=example,dc=com' }
+ let(:dn) { 'uid=John Smith, ou=People, dc=example, dc=com' }
+ let(:user) { create(:omniauth_user, extern_uid: Gitlab::LDAP::Person.normalize_dn(dn)) }
let(:login) { 'john' }
let(:password) { 'password' }
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 9a4705d1cee..260df6e4dae 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -44,23 +44,25 @@ describe Gitlab::LDAP::User do
end
describe '.find_by_uid_and_provider' do
+ let(:dn) { 'CN=John Åström, CN=Users, DC=Example, DC=com' }
+
it 'retrieves the correct user' do
special_info = {
name: 'John Åström',
email: 'john@example.com',
nickname: 'jastrom'
}
- special_hash = OmniAuth::AuthHash.new(uid: 'CN=John Åström,CN=Users,DC=Example,DC=com', provider: 'ldapmain', info: special_info)
+ special_hash = OmniAuth::AuthHash.new(uid: dn, provider: 'ldapmain', info: special_info)
special_chars_user = described_class.new(special_hash)
user = special_chars_user.save
- expect(described_class.find_by_uid_and_provider(special_hash.uid, special_hash.provider)).to eq user
+ expect(described_class.find_by_uid_and_provider(dn, 'ldapmain')).to eq user
end
end
describe 'find or create' do
it "finds the user if already existing" do
- create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain')
+ create(:omniauth_user, extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
expect { ldap_user.save }.not_to change { User.count }
end
diff --git a/spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb
new file mode 100644
index 00000000000..48655851140
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::BranchFormatter do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { create(:commit, project: project) }
+ let(:repo) { double }
+ let(:raw) do
+ {
+ ref: 'branch-merged',
+ repo: repo,
+ sha: commit.id
+ }
+ end
+
+ describe '#exists?' do
+ it 'returns true when branch exists and commit is part of the branch' do
+ branch = described_class.new(project, double(raw))
+
+ expect(branch.exists?).to eq true
+ end
+
+ it 'returns false when branch exists and commit is not part of the branch' do
+ branch = described_class.new(project, double(raw.merge(ref: 'feature')))
+
+ expect(branch.exists?).to eq false
+ end
+
+ it 'returns false when branch does not exist' do
+ branch = described_class.new(project, double(raw.merge(ref: 'removed-branch')))
+
+ expect(branch.exists?).to eq false
+ end
+ end
+
+ describe '#repo' do
+ it 'returns raw repo' do
+ branch = described_class.new(project, double(raw))
+
+ expect(branch.repo).to eq repo
+ end
+ end
+
+ describe '#sha' do
+ it 'returns raw sha' do
+ branch = described_class.new(project, double(raw))
+
+ expect(branch.sha).to eq commit.id
+ end
+ end
+
+ describe '#valid?' do
+ it 'returns true when raw sha and ref are present' do
+ branch = described_class.new(project, double(raw))
+
+ expect(branch.valid?).to eq true
+ end
+
+ it 'returns false when raw sha is blank' do
+ branch = described_class.new(project, double(raw.merge(sha: nil)))
+
+ expect(branch.valid?).to eq false
+ end
+
+ it 'returns false when raw ref is blank' do
+ branch = described_class.new(project, double(raw.merge(ref: nil)))
+
+ expect(branch.valid?).to eq false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/client_spec.rb b/spec/lib/gitlab/legacy_github_import/client_spec.rb
new file mode 100644
index 00000000000..80b767abce0
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/client_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::Client do
+ let(:token) { '123456' }
+ let(:github_provider) { Settingslogic.new('app_id' => 'asd123', 'app_secret' => 'asd123', 'name' => 'github', 'args' => { 'client_options' => {} }) }
+
+ subject(:client) { described_class.new(token) }
+
+ before do
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([github_provider])
+ end
+
+ it 'convert OAuth2 client options to symbols' do
+ client.client.options.keys.each do |key|
+ expect(key).to be_kind_of(Symbol)
+ end
+ end
+
+ it 'does not crash (e.g. Settingslogic::MissingSetting) when verify_ssl config is not present' do
+ expect { client.api }.not_to raise_error
+ end
+
+ context 'when config is missing' do
+ before do
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
+ end
+
+ it 'is still possible to get an Octokit client' do
+ expect { client.api }.not_to raise_error
+ end
+
+ it 'is not be possible to get an OAuth2 client' do
+ expect { client.client }.to raise_error(Projects::ImportService::Error)
+ end
+ end
+
+ context 'allow SSL verification to be configurable on API' do
+ before do
+ github_provider['verify_ssl'] = false
+ end
+
+ it 'uses supplied value' do
+ expect(client.client.options[:connection_opts][:ssl]).to eq({ verify: false })
+ expect(client.api.connection_options[:ssl]).to eq({ verify: false })
+ end
+ end
+
+ describe '#api_endpoint' do
+ context 'when provider does not specity an API endpoint' do
+ it 'uses GitHub root API endpoint' do
+ expect(client.api.api_endpoint).to eq 'https://api.github.com/'
+ end
+ end
+
+ context 'when provider specify a custom API endpoint' do
+ before do
+ github_provider['args']['client_options']['site'] = 'https://github.company.com/'
+ end
+
+ it 'uses the custom API endpoint' do
+ expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options)
+ expect(client.api.api_endpoint).to eq 'https://github.company.com/'
+ end
+ end
+
+ context 'when given a host' do
+ subject(:client) { described_class.new(token, host: 'https://try.gitea.io/') }
+
+ it 'builds a endpoint with the given host and the default API version' do
+ expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
+ end
+ end
+
+ context 'when given an API version' do
+ subject(:client) { described_class.new(token, api_version: 'v3') }
+
+ it 'does not use the API version without a host' do
+ expect(client.api.api_endpoint).to eq 'https://api.github.com/'
+ end
+ end
+
+ context 'when given a host and version' do
+ subject(:client) { described_class.new(token, host: 'https://try.gitea.io/', api_version: 'v3') }
+
+ it 'builds a endpoint with the given options' do
+ expect(client.api.api_endpoint).to eq 'https://try.gitea.io/api/v3/'
+ end
+ end
+ end
+
+ it 'does not raise error when rate limit is disabled' do
+ stub_request(:get, /api.github.com/)
+ allow(client.api).to receive(:rate_limit!).and_raise(Octokit::NotFound)
+
+ expect { client.issues {} }.not_to raise_error
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/comment_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/comment_formatter_spec.rb
new file mode 100644
index 00000000000..413654e108c
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/comment_formatter_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::CommentFormatter do
+ let(:client) { double }
+ let(:project) { create(:project) }
+ let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
+ let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') }
+ let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') }
+ let(:base) do
+ {
+ body: "I'm having a problem with this.",
+ user: octocat,
+ commit_id: nil,
+ diff_hunk: nil,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+ end
+
+ subject(:comment) { described_class.new(project, raw, client) }
+
+ before do
+ allow(client).to receive(:user).and_return(octocat)
+ end
+
+ describe '#attributes' do
+ context 'when do not reference a portion of the diff' do
+ let(:raw) { double(base) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ note: "*Created by: octocat*\n\nI'm having a problem with this.",
+ commit_id: nil,
+ line_code: nil,
+ author_id: project.creator_id,
+ type: nil,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(comment.attributes).to eq(expected)
+ end
+ end
+
+ context 'when on a portion of the diff' do
+ let(:diff) do
+ {
+ body: 'Great stuff',
+ commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
+ diff_hunk: "@@ -1,5 +1,9 @@\n class User\n def name\n- 'John Doe'\n+ 'Jane Doe'",
+ path: 'file1.txt'
+ }
+ end
+
+ let(:raw) { double(base.merge(diff)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ note: "*Created by: octocat*\n\nGreat stuff",
+ commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
+ line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_4_3',
+ author_id: project.creator_id,
+ type: 'LegacyDiffNote',
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(comment.attributes).to eq(expected)
+ end
+ end
+
+ context 'when author is a GitLab user' do
+ let(:raw) { double(base.merge(user: octocat)) }
+
+ it 'returns GitLab user id associated with GitHub id as author_id' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+
+ it 'returns GitLab user id associated with GitHub email as author_id' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+
+ it 'returns note without created at tag line' do
+ create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(comment.attributes.fetch(:note)).to eq("I'm having a problem with this.")
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/importer_spec.rb b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
new file mode 100644
index 00000000000..20514486727
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
@@ -0,0 +1,309 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::Importer do
+ shared_examples 'Gitlab::LegacyGithubImport::Importer#execute' do
+ let(:expected_not_called) { [] }
+
+ before do
+ allow(project).to receive(:import_data).and_return(double.as_null_object)
+ end
+
+ it 'calls import methods' do
+ importer = described_class.new(project)
+
+ expected_called = [
+ :import_labels, :import_milestones, :import_pull_requests, :import_issues,
+ :import_wiki, :import_releases, :handle_errors
+ ]
+
+ expected_called -= expected_not_called
+
+ aggregate_failures do
+ expected_called.each do |method_name|
+ expect(importer).to receive(method_name)
+ end
+
+ expect(importer).to receive(:import_comments).with(:issues)
+ expect(importer).to receive(:import_comments).with(:pull_requests)
+
+ expected_not_called.each do |method_name|
+ expect(importer).not_to receive(method_name)
+ end
+ end
+
+ importer.execute
+ end
+ end
+
+ shared_examples 'Gitlab::LegacyGithubImport::Importer#execute an error occurs' do
+ before do
+ allow(project).to receive(:import_data).and_return(double.as_null_object)
+
+ allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
+
+ allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound)
+ allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
+
+ allow_any_instance_of(Octokit::Client).to receive(:user).and_return(octocat)
+ allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2])
+ allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
+ allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
+ allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request])
+ allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([])
+ allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([])
+ allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
+ allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
+ end
+
+ let(:label1) do
+ double(
+ name: 'Bug',
+ color: 'ff0000',
+ url: "#{api_root}/repos/octocat/Hello-World/labels/bug"
+ )
+ end
+
+ let(:label2) do
+ double(
+ name: nil,
+ color: 'ff0000',
+ url: "#{api_root}/repos/octocat/Hello-World/labels/bug"
+ )
+ end
+
+ let(:milestone) do
+ double(
+ id: 1347, # For Gitea
+ number: 1347,
+ state: 'open',
+ title: '1.0',
+ description: 'Version 1.0',
+ due_on: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: "#{api_root}/repos/octocat/Hello-World/milestones/1"
+ )
+ end
+
+ let(:issue1) do
+ double(
+ number: 1347,
+ milestone: nil,
+ state: 'open',
+ title: 'Found a bug',
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: "#{api_root}/repos/octocat/Hello-World/issues/1347",
+ labels: [double(name: 'Label #1')]
+ )
+ end
+
+ let(:issue2) do
+ double(
+ number: 1348,
+ milestone: nil,
+ state: 'open',
+ title: nil,
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: "#{api_root}/repos/octocat/Hello-World/issues/1348",
+ labels: [double(name: 'Label #2')]
+ )
+ end
+
+ let(:release1) do
+ double(
+ tag_name: 'v1.0.0',
+ name: 'First release',
+ body: 'Release v1.0.0',
+ draft: false,
+ created_at: created_at,
+ updated_at: updated_at,
+ url: "#{api_root}/repos/octocat/Hello-World/releases/1"
+ )
+ end
+
+ let(:release2) do
+ double(
+ tag_name: 'v2.0.0',
+ name: 'Second release',
+ body: nil,
+ draft: false,
+ created_at: created_at,
+ updated_at: updated_at,
+ url: "#{api_root}/repos/octocat/Hello-World/releases/2"
+ )
+ end
+
+ subject { described_class.new(project) }
+
+ it 'returns true' do
+ expect(subject.execute).to eq true
+ end
+
+ it 'does not raise an error' do
+ expect { subject.execute }.not_to raise_error
+ end
+
+ it 'stores error messages' do
+ error = {
+ message: 'The remote data could not be fully imported.',
+ errors: [
+ { type: :label, url: "#{api_root}/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
+ { type: :issue, url: "#{api_root}/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank" },
+ { type: :wiki, errors: "Gitlab::Shell::Error" }
+ ]
+ }
+
+ unless project.gitea_import?
+ error[:errors] << { type: :release, url: "#{api_root}/repos/octocat/Hello-World/releases/2", errors: "Validation failed: Description can't be blank" }
+ end
+
+ described_class.new(project).execute
+
+ expect(project.import_error).to eq error.to_json
+ end
+ end
+
+ shared_examples 'Gitlab::LegacyGithubImport unit-testing' do
+ describe '#clean_up_restored_branches' do
+ subject { described_class.new(project) }
+
+ before do
+ allow(gh_pull_request).to receive(:source_branch_exists?).at_least(:once) { false }
+ allow(gh_pull_request).to receive(:target_branch_exists?).at_least(:once) { false }
+ end
+
+ context 'when pull request stills open' do
+ let(:gh_pull_request) { Gitlab::LegacyGithubImport::PullRequestFormatter.new(project, pull_request) }
+
+ it 'does not remove branches' do
+ expect(subject).not_to receive(:remove_branch)
+ subject.send(:clean_up_restored_branches, gh_pull_request)
+ end
+ end
+
+ context 'when pull request is closed' do
+ let(:gh_pull_request) { Gitlab::LegacyGithubImport::PullRequestFormatter.new(project, closed_pull_request) }
+
+ it 'does remove branches' do
+ expect(subject).to receive(:remove_branch).at_least(2).times
+ subject.send(:clean_up_restored_branches, gh_pull_request)
+ end
+ end
+ end
+ end
+
+ let(:project) { create(:project, :repository, :wiki_disabled, import_url: "#{repo_root}/octocat/Hello-World.git") }
+ let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
+ let(:credentials) { { user: 'joe' } }
+
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+ let(:repository) { double(id: 1, fork: false) }
+ let(:source_sha) { create(:commit, project: project).id }
+ let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha, user: octocat) }
+ let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
+ let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha, user: octocat) }
+ let(:pull_request) do
+ double(
+ number: 1347,
+ milestone: nil,
+ state: 'open',
+ title: 'New feature',
+ body: 'Please pull these awesome changes',
+ head: source_branch,
+ base: target_branch,
+ assignee: nil,
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ merged_at: nil,
+ url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
+ labels: [double(name: 'Label #2')]
+ )
+ end
+ let(:closed_pull_request) do
+ double(
+ number: 1347,
+ milestone: nil,
+ state: 'closed',
+ title: 'New feature',
+ body: 'Please pull these awesome changes',
+ head: source_branch,
+ base: target_branch,
+ assignee: nil,
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: updated_at,
+ merged_at: nil,
+ url: "#{api_root}/repos/octocat/Hello-World/pulls/1347",
+ labels: [double(name: 'Label #2')]
+ )
+ end
+
+ context 'when importing a GitHub project' do
+ let(:api_root) { 'https://api.github.com' }
+ let(:repo_root) { 'https://github.com' }
+ subject { described_class.new(project) }
+
+ it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute'
+ it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute an error occurs'
+ it_behaves_like 'Gitlab::LegacyGithubImport unit-testing'
+
+ describe '#client' do
+ it 'instantiates a Client' do
+ allow(project).to receive(:import_data).and_return(double(credentials: credentials))
+ expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with(
+ credentials[:user],
+ {}
+ )
+
+ subject.client
+ end
+ end
+ end
+
+ context 'when importing a Gitea project' do
+ let(:api_root) { 'https://try.gitea.io/api/v1' }
+ let(:repo_root) { 'https://try.gitea.io' }
+ subject { described_class.new(project) }
+
+ before do
+ project.update(import_type: 'gitea', import_url: "#{repo_root}/foo/group/project.git")
+ end
+
+ it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute' do
+ let(:expected_not_called) { [:import_releases] }
+ end
+ it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute an error occurs'
+ it_behaves_like 'Gitlab::LegacyGithubImport unit-testing'
+
+ describe '#client' do
+ it 'instantiates a Client' do
+ allow(project).to receive(:import_data).and_return(double(credentials: credentials))
+ expect(Gitlab::LegacyGithubImport::Client).to receive(:new).with(
+ credentials[:user],
+ { host: "#{repo_root}:443/foo", api_version: 'v1' }
+ )
+
+ subject.client
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb
new file mode 100644
index 00000000000..3b5d8945344
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::IssuableFormatter do
+ let(:raw_data) do
+ double(number: 42)
+ end
+ let(:project) { double(import_type: 'github') }
+ let(:issuable_formatter) { described_class.new(project, raw_data) }
+
+ describe '#project_association' do
+ it { expect { issuable_formatter.project_association }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#number' do
+ it { expect(issuable_formatter.number).to eq(42) }
+ end
+
+ describe '#find_condition' do
+ it { expect(issuable_formatter.find_condition).to eq({ iid: 42 }) }
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb
new file mode 100644
index 00000000000..1a4d5dbfb70
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb
@@ -0,0 +1,195 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::IssueFormatter do
+ let(:client) { double }
+ let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
+ let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+
+ let(:base_data) do
+ {
+ number: 1347,
+ milestone: nil,
+ state: 'open',
+ title: 'Found a bug',
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil
+ }
+ end
+
+ subject(:issue) { described_class.new(project, raw_data, client) }
+
+ before do
+ allow(client).to receive(:user).and_return(octocat)
+ end
+
+ shared_examples 'Gitlab::LegacyGithubImport::IssueFormatter#attributes' do
+ context 'when issue is open' do
+ let(:raw_data) { double(base_data.merge(state: 'open')) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ iid: 1347,
+ project: project,
+ milestone: nil,
+ title: 'Found a bug',
+ description: "*Created by: octocat*\n\nI'm having a problem with this.",
+ state: 'opened',
+ author_id: project.creator_id,
+ assignee_ids: [],
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(issue.attributes).to eq(expected)
+ end
+ end
+
+ context 'when issue is closed' do
+ let(:raw_data) { double(base_data.merge(state: 'closed')) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ iid: 1347,
+ project: project,
+ milestone: nil,
+ title: 'Found a bug',
+ description: "*Created by: octocat*\n\nI'm having a problem with this.",
+ state: 'closed',
+ author_id: project.creator_id,
+ assignee_ids: [],
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(issue.attributes).to eq(expected)
+ end
+ end
+
+ context 'when it is assigned to someone' do
+ let(:raw_data) { double(base_data.merge(assignee: octocat)) }
+
+ it 'returns nil as assignee_id when is not a GitLab user' do
+ expect(issue.attributes.fetch(:assignee_ids)).to be_empty
+ end
+
+ it 'returns GitLab user id associated with GitHub id as assignee_id' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(issue.attributes.fetch(:assignee_ids)).to eq [gl_user.id]
+ end
+
+ it 'returns GitLab user id associated with GitHub email as assignee_id' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(issue.attributes.fetch(:assignee_ids)).to eq [gl_user.id]
+ end
+ end
+
+ context 'when it has a milestone' do
+ let(:milestone) { double(id: 42, number: 42) }
+ let(:raw_data) { double(base_data.merge(milestone: milestone)) }
+
+ it 'returns nil when milestone does not exist' do
+ expect(issue.attributes.fetch(:milestone)).to be_nil
+ end
+
+ it 'returns milestone when it exists' do
+ milestone = create(:milestone, project: project, iid: 42)
+
+ expect(issue.attributes.fetch(:milestone)).to eq milestone
+ end
+ end
+
+ context 'when author is a GitLab user' do
+ let(:raw_data) { double(base_data.merge(user: octocat)) }
+
+ it 'returns project creator_id as author_id when is not a GitLab user' do
+ expect(issue.attributes.fetch(:author_id)).to eq project.creator_id
+ end
+
+ it 'returns GitLab user id associated with GitHub id as author_id' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+
+ it 'returns GitLab user id associated with GitHub email as author_id' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+
+ it 'returns description without created at tag line' do
+ create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(issue.attributes.fetch(:description)).to eq("I'm having a problem with this.")
+ end
+ end
+ end
+
+ shared_examples 'Gitlab::LegacyGithubImport::IssueFormatter#number' do
+ let(:raw_data) { double(base_data.merge(number: 1347)) }
+
+ it 'returns issue number' do
+ expect(issue.number).to eq 1347
+ end
+ end
+
+ context 'when importing a GitHub project' do
+ it_behaves_like 'Gitlab::LegacyGithubImport::IssueFormatter#attributes'
+ it_behaves_like 'Gitlab::LegacyGithubImport::IssueFormatter#number'
+ end
+
+ context 'when importing a Gitea project' do
+ before do
+ project.update(import_type: 'gitea')
+ end
+
+ it_behaves_like 'Gitlab::LegacyGithubImport::IssueFormatter#attributes'
+ it_behaves_like 'Gitlab::LegacyGithubImport::IssueFormatter#number'
+ end
+
+ describe '#has_comments?' do
+ context 'when number of comments is greater than zero' do
+ let(:raw_data) { double(base_data.merge(comments: 1)) }
+
+ it 'returns true' do
+ expect(issue.has_comments?).to eq true
+ end
+ end
+
+ context 'when number of comments is equal to zero' do
+ let(:raw_data) { double(base_data.merge(comments: 0)) }
+
+ it 'returns false' do
+ expect(issue.has_comments?).to eq false
+ end
+ end
+ end
+
+ describe '#pull_request?' do
+ context 'when mention a pull request' do
+ let(:raw_data) { double(base_data.merge(pull_request: double)) }
+
+ it 'returns true' do
+ expect(issue.pull_request?).to eq true
+ end
+ end
+
+ context 'when does not mention a pull request' do
+ let(:raw_data) { double(base_data.merge(pull_request: nil)) }
+
+ it 'returns false' do
+ expect(issue.pull_request?).to eq false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb
new file mode 100644
index 00000000000..0d1d04f1bf6
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::LabelFormatter do
+ let(:project) { create(:project) }
+ let(:raw) { double(name: 'improvements', color: 'e6e6e6') }
+
+ subject { described_class.new(project, raw) }
+
+ describe '#attributes' do
+ it 'returns formatted attributes' do
+ expect(subject.attributes).to eq({
+ project: project,
+ title: 'improvements',
+ color: '#e6e6e6'
+ })
+ end
+ end
+
+ describe '#create!' do
+ context 'when label does not exist' do
+ it 'creates a new label' do
+ expect { subject.create! }.to change(Label, :count).by(1)
+ end
+ end
+
+ context 'when label exists' do
+ it 'does not create a new label' do
+ Labels::CreateService.new(name: raw.name).execute(project: project)
+
+ expect { subject.create! }.not_to change(Label, :count)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb
new file mode 100644
index 00000000000..1db4bbb568c
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::MilestoneFormatter do
+ let(:project) { create(:project) }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+ let(:base_data) do
+ {
+ state: 'open',
+ title: '1.0',
+ description: 'Version 1.0',
+ due_on: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil
+ }
+ end
+ let(:iid_attr) { :number }
+
+ subject(:formatter) { described_class.new(project, raw_data) }
+
+ shared_examples 'Gitlab::LegacyGithubImport::MilestoneFormatter#attributes' do
+ let(:data) { base_data.merge(iid_attr => 1347) }
+
+ context 'when milestone is open' do
+ let(:raw_data) { double(data.merge(state: 'open')) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ iid: 1347,
+ project: project,
+ title: '1.0',
+ description: 'Version 1.0',
+ state: 'active',
+ due_date: nil,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(formatter.attributes).to eq(expected)
+ end
+ end
+
+ context 'when milestone is closed' do
+ let(:raw_data) { double(data.merge(state: 'closed')) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ iid: 1347,
+ project: project,
+ title: '1.0',
+ description: 'Version 1.0',
+ state: 'closed',
+ due_date: nil,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(formatter.attributes).to eq(expected)
+ end
+ end
+
+ context 'when milestone has a due date' do
+ let(:due_date) { DateTime.strptime('2011-01-28T19:01:12Z') }
+ let(:raw_data) { double(data.merge(due_on: due_date)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ iid: 1347,
+ project: project,
+ title: '1.0',
+ description: 'Version 1.0',
+ state: 'active',
+ due_date: due_date,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(formatter.attributes).to eq(expected)
+ end
+ end
+ end
+
+ context 'when importing a GitHub project' do
+ it_behaves_like 'Gitlab::LegacyGithubImport::MilestoneFormatter#attributes'
+ end
+
+ context 'when importing a Gitea project' do
+ let(:iid_attr) { :id }
+ before do
+ project.update(import_type: 'gitea')
+ end
+
+ it_behaves_like 'Gitlab::LegacyGithubImport::MilestoneFormatter#attributes'
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
new file mode 100644
index 00000000000..737c9a624e0
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::ProjectCreator do
+ let(:user) { create(:user) }
+ let(:namespace) { create(:group, owner: user) }
+
+ let(:repo) do
+ OpenStruct.new(
+ login: 'vim',
+ name: 'vim',
+ full_name: 'asd/vim',
+ clone_url: 'https://gitlab.com/asd/vim.git'
+ )
+ end
+
+ subject(:service) { described_class.new(repo, repo.name, namespace, user, github_access_token: 'asdffg') }
+
+ before do
+ namespace.add_owner(user)
+ allow_any_instance_of(Project).to receive(:add_import_job)
+ end
+
+ describe '#execute' do
+ it 'creates a project' do
+ expect { service.execute }.to change(Project, :count).by(1)
+ end
+
+ it 'handle GitHub credentials' do
+ project = service.execute
+
+ expect(project.import_url).to eq('https://asdffg@gitlab.com/asd/vim.git')
+ expect(project.safe_import_url).to eq('https://*****@gitlab.com/asd/vim.git')
+ expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil)
+ end
+
+ context 'when GitHub project is private' do
+ it 'sets project visibility to private' do
+ repo.private = true
+
+ project = service.execute
+
+ expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'when GitHub project is public' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'sets project visibility to the default project visibility' do
+ repo.private = false
+
+ project = service.execute
+
+ expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+ end
+
+ context 'when GitHub project has wiki' do
+ it 'does not create the wiki repository' do
+ allow(repo).to receive(:has_wiki?).and_return(true)
+
+ project = service.execute
+
+ expect(project.wiki.repository_exists?).to eq false
+ end
+ end
+
+ context 'when GitHub project does not have wiki' do
+ it 'creates the wiki repository' do
+ allow(repo).to receive(:has_wiki?).and_return(false)
+
+ project = service.execute
+
+ expect(project.wiki.repository_exists?).to eq true
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb
new file mode 100644
index 00000000000..267a41e3f32
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb
@@ -0,0 +1,347 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::PullRequestFormatter do
+ let(:client) { double }
+ let(:project) { create(:project, :repository) }
+ let(:source_sha) { create(:commit, project: project).id }
+ let(:target_commit) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit) }
+ let(:target_sha) { target_commit.id }
+ let(:target_short_sha) { target_commit.id.to_s[0..7] }
+ let(:repository) { double(id: 1, fork: false) }
+ let(:source_repo) { repository }
+ let(:source_branch) { double(ref: 'branch-merged', repo: source_repo, sha: source_sha) }
+ let(:forked_source_repo) { double(id: 2, fork: true, name: 'otherproject', full_name: 'company/otherproject') }
+ let(:target_repo) { repository }
+ let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha, user: octocat) }
+ let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) }
+ let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) }
+ let(:branch_deleted_repo) { double(ref: 'master', repo: nil, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) }
+ let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+ let(:base_data) do
+ {
+ number: 1347,
+ milestone: nil,
+ state: 'open',
+ title: 'New feature',
+ body: 'Please pull these awesome changes',
+ head: source_branch,
+ base: target_branch,
+ assignee: nil,
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ merged_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
+ }
+ end
+
+ subject(:pull_request) { described_class.new(project, raw_data, client) }
+
+ before do
+ allow(client).to receive(:user).and_return(octocat)
+ end
+
+ shared_examples 'Gitlab::LegacyGithubImport::PullRequestFormatter#attributes' do
+ context 'when pull request is open' do
+ let(:raw_data) { double(base_data.merge(state: 'open')) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ iid: 1347,
+ title: 'New feature',
+ description: "*Created by: octocat*\n\nPlease pull these awesome changes",
+ source_project: project,
+ source_branch: 'branch-merged',
+ source_branch_sha: source_sha,
+ target_project: project,
+ target_branch: 'master',
+ target_branch_sha: target_sha,
+ state: 'opened',
+ milestone: nil,
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ imported: true
+ }
+
+ expect(pull_request.attributes).to eq(expected)
+ end
+ end
+
+ context 'when pull request is closed' do
+ let(:raw_data) { double(base_data.merge(state: 'closed')) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ iid: 1347,
+ title: 'New feature',
+ description: "*Created by: octocat*\n\nPlease pull these awesome changes",
+ source_project: project,
+ source_branch: 'branch-merged',
+ source_branch_sha: source_sha,
+ target_project: project,
+ target_branch: 'master',
+ target_branch_sha: target_sha,
+ state: 'closed',
+ milestone: nil,
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ imported: true
+ }
+
+ expect(pull_request.attributes).to eq(expected)
+ end
+ end
+
+ context 'when pull request is merged' do
+ let(:merged_at) { DateTime.strptime('2011-01-28T13:01:12Z') }
+ let(:raw_data) { double(base_data.merge(state: 'closed', merged_at: merged_at)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ iid: 1347,
+ title: 'New feature',
+ description: "*Created by: octocat*\n\nPlease pull these awesome changes",
+ source_project: project,
+ source_branch: 'branch-merged',
+ source_branch_sha: source_sha,
+ target_project: project,
+ target_branch: 'master',
+ target_branch_sha: target_sha,
+ state: 'merged',
+ milestone: nil,
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ imported: true
+ }
+
+ expect(pull_request.attributes).to eq(expected)
+ end
+ end
+
+ context 'when it is assigned to someone' do
+ let(:raw_data) { double(base_data.merge(assignee: octocat)) }
+
+ it 'returns nil as assignee_id when is not a GitLab user' do
+ expect(pull_request.attributes.fetch(:assignee_id)).to be_nil
+ end
+
+ it 'returns GitLab user id associated with GitHub id as assignee_id' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id
+ end
+
+ it 'returns GitLab user id associated with GitHub email as assignee_id' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id
+ end
+ end
+
+ context 'when author is a GitLab user' do
+ let(:raw_data) { double(base_data.merge(user: octocat)) }
+
+ it 'returns project creator_id as author_id when is not a GitLab user' do
+ expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id
+ end
+
+ it 'returns GitLab user id associated with GitHub id as author_id' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+
+ it 'returns GitLab user id associated with GitHub email as author_id' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+
+ it 'returns description without created at tag line' do
+ create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(pull_request.attributes.fetch(:description)).to eq('Please pull these awesome changes')
+ end
+ end
+
+ context 'when it has a milestone' do
+ let(:milestone) { double(id: 42, number: 42) }
+ let(:raw_data) { double(base_data.merge(milestone: milestone)) }
+
+ it 'returns nil when milestone does not exist' do
+ expect(pull_request.attributes.fetch(:milestone)).to be_nil
+ end
+
+ it 'returns milestone when it exists' do
+ milestone = create(:milestone, project: project, iid: 42)
+
+ expect(pull_request.attributes.fetch(:milestone)).to eq milestone
+ end
+ end
+ end
+
+ shared_examples 'Gitlab::LegacyGithubImport::PullRequestFormatter#number' do
+ let(:raw_data) { double(base_data) }
+
+ it 'returns pull request number' do
+ expect(pull_request.number).to eq 1347
+ end
+ end
+
+ shared_examples 'Gitlab::LegacyGithubImport::PullRequestFormatter#source_branch_name' do
+ context 'when source branch exists' do
+ let(:raw_data) { double(base_data) }
+
+ it 'returns branch ref' do
+ expect(pull_request.source_branch_name).to eq 'branch-merged'
+ end
+ end
+
+ context 'when source branch does not exist' do
+ let(:raw_data) { double(base_data.merge(head: removed_branch)) }
+
+ it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do
+ expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/removed-branch"
+ end
+ end
+
+ context 'when source branch is from a fork' do
+ let(:raw_data) { double(base_data.merge(head: forked_branch)) }
+
+ it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do
+ expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/master"
+ end
+ end
+
+ context 'when source branch is from a deleted fork' do
+ let(:raw_data) { double(base_data.merge(head: branch_deleted_repo)) }
+
+ it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do
+ expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/master"
+ end
+ end
+ end
+
+ shared_examples 'Gitlab::LegacyGithubImport::PullRequestFormatter#target_branch_name' do
+ context 'when target branch exists' do
+ let(:raw_data) { double(base_data) }
+
+ it 'returns branch ref' do
+ expect(pull_request.target_branch_name).to eq 'master'
+ end
+ end
+
+ context 'when target branch does not exist' do
+ let(:raw_data) { double(base_data.merge(base: removed_branch)) }
+
+ it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do
+ expect(pull_request.target_branch_name).to eq 'gl-2e5d3239/1347/octocat/removed-branch'
+ end
+ end
+ end
+
+ context 'when importing a GitHub project' do
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#attributes'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#number'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#source_branch_name'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#target_branch_name'
+ end
+
+ context 'when importing a Gitea project' do
+ before do
+ project.update(import_type: 'gitea')
+ end
+
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#attributes'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#number'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#source_branch_name'
+ it_behaves_like 'Gitlab::LegacyGithubImport::PullRequestFormatter#target_branch_name'
+ end
+
+ describe '#valid?' do
+ context 'when source, and target repos are not a fork' do
+ let(:raw_data) { double(base_data) }
+
+ it 'returns true' do
+ expect(pull_request.valid?).to eq true
+ end
+ end
+
+ context 'when source repo is a fork' do
+ let(:source_repo) { double(id: 2) }
+ let(:raw_data) { double(base_data) }
+
+ it 'returns true' do
+ expect(pull_request.valid?).to eq true
+ end
+ end
+
+ context 'when target repo is a fork' do
+ let(:target_repo) { double(id: 2) }
+ let(:raw_data) { double(base_data) }
+
+ it 'returns true' do
+ expect(pull_request.valid?).to eq true
+ end
+ end
+ end
+
+ describe '#cross_project?' do
+ context 'when source and target repositories are different' do
+ let(:raw_data) { double(base_data.merge(head: forked_branch)) }
+
+ it 'returns true' do
+ expect(pull_request.cross_project?).to eq true
+ end
+ end
+
+ context 'when source repository does not exist anymore' do
+ let(:raw_data) { double(base_data.merge(head: branch_deleted_repo)) }
+
+ it 'returns true' do
+ expect(pull_request.cross_project?).to eq true
+ end
+ end
+
+ context 'when source and target repositories are the same' do
+ let(:raw_data) { double(base_data.merge(head: source_branch)) }
+
+ it 'returns false' do
+ expect(pull_request.cross_project?).to eq false
+ end
+ end
+ end
+
+ describe '#source_branch_exists?' do
+ let(:raw_data) { double(base_data.merge(head: forked_branch)) }
+
+ it 'returns false when is a cross_project' do
+ expect(pull_request.source_branch_exists?).to eq false
+ end
+ end
+
+ describe '#url' do
+ let(:raw_data) { double(base_data) }
+
+ it 'return raw url' do
+ expect(pull_request.url).to eq 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
+ end
+ end
+
+ describe '#opened?' do
+ let(:raw_data) { double(base_data.merge(state: 'open')) }
+
+ it 'returns true when state is "open"' do
+ expect(pull_request.opened?).to be_truthy
+ end
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
new file mode 100644
index 00000000000..082e3b36dd0
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::ReleaseFormatter do
+ let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
+ let(:octocat) { double(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+
+ let(:base_data) do
+ {
+ tag_name: 'v1.0.0',
+ name: 'First release',
+ draft: false,
+ created_at: created_at,
+ published_at: created_at,
+ body: 'Release v1.0.0'
+ }
+ end
+
+ subject(:release) { described_class.new(project, raw_data) }
+
+ describe '#attributes' do
+ let(:raw_data) { double(base_data) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ tag: 'v1.0.0',
+ description: 'Release v1.0.0',
+ created_at: created_at,
+ updated_at: created_at
+ }
+
+ expect(release.attributes).to eq(expected)
+ end
+ end
+
+ describe '#valid' do
+ context 'when release is not a draft' do
+ let(:raw_data) { double(base_data) }
+
+ it 'returns true' do
+ expect(release.valid?).to eq true
+ end
+ end
+
+ context 'when release is draft' do
+ let(:raw_data) { double(base_data.merge(draft: true)) }
+
+ it 'returns false' do
+ expect(release.valid?).to eq false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb
new file mode 100644
index 00000000000..3cd096eb0ad
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::UserFormatter do
+ let(:client) { double }
+ let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
+
+ subject(:user) { described_class.new(client, octocat) }
+
+ before do
+ allow(client).to receive(:user).and_return(octocat)
+ end
+
+ describe '#gitlab_id' do
+ context 'when GitHub user is a GitLab user' do
+ it 'return GitLab user id when user associated their account with GitHub' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(user.gitlab_id).to eq gl_user.id
+ end
+
+ it 'returns GitLab user id when user primary email matches GitHub email' do
+ gl_user = create(:user, email: octocat.email)
+
+ expect(user.gitlab_id).to eq gl_user.id
+ end
+
+ it 'returns GitLab user id when any of user linked emails matches GitHub email' do
+ gl_user = create(:user, email: 'johndoe@example.com')
+ create(:email, user: gl_user, email: octocat.email)
+
+ expect(user.gitlab_id).to eq gl_user.id
+ end
+ end
+
+ it 'returns nil when GitHub user is not a GitLab user' do
+ expect(user.gitlab_id).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb
new file mode 100644
index 00000000000..7723533aee2
--- /dev/null
+++ b/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::LegacyGithubImport::WikiFormatter do
+ let(:project) do
+ create(:project,
+ namespace: create(:namespace, path: 'gitlabhq'),
+ import_url: 'https://xxx@github.com/gitlabhq/sample.gitlabhq.git')
+ end
+
+ subject(:wiki) { described_class.new(project) }
+
+ describe '#disk_path' do
+ it 'appends .wiki to project path' do
+ expect(wiki.disk_path).to eq project.wiki.disk_path
+ end
+ end
+
+ describe '#import_url' do
+ it 'returns URL of the wiki repository' do
+ expect(wiki.import_url).to eq 'https://xxx@github.com/gitlabhq/sample.gitlabhq.wiki.git'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/background_transaction_spec.rb b/spec/lib/gitlab/metrics/background_transaction_spec.rb
new file mode 100644
index 00000000000..17445fe6de5
--- /dev/null
+++ b/spec/lib/gitlab/metrics/background_transaction_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::BackgroundTransaction do
+ let(:test_worker_class) { double(:class, name: 'TestWorker') }
+
+ subject { described_class.new(test_worker_class) }
+
+ describe '#action' do
+ it 'returns transaction action name' do
+ expect(subject.action).to eq('TestWorker#perform')
+ end
+ end
+
+ describe '#label' do
+ it 'returns labels based on class name' do
+ expect(subject.labels).to eq(controller: 'TestWorker', action: 'perform')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/influx_sampler_spec.rb b/spec/lib/gitlab/metrics/influx_sampler_spec.rb
deleted file mode 100644
index 999a9536d82..00000000000
--- a/spec/lib/gitlab/metrics/influx_sampler_spec.rb
+++ /dev/null
@@ -1,150 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Metrics::InfluxSampler do
- let(:sampler) { described_class.new(5) }
-
- after do
- Allocations.stop if Gitlab::Metrics.mri?
- end
-
- describe '#start' do
- it 'runs once and gathers a sample at a given interval' do
- expect(sampler).to receive(:sleep).with(a_kind_of(Numeric)).twice
- expect(sampler).to receive(:sample).once
- expect(sampler).to receive(:running).and_return(true, false)
-
- sampler.start.join
- end
- end
-
- describe '#sample' do
- it 'samples various statistics' do
- expect(sampler).to receive(:sample_memory_usage)
- expect(sampler).to receive(:sample_file_descriptors)
- expect(sampler).to receive(:sample_objects)
- expect(sampler).to receive(:sample_gc)
- expect(sampler).to receive(:flush)
-
- sampler.sample
- end
-
- it 'clears any GC profiles' do
- expect(sampler).to receive(:flush)
- expect(GC::Profiler).to receive(:clear)
-
- sampler.sample
- end
- end
-
- describe '#flush' do
- it 'schedules the metrics using Sidekiq' do
- expect(Gitlab::Metrics).to receive(:submit_metrics)
- .with([an_instance_of(Hash)])
-
- sampler.sample_memory_usage
- sampler.flush
- end
- end
-
- describe '#sample_memory_usage' do
- it 'adds a metric containing the memory usage' do
- expect(Gitlab::Metrics::System).to receive(:memory_usage)
- .and_return(9000)
-
- expect(sampler).to receive(:add_metric)
- .with(/memory_usage/, value: 9000)
- .and_call_original
-
- sampler.sample_memory_usage
- end
- end
-
- describe '#sample_file_descriptors' do
- it 'adds a metric containing the amount of open file descriptors' do
- expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
- .and_return(4)
-
- expect(sampler).to receive(:add_metric)
- .with(/file_descriptors/, value: 4)
- .and_call_original
-
- sampler.sample_file_descriptors
- end
- end
-
- if Gitlab::Metrics.mri?
- describe '#sample_objects' do
- it 'adds a metric containing the amount of allocated objects' do
- expect(sampler).to receive(:add_metric)
- .with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash))
- .at_least(:once)
- .and_call_original
-
- sampler.sample_objects
- end
-
- it 'ignores classes without a name' do
- expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
-
- expect(sampler).not_to receive(:add_metric)
- .with('object_counts', an_instance_of(Hash), type: nil)
-
- sampler.sample_objects
- end
- end
- end
-
- describe '#sample_gc' do
- it 'adds a metric containing garbage collection statistics' do
- expect(GC::Profiler).to receive(:total_time).and_return(0.24)
-
- expect(sampler).to receive(:add_metric)
- .with(/gc_statistics/, an_instance_of(Hash))
- .and_call_original
-
- sampler.sample_gc
- end
- end
-
- describe '#add_metric' do
- it 'prefixes the series name for a Rails process' do
- expect(sampler).to receive(:sidekiq?).and_return(false)
-
- expect(Gitlab::Metrics::Metric).to receive(:new)
- .with('rails_cats', { value: 10 }, {})
- .and_call_original
-
- sampler.add_metric('cats', value: 10)
- end
-
- it 'prefixes the series name for a Sidekiq process' do
- expect(sampler).to receive(:sidekiq?).and_return(true)
-
- expect(Gitlab::Metrics::Metric).to receive(:new)
- .with('sidekiq_cats', { value: 10 }, {})
- .and_call_original
-
- sampler.add_metric('cats', value: 10)
- end
- end
-
- describe '#sleep_interval' do
- it 'returns a Numeric' do
- expect(sampler.sleep_interval).to be_a_kind_of(Numeric)
- end
-
- # Testing random behaviour is very hard, so treat this test as a basic smoke
- # test instead of a very accurate behaviour/unit test.
- it 'does not return the same interval twice in a row' do
- last = nil
-
- 100.times do
- interval = sampler.sleep_interval
-
- expect(interval).not_to eq(last)
-
- last = interval
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index 4b19ee19103..977bc250049 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Gitlab::Metrics::Instrumentation do
- let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:env) { {} }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
before do
@dummy = Class.new do
diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb
index a247f03b2da..5341addf911 100644
--- a/spec/lib/gitlab/metrics/method_call_spec.rb
+++ b/spec/lib/gitlab/metrics/method_call_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Gitlab::Metrics::MethodCall do
- let(:method_call) { described_class.new('Foo#bar', 'foo') }
+ let(:transaction) { double(:transaction, labels: {}) }
+ let(:method_call) { described_class.new('Foo#bar', :Foo, '#bar', transaction) }
describe '#measure' do
it 'measures the performance of the supplied block' do
@@ -11,6 +12,54 @@ describe Gitlab::Metrics::MethodCall do
expect(method_call.cpu_time).to be_a_kind_of(Numeric)
expect(method_call.call_count).to eq(1)
end
+
+ context 'when measurement is above threshold' do
+ before do
+ allow(method_call).to receive(:above_threshold?).and_return(true)
+ end
+
+ context 'prometheus instrumentation is enabled' do
+ before do
+ Feature.get(:prometheus_metrics_method_instrumentation).enable
+ end
+
+ it 'observes the performance of the supplied block' do
+ expect(described_class.call_duration_histogram)
+ .to receive(:observe)
+ .with({ module: :Foo, method: '#bar' }, be_a_kind_of(Numeric))
+
+ method_call.measure { 'foo' }
+ end
+ end
+
+ context 'prometheus instrumentation is disabled' do
+ before do
+ Feature.get(:prometheus_metrics_method_instrumentation).disable
+ end
+
+ it 'does not observe the performance' do
+ expect(described_class.call_duration_histogram)
+ .not_to receive(:observe)
+
+ method_call.measure { 'foo' }
+ end
+ end
+ end
+
+ context 'when measurement is below threshold' do
+ before do
+ allow(method_call).to receive(:above_threshold?).and_return(false)
+
+ Feature.get(:prometheus_metrics_method_instrumentation).enable
+ end
+
+ it 'does not observe the performance' do
+ expect(described_class.call_duration_histogram)
+ .not_to receive(:observe)
+
+ method_call.measure { 'foo' }
+ end
+ end
end
describe '#to_metric' do
@@ -19,7 +68,7 @@ describe Gitlab::Metrics::MethodCall do
metric = method_call.to_metric
expect(metric).to be_an_instance_of(Gitlab::Metrics::Metric)
- expect(metric.series).to eq('foo')
+ expect(metric.series).to eq('rails_method_calls')
expect(metric.values[:duration]).to be_a_kind_of(Numeric)
expect(metric.values[:cpu_duration]).to be_a_kind_of(Numeric)
@@ -30,7 +79,13 @@ describe Gitlab::Metrics::MethodCall do
end
describe '#above_threshold?' do
+ before do
+ allow(Gitlab::Metrics).to receive(:method_call_threshold).and_return(100)
+ end
+
it 'returns false when the total call time is not above the threshold' do
+ expect(method_call).to receive(:real_time).and_return(9)
+
expect(method_call.above_threshold?).to eq(false)
end
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index ec415f2bd85..b84387204ee 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -18,34 +18,6 @@ describe Gitlab::Metrics::RackMiddleware do
expect(middleware.call(env)).to eq('yay')
end
- it 'tags a transaction with the name and action of a controller' do
- klass = double(:klass, name: 'TestController', content_type: 'text/html')
- controller = double(:controller, class: klass, action_name: 'show')
-
- env['action_controller.instance'] = controller
-
- allow(app).to receive(:call).with(env)
-
- expect(middleware).to receive(:tag_controller)
- .with(an_instance_of(Gitlab::Metrics::Transaction), env)
-
- middleware.call(env)
- end
-
- it 'tags a transaction with the method and path of the route in the grape endpoint' do
- route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)")
- endpoint = double(:endpoint, route: route)
-
- env['api.endpoint'] = endpoint
-
- allow(app).to receive(:call).with(env)
-
- expect(middleware).to receive(:tag_endpoint)
- .with(an_instance_of(Gitlab::Metrics::Transaction), env)
-
- middleware.call(env)
- end
-
it 'tracks any raised exceptions' do
expect(app).to receive(:call).with(env).and_raise(RuntimeError)
@@ -60,7 +32,7 @@ describe Gitlab::Metrics::RackMiddleware do
let(:transaction) { middleware.transaction_from_env(env) }
it 'returns a Transaction' do
- expect(transaction).to be_an_instance_of(Gitlab::Metrics::Transaction)
+ expect(transaction).to be_an_instance_of(Gitlab::Metrics::WebTransaction)
end
it 'stores the request method and URI in the transaction as values' do
@@ -84,58 +56,4 @@ describe Gitlab::Metrics::RackMiddleware do
end
end
end
-
- describe '#tag_controller' do
- let(:transaction) { middleware.transaction_from_env(env) }
- let(:content_type) { 'text/html' }
-
- before do
- klass = double(:klass, name: 'TestController')
- controller = double(:controller, class: klass, action_name: 'show', content_type: content_type)
-
- env['action_controller.instance'] = controller
- end
-
- it 'tags a transaction with the name and action of a controller' do
- middleware.tag_controller(transaction, env)
-
- expect(transaction.action).to eq('TestController#show')
- end
-
- context 'when the response content type is not :html' do
- let(:content_type) { 'application/json' }
-
- it 'appends the mime type to the transaction action' do
- middleware.tag_controller(transaction, env)
-
- expect(transaction.action).to eq('TestController#show.json')
- end
- end
- end
-
- describe '#tag_endpoint' do
- let(:transaction) { middleware.transaction_from_env(env) }
-
- it 'tags a transaction with the method and path of the route in the grape endpount' do
- route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)")
- endpoint = double(:endpoint, route: route)
-
- env['api.endpoint'] = endpoint
-
- middleware.tag_endpoint(transaction, env)
-
- expect(transaction.action).to eq('Grape#GET /projects/:id/archive')
- end
-
- it 'does not tag a transaction if route infos are missing' do
- endpoint = double(:endpoint)
- allow(endpoint).to receive(:route).and_raise
-
- env['api.endpoint'] = endpoint
-
- middleware.tag_endpoint(transaction, env)
-
- expect(transaction.action).to be_nil
- end
- end
end
diff --git a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
new file mode 100644
index 00000000000..667e4747897
--- /dev/null
+++ b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
@@ -0,0 +1,150 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Samplers::InfluxSampler do
+ let(:sampler) { described_class.new(5) }
+
+ after do
+ Allocations.stop if Gitlab::Metrics.mri?
+ end
+
+ describe '#start' do
+ it 'runs once and gathers a sample at a given interval' do
+ expect(sampler).to receive(:sleep).with(a_kind_of(Numeric)).twice
+ expect(sampler).to receive(:sample).once
+ expect(sampler).to receive(:running).and_return(true, false)
+
+ sampler.start.join
+ end
+ end
+
+ describe '#sample' do
+ it 'samples various statistics' do
+ expect(sampler).to receive(:sample_memory_usage)
+ expect(sampler).to receive(:sample_file_descriptors)
+ expect(sampler).to receive(:sample_objects)
+ expect(sampler).to receive(:sample_gc)
+ expect(sampler).to receive(:flush)
+
+ sampler.sample
+ end
+
+ it 'clears any GC profiles' do
+ expect(sampler).to receive(:flush)
+ expect(GC::Profiler).to receive(:clear)
+
+ sampler.sample
+ end
+ end
+
+ describe '#flush' do
+ it 'schedules the metrics using Sidekiq' do
+ expect(Gitlab::Metrics).to receive(:submit_metrics)
+ .with([an_instance_of(Hash)])
+
+ sampler.sample_memory_usage
+ sampler.flush
+ end
+ end
+
+ describe '#sample_memory_usage' do
+ it 'adds a metric containing the memory usage' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage)
+ .and_return(9000)
+
+ expect(sampler).to receive(:add_metric)
+ .with(/memory_usage/, value: 9000)
+ .and_call_original
+
+ sampler.sample_memory_usage
+ end
+ end
+
+ describe '#sample_file_descriptors' do
+ it 'adds a metric containing the amount of open file descriptors' do
+ expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
+ .and_return(4)
+
+ expect(sampler).to receive(:add_metric)
+ .with(/file_descriptors/, value: 4)
+ .and_call_original
+
+ sampler.sample_file_descriptors
+ end
+ end
+
+ if Gitlab::Metrics.mri?
+ describe '#sample_objects' do
+ it 'adds a metric containing the amount of allocated objects' do
+ expect(sampler).to receive(:add_metric)
+ .with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash))
+ .at_least(:once)
+ .and_call_original
+
+ sampler.sample_objects
+ end
+
+ it 'ignores classes without a name' do
+ expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
+
+ expect(sampler).not_to receive(:add_metric)
+ .with('object_counts', an_instance_of(Hash), type: nil)
+
+ sampler.sample_objects
+ end
+ end
+ end
+
+ describe '#sample_gc' do
+ it 'adds a metric containing garbage collection statistics' do
+ expect(GC::Profiler).to receive(:total_time).and_return(0.24)
+
+ expect(sampler).to receive(:add_metric)
+ .with(/gc_statistics/, an_instance_of(Hash))
+ .and_call_original
+
+ sampler.sample_gc
+ end
+ end
+
+ describe '#add_metric' do
+ it 'prefixes the series name for a Rails process' do
+ expect(sampler).to receive(:sidekiq?).and_return(false)
+
+ expect(Gitlab::Metrics::Metric).to receive(:new)
+ .with('rails_cats', { value: 10 }, {})
+ .and_call_original
+
+ sampler.add_metric('cats', value: 10)
+ end
+
+ it 'prefixes the series name for a Sidekiq process' do
+ expect(sampler).to receive(:sidekiq?).and_return(true)
+
+ expect(Gitlab::Metrics::Metric).to receive(:new)
+ .with('sidekiq_cats', { value: 10 }, {})
+ .and_call_original
+
+ sampler.add_metric('cats', value: 10)
+ end
+ end
+
+ describe '#sleep_interval' do
+ it 'returns a Numeric' do
+ expect(sampler.sleep_interval).to be_a_kind_of(Numeric)
+ end
+
+ # Testing random behaviour is very hard, so treat this test as a basic smoke
+ # test instead of a very accurate behaviour/unit test.
+ it 'does not return the same interval twice in a row' do
+ last = nil
+
+ 100.times do
+ interval = sampler.sleep_interval
+
+ expect(interval).not_to eq(last)
+
+ last = interval
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
new file mode 100644
index 00000000000..53699327da1
--- /dev/null
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Samplers::RubySampler do
+ let(:sampler) { described_class.new(5) }
+
+ after do
+ Allocations.stop if Gitlab::Metrics.mri?
+ end
+
+ describe '#sample' do
+ it 'samples various statistics' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage)
+ expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
+ expect(sampler).to receive(:sample_objects)
+ expect(sampler).to receive(:sample_gc)
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing the memory usage' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage)
+ .and_return(9000)
+
+ expect(sampler.metrics[:memory_usage]).to receive(:set)
+ .with({}, 9000)
+ .and_call_original
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing the amount of open file descriptors' do
+ expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
+ .and_return(4)
+
+ expect(sampler.metrics[:file_descriptors]).to receive(:set)
+ .with({}, 4)
+ .and_call_original
+
+ sampler.sample
+ end
+
+ it 'clears any GC profiles' do
+ expect(GC::Profiler).to receive(:clear)
+
+ sampler.sample
+ end
+ end
+
+ describe '#sample_gc' do
+ it 'adds a metric containing garbage collection time statistics' do
+ expect(GC::Profiler).to receive(:total_time).and_return(0.24)
+
+ expect(sampler.metrics[:total_time]).to receive(:set)
+ .with({}, 240)
+ .and_call_original
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing garbage collection statistics' do
+ GC.stat.keys.each do |key|
+ expect(sampler.metrics[key]).to receive(:set).with({}, anything).and_call_original
+ end
+
+ sampler.sample
+ end
+ end
+
+ if Gitlab::Metrics.mri?
+ describe '#sample_objects' do
+ it 'adds a metric containing the amount of allocated objects' do
+ expect(sampler.metrics[:objects_total]).to receive(:set)
+ .with(include(class: anything), be > 0)
+ .at_least(:once)
+ .and_call_original
+
+ sampler.sample
+ end
+
+ it 'ignores classes without a name' do
+ expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
+
+ expect(sampler.metrics[:objects_total]).not_to receive(:set)
+ .with(include(class: 'object_counts'), anything)
+
+ sampler.sample
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
new file mode 100644
index 00000000000..771b633a2b9
--- /dev/null
+++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Samplers::UnicornSampler do
+ subject { described_class.new(1.second) }
+
+ describe '#sample' do
+ let(:unicorn) { double('unicorn') }
+ let(:raindrops) { double('raindrops') }
+ let(:stats) { double('stats') }
+
+ before do
+ stub_const('Unicorn', unicorn)
+ stub_const('Raindrops::Linux', raindrops)
+ allow(raindrops).to receive(:unix_listener_stats).and_return({})
+ allow(raindrops).to receive(:tcp_listener_stats).and_return({})
+ end
+
+ context 'unicorn listens on unix sockets' do
+ let(:socket_address) { '/some/sock' }
+ let(:sockets) { [socket_address] }
+
+ before do
+ allow(unicorn).to receive(:listener_names).and_return(sockets)
+ end
+
+ it 'samples socket data' do
+ expect(raindrops).to receive(:unix_listener_stats).with(sockets)
+
+ subject.sample
+ end
+
+ context 'stats collected' do
+ before do
+ allow(stats).to receive(:active).and_return('active')
+ allow(stats).to receive(:queued).and_return('queued')
+ allow(raindrops).to receive(:unix_listener_stats).and_return({ socket_address => stats })
+ end
+
+ it 'updates metrics type unix and with addr' do
+ labels = { type: 'unix', address: socket_address }
+
+ expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
+ expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
+
+ subject.sample
+ end
+ end
+ end
+
+ context 'unicorn listens on tcp sockets' do
+ let(:tcp_socket_address) { '0.0.0.0:8080' }
+ let(:tcp_sockets) { [tcp_socket_address] }
+
+ before do
+ allow(unicorn).to receive(:listener_names).and_return(tcp_sockets)
+ end
+
+ it 'samples socket data' do
+ expect(raindrops).to receive(:tcp_listener_stats).with(tcp_sockets)
+
+ subject.sample
+ end
+
+ context 'stats collected' do
+ before do
+ allow(stats).to receive(:active).and_return('active')
+ allow(stats).to receive(:queued).and_return('queued')
+ allow(raindrops).to receive(:tcp_listener_stats).and_return({ tcp_socket_address => stats })
+ end
+
+ it 'updates metrics type unix and with addr' do
+ labels = { type: 'tcp', address: tcp_socket_address }
+
+ expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
+ expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
+
+ subject.sample
+ end
+ end
+ end
+ end
+
+ describe '#start' do
+ context 'when enabled' do
+ before do
+ allow(subject).to receive(:enabled?).and_return(true)
+ end
+
+ it 'creates new thread' do
+ expect(Thread).to receive(:new)
+
+ subject.start
+ end
+ end
+
+ context 'when disabled' do
+ before do
+ allow(subject).to receive(:enabled?).and_return(false)
+ end
+
+ it "doesn't create new thread" do
+ expect(Thread).not_to receive(:new)
+
+ subject.start
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
index b576d7173f5..ae1d8b47fe9 100644
--- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -8,8 +8,8 @@ describe Gitlab::Metrics::SidekiqMiddleware do
it 'tracks the transaction' do
worker = double(:worker, class: double(:class, name: 'TestWorker'))
- expect(Gitlab::Metrics::Transaction).to receive(:new)
- .with('TestWorker#perform')
+ expect(Gitlab::Metrics::BackgroundTransaction).to receive(:new)
+ .with(worker.class)
.and_call_original
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set)
@@ -23,8 +23,8 @@ describe Gitlab::Metrics::SidekiqMiddleware do
it 'tracks the transaction (for messages without `enqueued_at`)' do
worker = double(:worker, class: double(:class, name: 'TestWorker'))
- expect(Gitlab::Metrics::Transaction).to receive(:new)
- .with('TestWorker#perform')
+ expect(Gitlab::Metrics::BackgroundTransaction).to receive(:new)
+ .with(worker.class)
.and_call_original
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set)
diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
index e7b595405a8..eca75a4fac1 100644
--- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Gitlab::Metrics::Subscribers::ActionView do
- let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:env) { {} }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
let(:subscriber) { described_class.new }
@@ -29,5 +30,13 @@ describe Gitlab::Metrics::Subscribers::ActionView do
subscriber.render_template(event)
end
+
+ it 'observes view rendering time' do
+ expect(subscriber.send(:metric_view_rendering_duration_seconds))
+ .to receive(:observe)
+ .with({ view: 'app/views/x.html.haml' }, 2.1)
+
+ subscriber.render_template(event)
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index ce6587e993f..9b3698fb4a8 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -1,11 +1,12 @@
require 'spec_helper'
describe Gitlab::Metrics::Subscribers::ActiveRecord do
- let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:env) { {} }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
let(:subscriber) { described_class.new }
let(:event) do
- double(:event, duration: 0.2,
+ double(:event, duration: 2,
payload: { sql: 'SELECT * FROM users WHERE id = 10' })
end
@@ -20,16 +21,24 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do
end
describe 'with a current transaction' do
+ it 'observes sql_duration metric' do
+ expect(subscriber).to receive(:current_transaction)
+ .at_least(:once)
+ .and_return(transaction)
+ expect(subscriber.send(:metric_sql_duration_seconds)).to receive(:observe).with({}, 0.002)
+ subscriber.sql(event)
+ end
+
it 'increments the :sql_duration value' do
expect(subscriber).to receive(:current_transaction)
.at_least(:once)
.and_return(transaction)
expect(transaction).to receive(:increment)
- .with(:sql_duration, 0.2)
+ .with(:sql_duration, 2, false)
expect(transaction).to receive(:increment)
- .with(:sql_count, 1)
+ .with(:sql_count, 1, false)
subscriber.sql(event)
end
diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
index f04dc8dcc02..58e28592cf9 100644
--- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
@@ -1,15 +1,16 @@
require 'spec_helper'
describe Gitlab::Metrics::Subscribers::RailsCache do
- let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:env) { {} }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
let(:subscriber) { described_class.new }
let(:event) { double(:event, duration: 15.2) }
describe '#cache_read' do
it 'increments the cache_read duration' do
- expect(subscriber).to receive(:increment)
- .with(:cache_read, event.duration)
+ expect(subscriber).to receive(:observe)
+ .with(:read, event.duration)
subscriber.cache_read(event)
end
@@ -17,7 +18,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
context 'with a transaction' do
before do
allow(subscriber).to receive(:current_transaction)
- .and_return(transaction)
+ .and_return(transaction)
end
context 'with hit event' do
@@ -25,9 +26,9 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
it 'increments the cache_read_hit count' do
expect(transaction).to receive(:increment)
- .with(:cache_read_hit_count, 1)
+ .with(:cache_read_hit_count, 1, false)
expect(transaction).to receive(:increment)
- .with(any_args).at_least(1) # Other calls
+ .with(any_args).at_least(1) # Other calls
subscriber.cache_read(event)
end
@@ -37,7 +38,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
it 'does not increment cache read miss' do
expect(transaction).not_to receive(:increment)
- .with(:cache_read_hit_count, 1)
+ .with(:cache_read_hit_count, 1)
subscriber.cache_read(event)
end
@@ -49,9 +50,15 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
it 'increments the cache_read_miss count' do
expect(transaction).to receive(:increment)
- .with(:cache_read_miss_count, 1)
+ .with(:cache_read_miss_count, 1, false)
expect(transaction).to receive(:increment)
- .with(any_args).at_least(1) # Other calls
+ .with(any_args).at_least(1) # Other calls
+
+ subscriber.cache_read(event)
+ end
+
+ it 'increments the cache_read_miss total' do
+ expect(subscriber.send(:metric_cache_misses_total)).to receive(:increment).with({})
subscriber.cache_read(event)
end
@@ -61,7 +68,13 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
it 'does not increment cache read miss' do
expect(transaction).not_to receive(:increment)
- .with(:cache_read_miss_count, 1)
+ .with(:cache_read_miss_count, 1)
+
+ subscriber.cache_read(event)
+ end
+
+ it 'does not increment cache_read_miss total' do
+ expect(subscriber.send(:metric_cache_misses_total)).not_to receive(:increment).with({})
subscriber.cache_read(event)
end
@@ -71,27 +84,27 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
end
describe '#cache_write' do
- it 'increments the cache_write duration' do
- expect(subscriber).to receive(:increment)
- .with(:cache_write, event.duration)
+ it 'observes write duration' do
+ expect(subscriber).to receive(:observe)
+ .with(:write, event.duration)
subscriber.cache_write(event)
end
end
describe '#cache_delete' do
- it 'increments the cache_delete duration' do
- expect(subscriber).to receive(:increment)
- .with(:cache_delete, event.duration)
+ it 'observes delete duration' do
+ expect(subscriber).to receive(:observe)
+ .with(:delete, event.duration)
subscriber.cache_delete(event)
end
end
describe '#cache_exist?' do
- it 'increments the cache_exists duration' do
- expect(subscriber).to receive(:increment)
- .with(:cache_exists, event.duration)
+ it 'observes the exists duration' do
+ expect(subscriber).to receive(:observe)
+ .with(:exists, event.duration)
subscriber.cache_exist?(event)
end
@@ -109,12 +122,12 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
context 'with a transaction' do
before do
allow(subscriber).to receive(:current_transaction)
- .and_return(transaction)
+ .and_return(transaction)
end
it 'increments the cache_read_hit count' do
expect(transaction).to receive(:increment)
- .with(:cache_read_hit_count, 1)
+ .with(:cache_read_hit_count, 1)
subscriber.cache_fetch_hit(event)
end
@@ -133,47 +146,61 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
context 'with a transaction' do
before do
allow(subscriber).to receive(:current_transaction)
- .and_return(transaction)
+ .and_return(transaction)
end
it 'increments the cache_fetch_miss count' do
expect(transaction).to receive(:increment)
- .with(:cache_read_miss_count, 1)
+ .with(:cache_read_miss_count, 1)
+
+ subscriber.cache_generate(event)
+ end
+
+ it 'increments the cache_read_miss total' do
+ expect(subscriber.send(:metric_cache_misses_total)).to receive(:increment).with({})
subscriber.cache_generate(event)
end
end
end
- describe '#increment' do
+ describe '#observe' do
context 'without a transaction' do
it 'returns' do
expect(transaction).not_to receive(:increment)
- subscriber.increment(:foo, 15.2)
+ subscriber.observe(:foo, 15.2)
end
end
context 'with a transaction' do
before do
allow(subscriber).to receive(:current_transaction)
- .and_return(transaction)
+ .and_return(transaction)
end
it 'increments the total and specific cache duration' do
expect(transaction).to receive(:increment)
- .with(:cache_duration, event.duration)
+ .with(:cache_duration, event.duration, false)
expect(transaction).to receive(:increment)
- .with(:cache_count, 1)
+ .with(:cache_count, 1, false)
expect(transaction).to receive(:increment)
- .with(:cache_delete_duration, event.duration)
+ .with(:cache_delete_duration, event.duration, false)
expect(transaction).to receive(:increment)
- .with(:cache_delete_count, 1)
+ .with(:cache_delete_count, 1, false)
+
+ subscriber.observe(:delete, event.duration)
+ end
+
+ it 'observes cache metric' do
+ expect(subscriber.send(:metric_cache_operation_duration_seconds))
+ .to receive(:observe)
+ .with(transaction.labels.merge(operation: :delete), event.duration / 1000.0)
- subscriber.increment(:cache_delete, event.duration)
+ subscriber.observe(:delete, event.duration)
end
end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
deleted file mode 100644
index 3779af81512..00000000000
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ /dev/null
@@ -1,203 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Metrics::Transaction do
- let(:transaction) { described_class.new }
-
- describe '#duration' do
- it 'returns the duration of a transaction in seconds' do
- transaction.run { sleep(0.5) }
-
- expect(transaction.duration).to be >= 0.5
- end
- end
-
- describe '#allocated_memory' do
- it 'returns the allocated memory in bytes' do
- transaction.run { 'a' * 32 }
-
- expect(transaction.allocated_memory).to be_a_kind_of(Numeric)
- end
- end
-
- describe '#run' do
- it 'yields the supplied block' do
- expect { |b| transaction.run(&b) }.to yield_control
- end
-
- it 'stores the transaction in the current thread' do
- transaction.run do
- expect(Thread.current[described_class::THREAD_KEY]).to eq(transaction)
- end
- end
-
- it 'removes the transaction from the current thread upon completion' do
- transaction.run { }
-
- expect(Thread.current[described_class::THREAD_KEY]).to be_nil
- end
- end
-
- describe '#add_metric' do
- it 'adds a metric to the transaction' do
- expect(Gitlab::Metrics::Metric).to receive(:new)
- .with('rails_foo', { number: 10 }, {})
-
- transaction.add_metric('foo', number: 10)
- end
- end
-
- describe '#method_call_for' do
- it 'returns a MethodCall' do
- method = transaction.method_call_for('Foo#bar')
-
- expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall)
- end
- end
-
- describe '#increment' do
- it 'increments a counter' do
- transaction.increment(:time, 1)
- transaction.increment(:time, 2)
-
- values = { duration: 0.0, time: 3, allocated_memory: a_kind_of(Numeric) }
-
- expect(transaction).to receive(:add_metric)
- .with('transactions', values, {})
-
- transaction.track_self
- end
- end
-
- describe '#set' do
- it 'sets a value' do
- transaction.set(:number, 10)
-
- values = {
- duration: 0.0,
- number: 10,
- allocated_memory: a_kind_of(Numeric)
- }
-
- expect(transaction).to receive(:add_metric)
- .with('transactions', values, {})
-
- transaction.track_self
- end
- end
-
- describe '#add_tag' do
- it 'adds a tag' do
- transaction.add_tag(:foo, 'bar')
-
- expect(transaction.tags).to eq({ foo: 'bar' })
- end
- end
-
- describe '#finish' do
- it 'tracks the transaction details and submits them to Sidekiq' do
- expect(transaction).to receive(:track_self)
- expect(transaction).to receive(:submit)
-
- transaction.finish
- end
- end
-
- describe '#track_self' do
- it 'adds a metric for the transaction itself' do
- values = {
- duration: transaction.duration,
- allocated_memory: a_kind_of(Numeric)
- }
-
- expect(transaction).to receive(:add_metric)
- .with('transactions', values, {})
-
- transaction.track_self
- end
- end
-
- describe '#submit' do
- it 'submits the metrics to Sidekiq' do
- transaction.track_self
-
- expect(Gitlab::Metrics).to receive(:submit_metrics)
- .with([an_instance_of(Hash)])
-
- transaction.submit
- end
-
- it 'adds the action as a tag for every metric' do
- transaction.action = 'Foo#bar'
- transaction.track_self
-
- hash = {
- series: 'rails_transactions',
- tags: { action: 'Foo#bar' },
- values: { duration: 0.0, allocated_memory: a_kind_of(Numeric) },
- timestamp: a_kind_of(Integer)
- }
-
- expect(Gitlab::Metrics).to receive(:submit_metrics)
- .with([hash])
-
- transaction.submit
- end
-
- it 'does not add an action tag for events' do
- transaction.action = 'Foo#bar'
- transaction.add_event(:meow)
-
- hash = {
- series: 'events',
- tags: { event: :meow },
- values: { count: 1 },
- timestamp: a_kind_of(Integer)
- }
-
- expect(Gitlab::Metrics).to receive(:submit_metrics)
- .with([hash])
-
- transaction.submit
- end
- end
-
- describe '#add_event' do
- it 'adds a metric' do
- transaction.add_event(:meow)
-
- expect(transaction.metrics[0]).to be_an_instance_of(Gitlab::Metrics::Metric)
- end
-
- it "does not prefix the metric's series name" do
- transaction.add_event(:meow)
-
- metric = transaction.metrics[0]
-
- expect(metric.series).to eq(described_class::EVENT_SERIES)
- end
-
- it 'tracks a counter for every event' do
- transaction.add_event(:meow)
-
- metric = transaction.metrics[0]
-
- expect(metric.values).to eq(count: 1)
- end
-
- it 'tracks the event name' do
- transaction.add_event(:meow)
-
- metric = transaction.metrics[0]
-
- expect(metric.tags).to eq(event: :meow)
- end
-
- it 'allows tracking of custom tags' do
- transaction.add_event(:meow, animal: 'cat')
-
- metric = transaction.metrics[0]
-
- expect(metric.tags).to eq(event: :meow, animal: 'cat')
- end
- end
-end
diff --git a/spec/lib/gitlab/metrics/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/unicorn_sampler_spec.rb
deleted file mode 100644
index dc0d1f2e940..00000000000
--- a/spec/lib/gitlab/metrics/unicorn_sampler_spec.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Metrics::UnicornSampler do
- subject { described_class.new(1.second) }
-
- describe '#sample' do
- let(:unicorn) { double('unicorn') }
- let(:raindrops) { double('raindrops') }
- let(:stats) { double('stats') }
-
- before do
- stub_const('Unicorn', unicorn)
- stub_const('Raindrops::Linux', raindrops)
- allow(raindrops).to receive(:unix_listener_stats).and_return({})
- allow(raindrops).to receive(:tcp_listener_stats).and_return({})
- end
-
- context 'unicorn listens on unix sockets' do
- let(:socket_address) { '/some/sock' }
- let(:sockets) { [socket_address] }
-
- before do
- allow(unicorn).to receive(:listener_names).and_return(sockets)
- end
-
- it 'samples socket data' do
- expect(raindrops).to receive(:unix_listener_stats).with(sockets)
-
- subject.sample
- end
-
- context 'stats collected' do
- before do
- allow(stats).to receive(:active).and_return('active')
- allow(stats).to receive(:queued).and_return('queued')
- allow(raindrops).to receive(:unix_listener_stats).and_return({ socket_address => stats })
- end
-
- it 'updates metrics type unix and with addr' do
- labels = { type: 'unix', address: socket_address }
-
- expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
- expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
-
- subject.sample
- end
- end
- end
-
- context 'unicorn listens on tcp sockets' do
- let(:tcp_socket_address) { '0.0.0.0:8080' }
- let(:tcp_sockets) { [tcp_socket_address] }
-
- before do
- allow(unicorn).to receive(:listener_names).and_return(tcp_sockets)
- end
-
- it 'samples socket data' do
- expect(raindrops).to receive(:tcp_listener_stats).with(tcp_sockets)
-
- subject.sample
- end
-
- context 'stats collected' do
- before do
- allow(stats).to receive(:active).and_return('active')
- allow(stats).to receive(:queued).and_return('queued')
- allow(raindrops).to receive(:tcp_listener_stats).and_return({ tcp_socket_address => stats })
- end
-
- it 'updates metrics type unix and with addr' do
- labels = { type: 'tcp', address: tcp_socket_address }
-
- expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
- expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
-
- subject.sample
- end
- end
- end
- end
-
- describe '#start' do
- context 'when enabled' do
- before do
- allow(subject).to receive(:enabled?).and_return(true)
- end
-
- it 'creates new thread' do
- expect(Thread).to receive(:new)
-
- subject.start
- end
- end
-
- context 'when disabled' do
- before do
- allow(subject).to receive(:enabled?).and_return(false)
- end
-
- it "doesn't create new thread" do
- expect(Thread).not_to receive(:new)
-
- subject.start
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb
new file mode 100644
index 00000000000..1d162f53a13
--- /dev/null
+++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb
@@ -0,0 +1,252 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::WebTransaction do
+ let(:env) { {} }
+ let(:transaction) { described_class.new(env) }
+
+ describe '#duration' do
+ it 'returns the duration of a transaction in seconds' do
+ transaction.run { sleep(0.5) }
+
+ expect(transaction.duration).to be >= 0.5
+ end
+ end
+
+ describe '#allocated_memory' do
+ it 'returns the allocated memory in bytes' do
+ transaction.run { 'a' * 32 }
+
+ expect(transaction.allocated_memory).to be_a_kind_of(Numeric)
+ end
+ end
+
+ describe '#run' do
+ it 'yields the supplied block' do
+ expect { |b| transaction.run(&b) }.to yield_control
+ end
+
+ it 'stores the transaction in the current thread' do
+ transaction.run do
+ expect(Thread.current[described_class::THREAD_KEY]).to eq(transaction)
+ end
+ end
+
+ it 'removes the transaction from the current thread upon completion' do
+ transaction.run { }
+
+ expect(Thread.current[described_class::THREAD_KEY]).to be_nil
+ end
+ end
+
+ describe '#add_metric' do
+ it 'adds a metric to the transaction' do
+ expect(Gitlab::Metrics::Metric).to receive(:new)
+ .with('rails_foo', { number: 10 }, {})
+
+ transaction.add_metric('foo', number: 10)
+ end
+ end
+
+ describe '#method_call_for' do
+ it 'returns a MethodCall' do
+ method = transaction.method_call_for('Foo#bar', :Foo, '#bar')
+
+ expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall)
+ end
+ end
+
+ describe '#increment' do
+ it 'increments a counter' do
+ transaction.increment(:time, 1)
+ transaction.increment(:time, 2)
+
+ values = { duration: 0.0, time: 3, allocated_memory: a_kind_of(Numeric) }
+
+ expect(transaction).to receive(:add_metric)
+ .with('transactions', values, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#set' do
+ it 'sets a value' do
+ transaction.set(:number, 10)
+
+ values = {
+ duration: 0.0,
+ number: 10,
+ allocated_memory: a_kind_of(Numeric)
+ }
+
+ expect(transaction).to receive(:add_metric)
+ .with('transactions', values, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#finish' do
+ it 'tracks the transaction details and submits them to Sidekiq' do
+ expect(transaction).to receive(:track_self)
+ expect(transaction).to receive(:submit)
+
+ transaction.finish
+ end
+ end
+
+ describe '#track_self' do
+ it 'adds a metric for the transaction itself' do
+ values = {
+ duration: transaction.duration,
+ allocated_memory: a_kind_of(Numeric)
+ }
+
+ expect(transaction).to receive(:add_metric)
+ .with('transactions', values, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#submit' do
+ it 'submits the metrics to Sidekiq' do
+ transaction.track_self
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics)
+ .with([an_instance_of(Hash)])
+
+ transaction.submit
+ end
+
+ it 'adds the action as a tag for every metric' do
+ allow(transaction).to receive(:labels).and_return(controller: 'Foo', action: 'bar')
+ transaction.track_self
+
+ hash = {
+ series: 'rails_transactions',
+ tags: { action: 'Foo#bar' },
+ values: { duration: 0.0, allocated_memory: a_kind_of(Numeric) },
+ timestamp: a_kind_of(Integer)
+ }
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics)
+ .with([hash])
+
+ transaction.submit
+ end
+
+ it 'does not add an action tag for events' do
+ allow(transaction).to receive(:labels).and_return(controller: 'Foo', action: 'bar')
+
+ transaction.add_event(:meow)
+
+ hash = {
+ series: 'events',
+ tags: { event: :meow },
+ values: { count: 1 },
+ timestamp: a_kind_of(Integer)
+ }
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics)
+ .with([hash])
+
+ transaction.submit
+ end
+ end
+
+ describe '#labels' do
+ context 'when request goes to Grape endpoint' do
+ before do
+ route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)')
+ endpoint = double(:endpoint, route: route)
+
+ env['api.endpoint'] = endpoint
+ end
+ it 'provides labels with the method and path of the route in the grape endpoint' do
+ expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive' })
+ expect(transaction.action).to eq('Grape#GET /projects/:id/archive')
+ end
+
+ it 'does not provide labels if route infos are missing' do
+ endpoint = double(:endpoint)
+ allow(endpoint).to receive(:route).and_raise
+
+ env['api.endpoint'] = endpoint
+
+ expect(transaction.labels).to eq({})
+ expect(transaction.action).to be_nil
+ end
+ end
+
+ context 'when request goes to ActionController' do
+ let(:content_type) { 'text/html' }
+
+ before do
+ klass = double(:klass, name: 'TestController')
+ controller = double(:controller, class: klass, action_name: 'show', content_type: content_type)
+
+ env['action_controller.instance'] = controller
+ end
+
+ it 'tags a transaction with the name and action of a controller' do
+ expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' })
+ expect(transaction.action).to eq('TestController#show')
+ end
+
+ context 'when the response content type is not :html' do
+ let(:content_type) { 'application/json' }
+
+ it 'appends the mime type to the transaction action' do
+ expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json' })
+ expect(transaction.action).to eq('TestController#show.json')
+ end
+ end
+ end
+
+ it 'returns no labels when no route information is present in env' do
+ expect(transaction.labels).to eq({})
+ expect(transaction.action).to eq(nil)
+ end
+ end
+
+ describe '#add_event' do
+ it 'adds a metric' do
+ transaction.add_event(:meow)
+
+ expect(transaction.metrics[0]).to be_an_instance_of(Gitlab::Metrics::Metric)
+ end
+
+ it "does not prefix the metric's series name" do
+ transaction.add_event(:meow)
+
+ metric = transaction.metrics[0]
+
+ expect(metric.series).to eq(described_class::EVENT_SERIES)
+ end
+
+ it 'tracks a counter for every event' do
+ transaction.add_event(:meow)
+
+ metric = transaction.metrics[0]
+
+ expect(metric.values).to eq(count: 1)
+ end
+
+ it 'tracks the event name' do
+ transaction.add_event(:meow)
+
+ metric = transaction.metrics[0]
+
+ expect(metric.tags).to eq(event: :meow)
+ end
+
+ it 'allows tracking of custom tags' do
+ transaction.add_event(:meow, animal: 'cat')
+
+ metric = transaction.metrics[0]
+
+ expect(metric.tags).to eq(event: :meow, animal: 'cat')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 599b8807d8d..1619fbd88b1 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -115,7 +115,7 @@ describe Gitlab::Metrics do
end
context 'with a transaction' do
- let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new({}) }
before do
allow(described_class).to receive(:current_transaction)
@@ -124,13 +124,13 @@ describe Gitlab::Metrics do
it 'adds a metric to the current transaction' do
expect(transaction).to receive(:increment)
- .with('foo_real_time', a_kind_of(Numeric))
+ .with('foo_real_time', a_kind_of(Numeric), false)
expect(transaction).to receive(:increment)
- .with('foo_cpu_time', a_kind_of(Numeric))
+ .with('foo_cpu_time', a_kind_of(Numeric), false)
expect(transaction).to receive(:increment)
- .with('foo_call_count', 1)
+ .with('foo_call_count', 1, false)
described_class.measure(:foo) { 10 }
end
@@ -143,31 +143,6 @@ describe Gitlab::Metrics do
end
end
- describe '.tag_transaction' do
- context 'without a transaction' do
- it 'does nothing' do
- expect_any_instance_of(Gitlab::Metrics::Transaction)
- .not_to receive(:add_tag)
-
- described_class.tag_transaction(:foo, 'bar')
- end
- end
-
- context 'with a transaction' do
- let(:transaction) { Gitlab::Metrics::Transaction.new }
-
- it 'adds the tag to the transaction' do
- expect(described_class).to receive(:current_transaction)
- .and_return(transaction)
-
- expect(transaction).to receive(:add_tag)
- .with(:foo, 'bar')
-
- described_class.tag_transaction(:foo, 'bar')
- end
- end
- end
-
describe '.action=' do
context 'without a transaction' do
it 'does nothing' do
@@ -180,7 +155,7 @@ describe Gitlab::Metrics do
context 'with a transaction' do
it 'sets the action of a transaction' do
- trans = Gitlab::Metrics::Transaction.new
+ trans = Gitlab::Metrics::WebTransaction.new({})
expect(described_class).to receive(:current_transaction)
.and_return(trans)
@@ -210,7 +185,7 @@ describe Gitlab::Metrics do
context 'with a transaction' do
it 'adds an event' do
- transaction = Gitlab::Metrics::Transaction.new
+ transaction = Gitlab::Metrics::WebTransaction.new({})
expect(transaction).to receive(:add_event).with(:meow)
@@ -224,7 +199,7 @@ describe Gitlab::Metrics do
shared_examples 'prometheus metrics API' do
describe '#counter' do
- subject { described_class.counter(:couter, 'doc') }
+ subject { described_class.counter(:counter, 'doc') }
describe '#increment' do
it 'successfully calls #increment without arguments' do
@@ -280,7 +255,7 @@ describe Gitlab::Metrics do
it_behaves_like 'prometheus metrics API'
describe '#null_metric' do
- subject { described_class.provide_metric(:test) }
+ subject { described_class.send(:provide_metric, :test) }
it { is_expected.to be_a(Gitlab::Metrics::NullMetric) }
end
@@ -321,7 +296,7 @@ describe Gitlab::Metrics do
it_behaves_like 'prometheus metrics API'
describe '#null_metric' do
- subject { described_class.provide_metric(:test) }
+ subject { described_class.send(:provide_metric, :test) }
it { is_expected.to be_nil }
end
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index cab662819ac..60a134be939 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -17,89 +17,123 @@ describe Gitlab::Middleware::Go do
describe 'when go-get=1' do
let(:current_user) { nil }
- context 'with simple 2-segment project path' do
- let!(:project) { create(:project, :private) }
+ shared_examples 'go-get=1' do |enabled_protocol:|
+ context 'with simple 2-segment project path' do
+ let!(:project) { create(:project, :private) }
- context 'with subpackages' do
- let(:path) { "#{project.full_path}/subpackage" }
+ context 'with subpackages' do
+ let(:path) { "#{project.full_path}/subpackage" }
- it 'returns the full project path' do
- expect_response_with_path(go, project.full_path)
- end
- end
-
- context 'without subpackages' do
- let(:path) { project.full_path }
-
- it 'returns the full project path' do
- expect_response_with_path(go, project.full_path)
+ it 'returns the full project path' do
+ expect_response_with_path(go, enabled_protocol, project.full_path)
+ end
end
- end
- end
- context 'with a nested project path' do
- let(:group) { create(:group, :nested) }
- let!(:project) { create(:project, :public, namespace: group) }
+ context 'without subpackages' do
+ let(:path) { project.full_path }
- shared_examples 'a nested project' do
- context 'when the project is public' do
it 'returns the full project path' do
- expect_response_with_path(go, project.full_path)
+ expect_response_with_path(go, enabled_protocol, project.full_path)
end
end
+ end
- context 'when the project is private' do
- before do
- project.update_attribute(:visibility_level, Project::PRIVATE)
- end
+ context 'with a nested project path' do
+ let(:group) { create(:group, :nested) }
+ let!(:project) { create(:project, :public, namespace: group) }
- context 'with access to the project' do
- let(:current_user) { project.creator }
+ shared_examples 'a nested project' do
+ context 'when the project is public' do
+ it 'returns the full project path' do
+ expect_response_with_path(go, enabled_protocol, project.full_path)
+ end
+ end
+ context 'when the project is private' do
before do
- project.team.add_master(current_user)
+ project.update_attribute(:visibility_level, Project::PRIVATE)
end
- it 'returns the full project path' do
- expect_response_with_path(go, project.full_path)
+ context 'with access to the project' do
+ let(:current_user) { project.creator }
+
+ before do
+ project.team.add_master(current_user)
+ end
+
+ it 'returns the full project path' do
+ expect_response_with_path(go, enabled_protocol, project.full_path)
+ end
end
- end
- context 'without access to the project' do
- it 'returns the 2-segment group path' do
- expect_response_with_path(go, group.full_path)
+ context 'without access to the project' do
+ it 'returns the 2-segment group path' do
+ expect_response_with_path(go, enabled_protocol, group.full_path)
+ end
end
end
end
- end
- context 'with subpackages' do
- let(:path) { "#{project.full_path}/subpackage" }
+ context 'with subpackages' do
+ let(:path) { "#{project.full_path}/subpackage" }
- it_behaves_like 'a nested project'
+ it_behaves_like 'a nested project'
+ end
+
+ context 'with a subpackage that is not a valid project path' do
+ let(:path) { "#{project.full_path}/---subpackage" }
+
+ it_behaves_like 'a nested project'
+ end
+
+ context 'without subpackages' do
+ let(:path) { project.full_path }
+
+ it_behaves_like 'a nested project'
+ end
end
- context 'with a subpackage that is not a valid project path' do
- let(:path) { "#{project.full_path}/---subpackage" }
+ context 'with a bogus path' do
+ let(:path) { "http:;url=http:&sol;&sol;www.example.com'http-equiv='refresh'x='?go-get=1" }
- it_behaves_like 'a nested project'
+ it 'skips go-import generation' do
+ expect(app).to receive(:call).and_return('no-go')
+
+ go
+ end
+ end
+ end
+
+ context 'with SSH disabled' do
+ before do
+ stub_application_setting(enabled_git_access_protocol: 'http')
end
- context 'without subpackages' do
- let(:path) { project.full_path }
+ include_examples 'go-get=1', enabled_protocol: :http
+ end
- it_behaves_like 'a nested project'
+ context 'with HTTP disabled' do
+ before do
+ stub_application_setting(enabled_git_access_protocol: 'ssh')
end
+
+ include_examples 'go-get=1', enabled_protocol: :ssh
end
- context 'with a bogus path' do
- let(:path) { "http:;url=http:&sol;&sol;www.example.com'http-equiv='refresh'x='?go-get=1" }
+ context 'with nothing disabled' do
+ before do
+ stub_application_setting(enabled_git_access_protocol: nil)
+ end
- it 'skips go-import generation' do
- expect(app).to receive(:call).and_return('no-go')
+ include_examples 'go-get=1', enabled_protocol: nil
+ end
- go
+ context 'with nothing disabled (blank string)' do
+ before do
+ stub_application_setting(enabled_git_access_protocol: '')
end
+
+ include_examples 'go-get=1', enabled_protocol: nil
end
end
@@ -113,10 +147,16 @@ describe Gitlab::Middleware::Go do
middleware.call(env)
end
- def expect_response_with_path(response, path)
+ def expect_response_with_path(response, protocol, path)
+ repository_url = case protocol
+ when :ssh
+ "ssh://git@#{Gitlab.config.gitlab.host}/#{path}.git"
+ when :http, nil
+ "http://#{Gitlab.config.gitlab.host}/#{path}.git"
+ end
expect(response[0]).to eq(200)
expect(response[1]['Content-Type']).to eq('text/html')
- expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git" /></head></html>}
+ expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}" /></head></html>}
expect(response[2].body).to eq([expected_body])
end
end
diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
index 88107536c9e..14f2c3cb86f 100644
--- a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
+++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Middleware::RailsQueueDuration do
let(:app) { double(:app) }
let(:middleware) { described_class.new(app) }
let(:env) { {} }
- let(:transaction) { double(:transaction) }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
before do
expect(app).to receive(:call).with(env).and_return('yay')
@@ -30,6 +30,16 @@ describe Gitlab::Middleware::RailsQueueDuration do
expect(transaction).to receive(:set).with(:rails_queue_duration, an_instance_of(Float))
expect(middleware.call(env)).to eq('yay')
end
+
+ it 'observes rails queue duration metrics and calls the app when the header is present' do
+ env['HTTP_GITLAB_WORKHORSE_PROXY_START'] = '2000000000'
+
+ expect(middleware.send(:metric_rails_queue_duration_seconds)).to receive(:observe).with(transaction.labels, 1)
+
+ Timecop.freeze(Time.at(3)) do
+ expect(middleware.call(env)).to eq('yay')
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb
index 742a792a1af..07ba11b93a3 100644
--- a/spec/lib/gitlab/middleware/read_only_spec.rb
+++ b/spec/lib/gitlab/middleware/read_only_spec.rb
@@ -83,15 +83,24 @@ describe Gitlab::Middleware::ReadOnly do
expect(subject).to disallow_request
end
- context 'whitelisted requests' do
- it 'expects DELETE request to logout to be allowed' do
- response = request.delete('/users/sign_out')
+ it 'expects POST of new file that looks like an LFS batch url to be disallowed' do
+ expect(Rails.application.routes).to receive(:recognize_path).and_call_original
+ response = request.post('/root/gitlab-ce/new/master/app/info/lfs/objects/batch')
- expect(response).not_to be_a_redirect
- expect(subject).not_to disallow_request
- end
+ expect(response).to be_a_redirect
+ expect(subject).to disallow_request
+ end
+ it 'returns last_vistited_url for disallowed request' do
+ response = request.post('/test_request')
+
+ expect(response.location).to eq 'http://localhost/'
+ end
+
+ context 'whitelisted requests' do
it 'expects a POST internal request to be allowed' do
+ expect(Rails.application.routes).not_to receive(:recognize_path)
+
response = request.post("/api/#{API::API.version}/internal")
expect(response).not_to be_a_redirect
@@ -99,11 +108,32 @@ describe Gitlab::Middleware::ReadOnly do
end
it 'expects a POST LFS request to batch URL to be allowed' do
+ expect(Rails.application.routes).to receive(:recognize_path).and_call_original
response = request.post('/root/rouge.git/info/lfs/objects/batch')
expect(response).not_to be_a_redirect
expect(subject).not_to disallow_request
end
+
+ it 'expects a POST request to git-upload-pack URL to be allowed' do
+ expect(Rails.application.routes).to receive(:recognize_path).and_call_original
+ response = request.post('/root/rouge.git/git-upload-pack')
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
+
+ it 'expects requests to sidekiq admin to be allowed' do
+ response = request.post('/admin/sidekiq')
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+
+ response = request.get('/admin/sidekiq')
+
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
end
end
diff --git a/spec/lib/gitlab/multi_collection_paginator_spec.rb b/spec/lib/gitlab/multi_collection_paginator_spec.rb
new file mode 100644
index 00000000000..68bd4f93159
--- /dev/null
+++ b/spec/lib/gitlab/multi_collection_paginator_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::MultiCollectionPaginator do
+ subject(:paginator) { described_class.new(Project.all.order(:id), Group.all.order(:id), per_page: 3) }
+
+ it 'combines both collections' do
+ project = create(:project)
+ group = create(:group)
+
+ expect(paginator.paginate(1)).to eq([project, group])
+ end
+
+ it 'includes elements second collection if first collection is empty' do
+ group = create(:group)
+
+ expect(paginator.paginate(1)).to eq([group])
+ end
+
+ context 'with a full first page' do
+ let!(:all_groups) { create_list(:group, 4) }
+ let!(:all_projects) { create_list(:project, 4) }
+
+ it 'knows the total count of the collection' do
+ expect(paginator.total_count).to eq(8)
+ end
+
+ it 'fills the first page with elements of the first collection' do
+ expect(paginator.paginate(1)).to eq(all_projects.take(3))
+ end
+
+ it 'fils the second page with a mixture of of the first & second collection' do
+ first_collection_element = all_projects.last
+ second_collection_elements = all_groups.take(2)
+
+ expected_collection = [first_collection_element] + second_collection_elements
+
+ expect(paginator.paginate(2)).to eq(expected_collection)
+ end
+
+ it 'fils the last page with elements from the second collection' do
+ expected_collection = all_groups[-2..-1]
+
+ expect(paginator.paginate(3)).to eq(expected_collection)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index db26e16e3b2..2f19fb7312d 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::OAuth::User do
let(:oauth_user) { described_class.new(auth_hash) }
let(:gl_user) { oauth_user.gl_user }
let(:uid) { 'my-uid' }
- let(:dn) { 'uid=user1,ou=People,dc=example' }
+ let(:dn) { 'uid=user1,ou=people,dc=example' }
let(:provider) { 'my-provider' }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) }
let(:info_hash) do
@@ -662,4 +662,13 @@ describe Gitlab::OAuth::User do
end
end
end
+
+ describe '.find_by_uid_and_provider' do
+ let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
+
+ it 'normalizes extern_uid' do
+ allow(oauth_user.auth_hash).to receive(:uid).and_return('MY-UID')
+ expect(oauth_user.find_user).to eql gl_user
+ end
+ end
end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 1f1c48ee9b5..0ae90069b7f 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -45,21 +45,16 @@ describe Gitlab::PathRegex do
Found new routes that could cause conflicts with existing namespaced routes
for groups or projects.
- Add <#{missing_words.join(', ')}> to `Gitlab::PathRegex::#{constant_name}
- to make sure no projects or namespaces can be created with those paths.
-
- To rename any existing records with those paths you can use the
- `Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}`
- migration helper.
-
- Make sure to make a note of the renamed records in the release blog post.
+ Nest <#{missing_words.join(', ')}> in a route containing `-`, that way
+ we know there will be no conflicts with groups or projects created with those
+ paths.
MISSING
end
if additional_words.any?
message += <<-ADDITIONAL
- Why are <#{additional_words.join(', ')}> in `#{constant_name}`?
+ Is <#{additional_words.join(', ')}> in `#{constant_name}` required?
If they are really required, update these specs to reflect that.
ADDITIONAL
@@ -68,14 +63,27 @@ describe Gitlab::PathRegex do
message
end
- let(:all_routes) do
+ let(:all_non_legacy_routes) do
route_set = Rails.application.routes
routes_collection = route_set.routes
routes_array = routes_collection.routes
- routes_array.map { |route| route.path.spec.to_s }
+
+ non_legacy_routes = routes_array.reject do |route|
+ route.name.to_s =~ /legacy_(\w*)_redirect/
+ end
+
+ non_deprecated_redirect_routes = non_legacy_routes.reject do |route|
+ app = route.app
+ # `app.app` is either another app, or `self`. We want to find the final app.
+ app = app.app while app.try(:app) && app.app != app
+
+ app.is_a?(ActionDispatch::Routing::PathRedirect) && app.block.include?('/-/')
+ end
+
+ non_deprecated_redirect_routes.map { |route| route.path.spec.to_s }
end
- let(:routes_without_format) { all_routes.map { |path| without_format(path) } }
+ let(:routes_without_format) { all_non_legacy_routes.map { |path| without_format(path) } }
# Routes not starting with `/:` or `/*`
# all routes not starting with a param
@@ -144,16 +152,7 @@ describe Gitlab::PathRegex do
let(:paths_after_group_id) do
group_routes.map do |route|
route.gsub(STARTING_WITH_GROUP, '').split('/').first
- end.uniq + ee_paths_after_group_id
- end
-
- let(:ee_paths_after_group_id) do
- %w(analytics
- ldap
- ldap_group_links
- notification_setting
- audit_events
- pipeline_quota hooks)
+ end.uniq
end
describe 'TOP_LEVEL_ROUTES' do
@@ -212,8 +211,6 @@ describe Gitlab::PathRegex do
it 'accepts group routes' do
expect(subject).to match('activity/')
- expect(subject).to match('group_members/')
- expect(subject).to match('subgroups/')
end
it 'is not case sensitive' do
@@ -245,8 +242,6 @@ describe Gitlab::PathRegex do
it 'accepts group routes' do
expect(subject).to match('activity/')
- expect(subject).to match('group_members/')
- expect(subject).to match('subgroups/')
end
end
@@ -267,8 +262,6 @@ describe Gitlab::PathRegex do
it 'accepts group routes' do
expect(subject).to match('activity/more/')
- expect(subject).to match('group_members/more/')
- expect(subject).to match('subgroups/more/')
end
end
end
@@ -290,9 +283,7 @@ describe Gitlab::PathRegex do
end
it 'rejects group routes' do
- expect(subject).not_to match('root/activity/')
- expect(subject).not_to match('root/group_members/')
- expect(subject).not_to match('root/subgroups/')
+ expect(subject).not_to match('root/-/')
end
end
@@ -312,9 +303,7 @@ describe Gitlab::PathRegex do
end
it 'rejects group routes' do
- expect(subject).not_to match('root/activity/more/')
- expect(subject).not_to match('root/group_members/more/')
- expect(subject).not_to match('root/subgroups/more/')
+ expect(subject).not_to match('root/-/')
end
end
end
@@ -347,9 +336,7 @@ describe Gitlab::PathRegex do
end
it 'accepts group routes' do
- expect(subject).to match('activity/')
- expect(subject).to match('group_members/')
- expect(subject).to match('subgroups/')
+ expect(subject).to match('analytics/')
end
it 'is not case sensitive' do
@@ -380,9 +367,7 @@ describe Gitlab::PathRegex do
end
it 'accepts group routes' do
- expect(subject).to match('root/activity/')
- expect(subject).to match('root/group_members/')
- expect(subject).to match('root/subgroups/')
+ expect(subject).to match('root/analytics/')
end
it 'is not case sensitive' do
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 9c3e7d7e9ba..a424f0f5cfe 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -70,6 +70,15 @@ describe Gitlab::ProjectSearchResults do
subject { described_class.parse_search_result(search_result) }
+ it 'can correctly parse filenames including ":"' do
+ special_char_result = "\nmaster:testdata/project::function1.yaml-1----\nmaster:testdata/project::function1.yaml:2:test: data1\n"
+
+ blob = described_class.parse_search_result(special_char_result)
+
+ expect(blob.ref).to eq('master')
+ expect(blob.filename).to eq('testdata/project::function1.yaml')
+ end
+
it "returns a valid FoundBlob" do
is_expected.to be_an Gitlab::SearchResults::FoundBlob
expect(subject.id).to be_nil
diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb
index d19bd611919..57b0ef8d1ad 100644
--- a/spec/lib/gitlab/project_template_spec.rb
+++ b/spec/lib/gitlab/project_template_spec.rb
@@ -4,9 +4,9 @@ describe Gitlab::ProjectTemplate do
describe '.all' do
it 'returns a all templates' do
expected = [
- described_class.new('rails', 'Ruby on Rails'),
- described_class.new('spring', 'Spring'),
- described_class.new('express', 'NodeJS Express')
+ described_class.new('rails', 'Ruby on Rails', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/rails'),
+ described_class.new('spring', 'Spring', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/spring'),
+ described_class.new('express', 'NodeJS Express', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/express')
]
expect(described_class.all).to be_an(Array)
@@ -31,7 +31,7 @@ describe Gitlab::ProjectTemplate do
end
describe 'instance methods' do
- subject { described_class.new('phoenix', 'Phoenix Framework') }
+ subject { described_class.new('phoenix', 'Phoenix Framework', 'Phoenix description', 'link-to-template') }
it { is_expected.to respond_to(:logo, :file, :archive_path) }
end
diff --git a/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb b/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb
new file mode 100644
index 00000000000..8b58f0b3725
--- /dev/null
+++ b/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb
@@ -0,0 +1,81 @@
+require 'spec_helper'
+
+describe Gitlab::QuickActions::SpendTimeAndDateSeparator do
+ subject { described_class }
+
+ shared_examples 'arg line with invalid parameters' do
+ it 'return nil' do
+ expect(subject.new(invalid_arg).execute).to eq(nil)
+ end
+ end
+
+ shared_examples 'arg line with valid parameters' do
+ it 'return time and date array' do
+ expect(subject.new(valid_arg).execute).to eq(expected_response)
+ end
+ end
+
+ describe '#execute' do
+ context 'invalid paramenter in arg line' do
+ context 'empty arg line' do
+ it_behaves_like 'arg line with invalid parameters' do
+ let(:invalid_arg) { '' }
+ end
+ end
+
+ context 'future date in arg line' do
+ it_behaves_like 'arg line with invalid parameters' do
+ let(:invalid_arg) { '10m 6023-02-02' }
+ end
+ end
+
+ context 'unparseable date(invalid mixes of delimiters)' do
+ it_behaves_like 'arg line with invalid parameters' do
+ let(:invalid_arg) { '10m 2017.02-02' }
+ end
+ end
+
+ context 'trash in arg line' do
+ let(:invalid_arg) { 'dfjkghdskjfghdjskfgdfg' }
+
+ it 'return nil as time value' do
+ time_date_response = subject.new(invalid_arg).execute
+
+ expect(time_date_response).to be_an_instance_of(Array)
+ expect(time_date_response.first).to eq(nil)
+ end
+ end
+ end
+
+ context 'only time present in arg line' do
+ it_behaves_like 'arg line with valid parameters' do
+ let(:valid_arg) { '2m 3m 5m 1h' }
+ let(:time) { Gitlab::TimeTrackingFormatter.parse(valid_arg) }
+ let(:date) { DateTime.now.to_date }
+ let(:expected_response) { [time, date] }
+ end
+ end
+
+ context 'simple time with date in arg line' do
+ it_behaves_like 'arg line with valid parameters' do
+ let(:raw_time) { '10m' }
+ let(:raw_date) { '2016-02-02' }
+ let(:valid_arg) { "#{raw_time} #{raw_date}" }
+ let(:date) { Date.parse(raw_date) }
+ let(:time) { Gitlab::TimeTrackingFormatter.parse(raw_time) }
+ let(:expected_response) { [time, date] }
+ end
+ end
+
+ context 'composite time with date in arg line' do
+ it_behaves_like 'arg line with valid parameters' do
+ let(:raw_time) { '2m 10m 1h 3d' }
+ let(:raw_date) { '2016/02/02' }
+ let(:valid_arg) { "#{raw_time} #{raw_date}" }
+ let(:date) { Date.parse(raw_date) }
+ let(:time) { Gitlab::TimeTrackingFormatter.parse(raw_time) }
+ let(:expected_response) { [time, date] }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/saml/auth_hash_spec.rb b/spec/lib/gitlab/saml/auth_hash_spec.rb
new file mode 100644
index 00000000000..a555935aea3
--- /dev/null
+++ b/spec/lib/gitlab/saml/auth_hash_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Saml::AuthHash do
+ include LoginHelpers
+
+ let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers) } }
+ subject(:saml_auth_hash) { described_class.new(omniauth_auth_hash) }
+
+ let(:info_hash) do
+ {
+ name: 'John',
+ email: 'john@mail.com'
+ }
+ end
+
+ let(:omniauth_auth_hash) do
+ OmniAuth::AuthHash.new(uid: 'my-uid',
+ provider: 'saml',
+ info: info_hash,
+ extra: { raw_info: OneLogin::RubySaml::Attributes.new(raw_info_attr) } )
+ end
+
+ before do
+ stub_saml_group_config(%w(Developers Freelancers Designers))
+ end
+
+ describe '#groups' do
+ it 'returns array of groups' do
+ expect(saml_auth_hash.groups).to eq(%w(Developers Freelancers))
+ end
+
+ context 'raw info hash attributes empty' do
+ let(:raw_info_attr) { {} }
+
+ it 'returns an empty array' do
+ expect(saml_auth_hash.groups).to be_a(Array)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 59923bfb14d..1765980e977 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -2,13 +2,15 @@ require 'spec_helper'
describe Gitlab::Saml::User do
include LdapHelpers
+ include LoginHelpers
let(:saml_user) { described_class.new(auth_hash) }
let(:gl_user) { saml_user.gl_user }
let(:uid) { 'my-uid' }
- let(:dn) { 'uid=user1,ou=People,dc=example' }
+ let(:dn) { 'uid=user1,ou=people,dc=example' }
let(:provider) { 'saml' }
- let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new({ 'groups' => %w(Developers Freelancers Designers) }) }) }
+ let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers Designers) } }
+ let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new(raw_info_attr) }) }
let(:info_hash) do
{
name: 'John',
@@ -18,22 +20,6 @@ describe Gitlab::Saml::User do
let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') }
describe '#save' do
- def stub_omniauth_config(messages)
- allow(Gitlab.config.omniauth).to receive_messages(messages)
- end
-
- def stub_ldap_config(messages)
- allow(Gitlab::LDAP::Config).to receive_messages(messages)
- end
-
- def stub_basic_saml_config
- allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } })
- end
-
- def stub_saml_group_config(groups)
- allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
- end
-
before do
stub_basic_saml_config
end
@@ -402,4 +388,16 @@ describe Gitlab::Saml::User do
end
end
end
+
+ describe '#find_user' do
+ context 'raw info hash attributes empty' do
+ let(:raw_info_attr) { {} }
+
+ it 'does not mark user as external' do
+ stub_saml_group_config(%w(Freelancers))
+
+ expect(saml_user.find_user.external).to be_falsy
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 139afa22d01..2158b2837e2 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -156,7 +156,7 @@ describe Gitlab::Shell do
it_behaves_like '#add_repository'
end
- context 'without gitaly', skip_gitaly_mock: true do
+ context 'without gitaly', :skip_gitaly_mock do
it_behaves_like '#add_repository'
end
end
@@ -333,7 +333,7 @@ describe Gitlab::Shell do
end
end
- describe '#fetch_remote local', skip_gitaly_mock: true do
+ describe '#fetch_remote local', :skip_gitaly_mock do
it_should_behave_like 'fetch_remote', false
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
new file mode 100644
index 00000000000..8fdbbacd04d
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::MemoryKiller do
+ subject { described_class.new }
+ let(:pid) { 999 }
+
+ let(:worker) { double(:worker, class: 'TestWorker') }
+ let(:job) { { 'jid' => 123 } }
+ let(:queue) { 'test_queue' }
+
+ def run
+ thread = subject.call(worker, job, queue) { nil }
+ thread&.join
+ end
+
+ before do
+ allow(subject).to receive(:get_rss).and_return(10.kilobytes)
+ allow(subject).to receive(:pid).and_return(pid)
+ end
+
+ context 'when MAX_RSS is set to 0' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 0)
+ end
+
+ it 'does nothing' do
+ expect(subject).not_to receive(:sleep)
+
+ run
+ end
+ end
+
+ context 'when MAX_RSS is exceeded' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 5.kilobytes)
+ end
+
+ it 'sends the STP, TERM and KILL signals at expected times' do
+ expect(subject).to receive(:sleep).with(15 * 60).ordered
+ expect(Process).to receive(:kill).with('SIGSTP', pid).ordered
+
+ expect(subject).to receive(:sleep).with(30).ordered
+ expect(Process).to receive(:kill).with('SIGTERM', pid).ordered
+
+ expect(subject).to receive(:sleep).with(10).ordered
+ expect(Process).to receive(:kill).with('SIGKILL', pid).ordered
+
+ run
+ end
+ end
+
+ context 'when MAX_RSS is not exceeded' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 15.kilobytes)
+ end
+
+ it 'does nothing' do
+ expect(subject).not_to receive(:sleep)
+
+ run
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb
index c2e77ef6b6c..884f27b212c 100644
--- a/spec/lib/gitlab/sidekiq_status_spec.rb
+++ b/spec/lib/gitlab/sidekiq_status_spec.rb
@@ -39,6 +39,18 @@ describe Gitlab::SidekiqStatus do
end
end
+ describe '.running?', :clean_gitlab_redis_shared_state do
+ it 'returns true if job is running' do
+ described_class.set('123')
+
+ expect(described_class.running?('123')).to be(true)
+ end
+
+ it 'returns false if job is not found' do
+ expect(described_class.running?('123')).to be(false)
+ end
+ end
+
describe '.num_running', :clean_gitlab_redis_shared_state do
it 'returns 0 if all jobs have been completed' do
expect(described_class.num_running(%w(123))).to eq(0)
diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb
index 48d56628ed5..ef51e3cc8df 100644
--- a/spec/lib/gitlab/sql/pattern_spec.rb
+++ b/spec/lib/gitlab/sql/pattern_spec.rb
@@ -137,22 +137,22 @@ describe Gitlab::SQL::Pattern do
end
end
- describe '.to_fuzzy_arel' do
- subject(:to_fuzzy_arel) { Issue.to_fuzzy_arel(:title, query) }
+ describe '.fuzzy_arel_match' do
+ subject(:fuzzy_arel_match) { Issue.fuzzy_arel_match(: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\%'/)
+ expect(fuzzy_arel_match.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
+ it 'returns a single equality condition' do
+ expect(fuzzy_arel_match.to_sql).to match(/title.*I?LIKE 'fo'/)
end
end
@@ -160,7 +160,23 @@ describe Gitlab::SQL::Pattern 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\%'/)
+ expect(fuzzy_arel_match.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%'/)
+ end
+ end
+
+ context 'with two words both shorter than 3 chars' do
+ let(:query) { 'fo ba' }
+
+ it 'returns a single ILIKE condition' do
+ expect(fuzzy_arel_match.to_sql).to match(/title.*I?LIKE 'fo ba'/)
+ end
+ end
+
+ context 'with two words, one shorter 3 chars' do
+ let(:query) { 'foo ba' }
+
+ it 'returns a single ILIKE condition using the longer word' do
+ expect(fuzzy_arel_match.to_sql).to match(/title.+I?LIKE '\%foo\%'/)
end
end
@@ -168,7 +184,7 @@ describe Gitlab::SQL::Pattern 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\%'/)
+ expect(fuzzy_arel_match.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%' AND .*title.*I?LIKE '\%really bar\%'/)
end
end
end
diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb
index 8026fba9f0a..fe6422c32b6 100644
--- a/spec/lib/gitlab/sql/union_spec.rb
+++ b/spec/lib/gitlab/sql/union_spec.rb
@@ -29,5 +29,12 @@ describe Gitlab::SQL::Union do
expect(union.to_sql).to include('UNION ALL')
end
+
+ it 'returns `NULL` if all relations are empty' do
+ empty_relation = User.none
+ union = described_class.new([empty_relation, empty_relation])
+
+ expect(union.to_sql).to eq('NULL')
+ end
end
end
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index f18823b61ef..d9b3c2350b1 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -20,6 +20,22 @@ describe Gitlab::UrlBlocker do
expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git')).to be true
end
+ it 'returns true for alternative version of 127.0.0.1 (0177.1)' do
+ expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true
+ end
+
+ it 'returns true for alternative version of 127.0.0.1 (0x7f.1)' do
+ expect(described_class.blocked_url?('https://0x7f.1:65535/foo/foo.git')).to be true
+ end
+
+ it 'returns true for alternative version of 127.0.0.1 (2130706433)' do
+ expect(described_class.blocked_url?('https://2130706433:65535/foo/foo.git')).to be true
+ end
+
+ it 'returns true for alternative version of 127.0.0.1 (127.000.000.001)' do
+ expect(described_class.blocked_url?('https://127.000.000.001:65535/foo/foo.git')).to be true
+ end
+
it 'returns true for a non-alphanumeric hostname' do
stub_resolv
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 777e9c8e21d..b5f2a15ada3 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -60,7 +60,9 @@ describe Gitlab::UsageData do
deploy_keys
deployments
environments
- gcp_clusters
+ clusters
+ clusters_enabled
+ clusters_disabled
in_review_folder
groups
issues
@@ -101,7 +103,7 @@ describe Gitlab::UsageData do
subject { described_class.features_usage_data_ce }
it 'gathers feature usage data' do
- expect(subject[:signup]).to eq(current_application_settings.signup_enabled?)
+ expect(subject[:signup]).to eq(current_application_settings.allow_signup?)
expect(subject[:ldap]).to eq(Gitlab.config.ldap.enabled)
expect(subject[:gravatar]).to eq(current_application_settings.gravatar_enabled?)
expect(subject[:omniauth]).to eq(Gitlab.config.omniauth.enabled)
diff --git a/spec/lib/gitlab/utils/merge_hash_spec.rb b/spec/lib/gitlab/utils/merge_hash_spec.rb
new file mode 100644
index 00000000000..4fa7bb31301
--- /dev/null
+++ b/spec/lib/gitlab/utils/merge_hash_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+describe Gitlab::Utils::MergeHash do
+ describe '.crush' do
+ it 'can flatten a hash to each element' do
+ input = { hello: "world", this: { crushes: ["an entire", "hash"] } }
+ expected_result = [:hello, "world", :this, :crushes, "an entire", "hash"]
+
+ expect(described_class.crush(input)).to eq(expected_result)
+ end
+ end
+
+ describe '.elements' do
+ it 'deep merges an array of elements' do
+ input = [{ hello: ["world"] },
+ { hello: "Everyone" },
+ { hello: { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzień dobry'] } },
+ "Goodbye", "Hallo"]
+ expected_output = [
+ {
+ hello:
+ [
+ "world",
+ "Everyone",
+ { greetings: ['Bonjour', 'Hello', 'Hallo', 'Dzień dobry'] }
+ ]
+ },
+ "Goodbye"
+ ]
+
+ expect(described_class.merge(input)).to eq(expected_output)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb
new file mode 100644
index 00000000000..4a104ab6d97
--- /dev/null
+++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Gitlab::Utils::StrongMemoize do
+ let(:klass) do
+ struct = Struct.new(:value) do
+ def method_name
+ strong_memoize(:method_name) do
+ trace << value
+ value
+ end
+ end
+
+ def trace
+ @trace ||= []
+ end
+ end
+
+ struct.include(described_class)
+ struct
+ end
+
+ subject(:object) { klass.new(value) }
+
+ shared_examples 'caching the value' do
+ it 'only calls the block once' do
+ value0 = object.method_name
+ value1 = object.method_name
+
+ expect(value0).to eq(value)
+ expect(value1).to eq(value)
+ expect(object.trace).to contain_exactly(value)
+ end
+
+ it 'returns and defines the instance variable for the exact value' do
+ returned_value = object.method_name
+ memoized_value = object.instance_variable_get(:@method_name)
+
+ expect(returned_value).to eql(value)
+ expect(memoized_value).to eql(value)
+ end
+ end
+
+ describe '#strong_memoize' do
+ [nil, false, true, 'value', 0, [0]].each do |value|
+ context "with value #{value}" do
+ let(:value) { value }
+
+ it_behaves_like 'caching the value'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 4dffe2bd82f..249c77dc636 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -41,7 +41,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_archive feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -66,12 +66,34 @@ describe Gitlab::Workhorse do
let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
subject { described_class.send_git_patch(repository, diff_refs) }
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
+ context 'when Gitaly workhorse_send_git_patch feature is enabled' do
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
- expect(key).to eq("Gitlab-Workhorse-Send-Data")
- expect(command).to eq("git-format-patch")
- expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("git-format-patch")
+ expect(params).to eq({
+ 'GitalyServer' => {
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'RawPatchRequest' => Gitaly::RawPatchRequest.new(
+ repository: repository.gitaly_repository,
+ left_commit_id: 'base',
+ right_commit_id: 'head'
+ ).to_json
+ }.deep_stringify_keys)
+ end
+ end
+
+ context 'when Gitaly workhorse_send_git_patch feature is disabled', :skip_gitaly_mock do
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("git-format-patch")
+ expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+ end
end
end
@@ -115,14 +137,36 @@ describe Gitlab::Workhorse do
describe '.send_git_diff' do
let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
- subject { described_class.send_git_patch(repository, diff_refs) }
+ subject { described_class.send_git_diff(repository, diff_refs) }
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
+ context 'when Gitaly workhorse_send_git_diff feature is enabled' do
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
- expect(key).to eq("Gitlab-Workhorse-Send-Data")
- expect(command).to eq("git-format-patch")
- expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("git-diff")
+ expect(params).to eq({
+ 'GitalyServer' => {
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'RawDiffRequest' => Gitaly::RawDiffRequest.new(
+ repository: repository.gitaly_repository,
+ left_commit_id: 'base',
+ right_commit_id: 'head'
+ ).to_json
+ }.deep_stringify_keys)
+ end
+ end
+
+ context 'when Gitaly workhorse_send_git_diff feature is disabled', :skip_gitaly_mock do
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("git-diff")
+ expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+ end
end
end
@@ -224,7 +268,8 @@ describe Gitlab::Workhorse do
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "project-#{project.id}",
- RepoPath: repo_path
+ RepoPath: repo_path,
+ ShowAllRefs: false
}
end
@@ -238,7 +283,8 @@ describe Gitlab::Workhorse do
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "wiki-#{project.id}",
- RepoPath: repo_path
+ RepoPath: repo_path,
+ ShowAllRefs: false
}
end
@@ -280,6 +326,12 @@ describe Gitlab::Workhorse do
expect(subject).to include(gitaly_params)
end
+
+ context 'show_all_refs enabled' do
+ subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) }
+
+ it { is_expected.to include(ShowAllRefs: true) }
+ end
end
context "when git_receive_pack action is passed" do
@@ -292,6 +344,12 @@ describe Gitlab::Workhorse do
let(:action) { 'info_refs' }
it { expect(subject).to include(gitaly_params) }
+
+ context 'show_all_refs enabled' do
+ subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) }
+
+ it { is_expected.to include(ShowAllRefs: true) }
+ end
end
context 'when action passed is not supported by Gitaly' do
@@ -383,7 +441,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_raw_show feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly workhorse_raw_show feature is disabled', :skip_gitaly_mock do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index acc5bd1da35..fac23dce44d 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -69,7 +69,7 @@ describe GoogleApi::CloudPlatform::Client do
let(:cluster_name) { 'test-cluster' }
let(:cluster_size) { 1 }
- let(:machine_type) { 'n1-standard-4' }
+ let(:machine_type) { 'n1-standard-2' }
let(:operation) { double }
before do
diff --git a/spec/lib/milestone_array_spec.rb b/spec/lib/milestone_array_spec.rb
new file mode 100644
index 00000000000..df91677b925
--- /dev/null
+++ b/spec/lib/milestone_array_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe MilestoneArray do
+ let(:object1) { instance_double("BirdMilestone", due_date: Time.now, start_date: Time.now - 15.days, title: 'v2.0') }
+ let(:object2) { instance_double("CatMilestone", due_date: Time.now - 1.day, start_date: nil, title: 'v1.0') }
+ let(:object3) { instance_double("DogMilestone", due_date: nil, start_date: Time.now - 30.days, title: 'v3.0') }
+ let(:array) { [object1, object3, object2] }
+
+ describe '#sort' do
+ it 'reorders array with due date in ascending order with nulls last' do
+ expect(described_class.sort(array, 'due_date_asc')).to eq([object2, object1, object3])
+ end
+
+ it 'reorders array with due date in desc order with nulls last' do
+ expect(described_class.sort(array, 'due_date_desc')).to eq([object1, object2, object3])
+ end
+
+ it 'reorders array with start date in ascending order with nulls last' do
+ expect(described_class.sort(array, 'start_date_asc')).to eq([object3, object1, object2])
+ end
+
+ it 'reorders array with start date in descending order with nulls last' do
+ expect(described_class.sort(array, 'start_date_desc')).to eq([object1, object3, object2])
+ end
+
+ it 'reorders array with title in ascending order' do
+ expect(described_class.sort(array, 'name_asc')).to eq([object2, object1, object3])
+ end
+
+ it 'reorders array with title in descending order' do
+ expect(described_class.sort(array, 'name_desc')).to eq([object3, object1, object2])
+ end
+ end
+end
diff --git a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
index b4b83b70d1c..a0fb86345f3 100644
--- a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
+++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
@@ -39,14 +39,6 @@ describe SystemCheck::App::GitUserDefaultSSHConfigCheck do
it { is_expected.to eq(expected_result) }
end
-
- it 'skips GitLab read-only instances' do
- stub_user
- stub_home_dir
- allow(Gitlab::Database).to receive(:read_only?).and_return(true)
-
- is_expected.to be_truthy
- end
end
describe '#check?' do
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 932e2fd8c95..f942a22b6d1 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -28,319 +28,334 @@ describe Notify do
end
def have_referable_subject(referable, reply: false)
- prefix = referable.project.name if referable.project
- prefix = "Re: #{prefix}" if reply
+ prefix = referable.project ? "#{referable.project.name} | " : ''
+ prefix.prepend('Re: ') if reply
suffix = "#{referable.title} (#{referable.to_reference})"
- have_subject [prefix, suffix].compact.join(' | ')
+ have_subject [prefix, suffix].compact.join
end
context 'for a project' do
- describe 'items that are assignable, the email' do
- let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
+ shared_examples 'an assignee email' do
+ it 'is sent to the assignee as the author' do
+ sender = subject.header[:from].addrs.first
+
+ aggregate_failures do
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ expect(subject).to deliver_to(assignee.email)
+ end
+ end
+ end
- shared_examples 'an assignee email' do
- it 'is sent to the assignee as the author' do
- sender = subject.header[:from].addrs.first
+ context 'for issues' do
+ describe 'that are new' do
+ subject { described_class.new_issue_email(issue.assignees.first.id, issue.id) }
+
+ it_behaves_like 'an assignee email'
+ it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
+ it 'has the correct subject and body' do
aggregate_failures do
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- expect(subject).to deliver_to(assignee.email)
+ is_expected.to have_referable_subject(issue)
+ is_expected.to have_body_text(project_issue_path(project, issue))
end
end
- end
- context 'for issues' do
- describe 'that are new' do
- subject { described_class.new_issue_email(issue.assignees.first.id, issue.id) }
+ it 'contains the description' do
+ is_expected.to have_html_escaped_body_text issue.description
+ end
- it_behaves_like 'an assignee email'
- it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
- let(:model) { issue }
- end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
-
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(issue)
- is_expected.to have_body_text(project_issue_path(project, issue))
- end
+ context 'when enabled email_author_in_body' do
+ before do
+ stub_application_setting(email_author_in_body: true)
end
- it 'contains the description' do
- is_expected.to have_html_escaped_body_text issue.description
+ it 'contains a link to note author' do
+ is_expected.to have_html_escaped_body_text(issue.author_name)
+ is_expected.to have_body_text 'created an issue:'
end
+ end
+ end
- context 'when enabled email_author_in_body' do
- before do
- stub_application_setting(email_author_in_body: true)
- end
+ describe 'that are reassigned' do
+ let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
+ subject { described_class.reassigned_issue_email(recipient.id, issue.id, [previous_assignee.id], current_user.id) }
- it 'contains a link to note author' do
- is_expected.to have_html_escaped_body_text(issue.author_name)
- is_expected.to have_body_text 'created an issue:'
- end
- end
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
- describe 'that have been reassigned' do
- subject { described_class.reassigned_issue_email(recipient.id, issue.id, [previous_assignee.id], current_user.id) }
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_html_escaped_body_text(previous_assignee.name)
+ is_expected.to have_html_escaped_body_text(assignee.name)
+ is_expected.to have_body_text(project_issue_path(project, issue))
end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
+ end
+ end
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ describe 'that have been relabeled' do
+ subject { described_class.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_html_escaped_body_text(previous_assignee.name)
- is_expected.to have_html_escaped_body_text(assignee.name)
- is_expected.to have_body_text(project_issue_path(project, issue))
- end
- end
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email with a labels subscriptions link in its footer'
- describe 'that have been relabeled' do
- subject { described_class.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_body_text('foo, bar, and baz')
+ is_expected.to have_body_text(project_issue_path(project, issue))
end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'a user cannot unsubscribe through footer link'
- it_behaves_like 'an email with a labels subscriptions link in its footer'
+ end
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
+ context 'with a preferred language' do
+ before do
+ Gitlab::I18n.locale = :es
end
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_body_text('foo, bar, and baz')
- is_expected.to have_body_text(project_issue_path(project, issue))
- end
+ after do
+ Gitlab::I18n.use_default_locale
end
- context 'with a preferred language' do
- before do
- Gitlab::I18n.locale = :es
- end
-
- after do
- Gitlab::I18n.use_default_locale
- end
-
- it 'always generates the email using the default language' do
- is_expected.to have_body_text('foo, bar, and baz')
- end
+ it 'always generates the email using the default language' do
+ is_expected.to have_body_text('foo, bar, and baz')
end
end
+ end
- describe 'status changed' do
- let(:status) { 'closed' }
- subject { described_class.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
+ describe 'status changed' do
+ let(:status) { 'closed' }
+ subject { described_class.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
- end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_body_text(status)
- is_expected.to have_html_escaped_body_text(current_user.name)
- is_expected.to have_body_text(project_issue_path project, issue)
- end
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_body_text(status)
+ is_expected.to have_html_escaped_body_text(current_user.name)
+ is_expected.to have_body_text(project_issue_path project, issue)
end
end
+ end
- describe 'moved to another project' do
- let(:new_issue) { create(:issue) }
- subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) }
+ describe 'moved to another project' do
+ let(:new_issue) { create(:issue) }
+ subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) }
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
- end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
- it 'contains description about action taken' do
- is_expected.to have_body_text 'Issue was moved to another project'
- end
+ it 'contains description about action taken' do
+ is_expected.to have_body_text 'Issue was moved to another project'
+ end
- it 'has the correct subject and body' do
- new_issue_url = project_issue_path(new_issue.project, new_issue)
+ it 'has the correct subject and body' do
+ new_issue_url = project_issue_path(new_issue.project, new_issue)
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_body_text(new_issue_url)
- is_expected.to have_body_text(project_issue_path(project, issue))
- end
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_body_text(new_issue_url)
+ is_expected.to have_body_text(project_issue_path(project, issue))
end
end
end
+ end
+
+ context 'for merge requests' do
+ describe 'that are new' do
+ subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
- context 'for merge requests' do
- describe 'that are new' do
- subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
+ it_behaves_like 'an assignee email'
+ it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
- it_behaves_like 'an assignee email'
- it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
- let(:model) { merge_request }
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(merge_request)
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
+ is_expected.to have_body_text(merge_request.source_branch)
+ is_expected.to have_body_text(merge_request.target_branch)
end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like 'an unsubscribeable thread'
-
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(merge_request)
- is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- is_expected.to have_body_text(merge_request.source_branch)
- is_expected.to have_body_text(merge_request.target_branch)
- end
+ end
+
+ it 'contains the description' do
+ is_expected.to have_html_escaped_body_text merge_request.description
+ end
+
+ context 'when enabled email_author_in_body' do
+ before do
+ stub_application_setting(email_author_in_body: true)
end
- it 'contains the description' do
- is_expected.to have_html_escaped_body_text merge_request.description
+ it 'contains a link to note author' do
+ is_expected.to have_html_escaped_body_text merge_request.author_name
+ is_expected.to have_body_text 'created a merge request:'
end
+ end
+ end
- context 'when enabled email_author_in_body' do
- before do
- stub_application_setting(email_author_in_body: true)
- end
+ describe 'that are reassigned' do
+ let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
+ subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
- it 'contains a link to note author' do
- is_expected.to have_html_escaped_body_text merge_request.author_name
- is_expected.to have_body_text 'created a merge request:'
- end
- end
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
- describe 'that are reassigned' do
- subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { merge_request }
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(merge_request, reply: true)
+ is_expected.to have_html_escaped_body_text(previous_assignee.name)
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
+ is_expected.to have_html_escaped_body_text(assignee.name)
end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like "an unsubscribeable thread"
+ end
+ end
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ describe 'that have been relabeled' do
+ subject { described_class.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(merge_request, reply: true)
- is_expected.to have_html_escaped_body_text(previous_assignee.name)
- is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- is_expected.to have_html_escaped_body_text(assignee.name)
- end
- end
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'an email with a labels subscriptions link in its footer'
- describe 'that have been relabeled' do
- subject { described_class.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { merge_request }
- end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like 'a user cannot unsubscribe through footer link'
- it_behaves_like 'an email with a labels subscriptions link in its footer'
+ it 'has the correct subject and body' do
+ is_expected.to have_referable_subject(merge_request, reply: true)
+ is_expected.to have_body_text('foo, bar, and baz')
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
+ end
+ end
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ describe 'status changed' do
+ let(:status) { 'reopened' }
+ subject { described_class.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
- it 'has the correct subject and body' do
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
+
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(current_user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'has the correct subject and body' do
+ aggregate_failures do
is_expected.to have_referable_subject(merge_request, reply: true)
- is_expected.to have_body_text('foo, bar, and baz')
+ is_expected.to have_body_text(status)
+ is_expected.to have_html_escaped_body_text(current_user.name)
is_expected.to have_body_text(project_merge_request_path(project, merge_request))
end
end
+ end
- describe 'status changed' do
- let(:status) { 'reopened' }
- subject { described_class.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
+ describe 'that are merged' do
+ let(:merge_author) { create(:user) }
+ subject { described_class.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { merge_request }
- end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
- it 'is sent as the author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(current_user.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ it 'is sent as the merge author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(merge_author.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(merge_request, reply: true)
- is_expected.to have_body_text(status)
- is_expected.to have_html_escaped_body_text(current_user.name)
- is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- end
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(merge_request, reply: true)
+ is_expected.to have_body_text('merged')
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
end
end
+ end
+ end
- describe 'that are merged' do
- let(:merge_author) { create(:user) }
- subject { described_class.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
+ context 'for snippet notes' do
+ let(:project_snippet) { create(:project_snippet, project: project) }
+ let(:project_snippet_note) { create(:note_on_project_snippet, project: project, noteable: project_snippet) }
- it_behaves_like 'a multiple recipients email'
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { merge_request }
- end
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like 'an unsubscribeable thread'
+ subject { described_class.note_snippet_email(project_snippet_note.author_id, project_snippet_note.id) }
- it 'is sent as the merge author' do
- sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq(merge_author.name)
- expect(sender.address).to eq(gitlab_sender)
- end
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { project_snippet }
+ end
+ it_behaves_like 'a user cannot unsubscribe through footer link'
- it 'has the correct subject and body' do
- aggregate_failures do
- is_expected.to have_referable_subject(merge_request, reply: true)
- is_expected.to have_body_text('merged')
- is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- end
- end
- end
+ it 'has the correct subject and body' do
+ is_expected.to have_referable_subject(project_snippet, reply: true)
+ is_expected.to have_html_escaped_body_text project_snippet_note.note
end
end
@@ -768,7 +783,25 @@ describe Notify do
shared_examples 'an email for a note on a diff discussion' do |model|
let(:note) { create(model, author: note_author) }
- it "includes diffs with character-level highlighting" do
+ context 'when note is on image' do
+ before do
+ allow_any_instance_of(DiffDiscussion).to receive(:on_image?).and_return(true)
+ end
+
+ it 'does not include diffs with character-level highlighting' do
+ is_expected.not_to have_body_text '<span class="p">}</span></span>'
+ end
+
+ it 'ends the intro with a dot' do
+ is_expected.to have_body_text "#{note.diff_file.file_path}</a>."
+ end
+ end
+
+ it 'ends the intro with a colon' do
+ is_expected.to have_body_text "#{note.diff_file.file_path}</a>:"
+ end
+
+ it 'includes diffs with character-level highlighting' do
is_expected.to have_body_text '<span class="p">}</span></span>'
end
@@ -1239,4 +1272,18 @@ describe Notify do
end
end
end
+
+ context 'for personal snippet notes' do
+ let(:personal_snippet) { create(:personal_snippet) }
+ let(:personal_snippet_note) { create(:note_on_personal_snippet, noteable: personal_snippet) }
+
+ subject { described_class.note_personal_snippet_email(personal_snippet_note.author_id, personal_snippet_note.id) }
+
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+
+ it 'has the correct subject and body' do
+ is_expected.to have_referable_subject(personal_snippet, reply: true)
+ is_expected.to have_html_escaped_body_text personal_snippet_note.note
+ end
+ end
end
diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
new file mode 100644
index 00000000000..9f41534441b
--- /dev/null
+++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
@@ -0,0 +1,166 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb')
+
+describe MigrateGcpClustersToNewClustersArchitectures, :migration do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:service) { create(:kubernetes_service, project: project) }
+
+ context 'when cluster is being created' do
+ let(:project_id) { project.id }
+ let(:user_id) { user.id }
+ let(:service_id) { service.id }
+ let(:status) { 2 } # creating
+ let(:gcp_cluster_size) { 1 }
+ let(:created_at) { "'2017-10-17 20:24:02'" }
+ let(:updated_at) { "'2017-10-17 20:28:44'" }
+ let(:enabled) { true }
+ let(:status_reason) { "''" }
+ let(:project_namespace) { "'sample-app'" }
+ let(:endpoint) { 'NULL' }
+ let(:ca_cert) { 'NULL' }
+ let(:encrypted_kubernetes_token) { 'NULL' }
+ let(:encrypted_kubernetes_token_iv) { 'NULL' }
+ let(:username) { 'NULL' }
+ let(:encrypted_password) { 'NULL' }
+ let(:encrypted_password_iv) { 'NULL' }
+ let(:gcp_project_id) { "'gcp_project_id'" }
+ let(:gcp_cluster_zone) { "'gcp_cluster_zone'" }
+ let(:gcp_cluster_name) { "'gcp_cluster_name'" }
+ let(:gcp_machine_type) { "'gcp_machine_type'" }
+ let(:gcp_operation_id) { 'NULL' }
+ let(:encrypted_gcp_token) { "'encrypted_gcp_token'" }
+ let(:encrypted_gcp_token_iv) { "'encrypted_gcp_token_iv'" }
+
+ let(:cluster) { Clusters::Cluster.last }
+ let(:cluster_id) { cluster.id }
+
+ before do
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO gcp_clusters (project_id, user_id, service_id, status, gcp_cluster_size, created_at, updated_at, enabled, status_reason, project_namespace, endpoint, ca_cert, encrypted_kubernetes_token, encrypted_kubernetes_token_iv, username, encrypted_password, encrypted_password_iv, gcp_project_id, gcp_cluster_zone, gcp_cluster_name, gcp_machine_type, gcp_operation_id, encrypted_gcp_token, encrypted_gcp_token_iv)
+ VALUES (#{project_id}, #{user_id}, #{service_id}, #{status}, #{gcp_cluster_size}, #{created_at}, #{updated_at}, #{enabled}, #{status_reason}, #{project_namespace}, #{endpoint}, #{ca_cert}, #{encrypted_kubernetes_token}, #{encrypted_kubernetes_token_iv}, #{username}, #{encrypted_password}, #{encrypted_password_iv}, #{gcp_project_id}, #{gcp_cluster_zone}, #{gcp_cluster_name}, #{gcp_machine_type}, #{gcp_operation_id}, #{encrypted_gcp_token}, #{encrypted_gcp_token_iv});
+ SQL
+ end
+
+ it 'correctly migrate to new clusters architectures' do
+ migrate!
+
+ expect(Clusters::Cluster.count).to eq(1)
+ expect(Clusters::Project.count).to eq(1)
+ expect(Clusters::Providers::Gcp.count).to eq(1)
+ expect(Clusters::Platforms::Kubernetes.count).to eq(1)
+
+ expect(cluster.user).to eq(user)
+ expect(cluster.enabled).to be_truthy
+ expect(cluster.name).to eq(gcp_cluster_name.delete!("'"))
+ expect(cluster.provider_type).to eq('gcp')
+ expect(cluster.platform_type).to eq('kubernetes')
+
+ expect(cluster.project).to eq(project)
+ expect(project.cluster).to eq(cluster)
+
+ expect(cluster.provider_gcp.cluster).to eq(cluster)
+ expect(cluster.provider_gcp.status).to eq(status)
+ expect(cluster.provider_gcp.status_reason).to eq(tr(status_reason))
+ expect(cluster.provider_gcp.gcp_project_id).to eq(tr(gcp_project_id))
+ expect(cluster.provider_gcp.zone).to eq(tr(gcp_cluster_zone))
+ expect(cluster.provider_gcp.num_nodes).to eq(gcp_cluster_size)
+ expect(cluster.provider_gcp.machine_type).to eq(tr(gcp_machine_type))
+ expect(cluster.provider_gcp.operation_id).to be_nil
+ expect(cluster.provider_gcp.endpoint).to be_nil
+ expect(cluster.provider_gcp.encrypted_access_token).to eq(tr(encrypted_gcp_token))
+ expect(cluster.provider_gcp.encrypted_access_token_iv).to eq(tr(encrypted_gcp_token_iv))
+
+ expect(cluster.platform_kubernetes.cluster).to eq(cluster)
+ expect(cluster.platform_kubernetes.api_url).to be_nil
+ expect(cluster.platform_kubernetes.ca_cert).to be_nil
+ expect(cluster.platform_kubernetes.namespace).to eq(tr(project_namespace))
+ expect(cluster.platform_kubernetes.username).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_password).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_password_iv).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_token).to be_nil
+ expect(cluster.platform_kubernetes.encrypted_token_iv).to be_nil
+ end
+ end
+
+ context 'when cluster has been created' do
+ let(:project_id) { project.id }
+ let(:user_id) { user.id }
+ let(:service_id) { service.id }
+ let(:status) { 3 } # created
+ let(:gcp_cluster_size) { 1 }
+ let(:created_at) { "'2017-10-17 20:24:02'" }
+ let(:updated_at) { "'2017-10-17 20:28:44'" }
+ let(:enabled) { true }
+ let(:status_reason) { "'general error'" }
+ let(:project_namespace) { "'sample-app'" }
+ let(:endpoint) { "'111.111.111.111'" }
+ let(:ca_cert) { "'ca_cert'" }
+ let(:encrypted_kubernetes_token) { "'encrypted_kubernetes_token'" }
+ let(:encrypted_kubernetes_token_iv) { "'encrypted_kubernetes_token_iv'" }
+ let(:username) { "'username'" }
+ let(:encrypted_password) { "'encrypted_password'" }
+ let(:encrypted_password_iv) { "'encrypted_password_iv'" }
+ let(:gcp_project_id) { "'gcp_project_id'" }
+ let(:gcp_cluster_zone) { "'gcp_cluster_zone'" }
+ let(:gcp_cluster_name) { "'gcp_cluster_name'" }
+ let(:gcp_machine_type) { "'gcp_machine_type'" }
+ let(:gcp_operation_id) { "'gcp_operation_id'" }
+ let(:encrypted_gcp_token) { "'encrypted_gcp_token'" }
+ let(:encrypted_gcp_token_iv) { "'encrypted_gcp_token_iv'" }
+
+ let(:cluster) { Clusters::Cluster.last }
+ let(:cluster_id) { cluster.id }
+
+ before do
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO gcp_clusters (project_id, user_id, service_id, status, gcp_cluster_size, created_at, updated_at, enabled, status_reason, project_namespace, endpoint, ca_cert, encrypted_kubernetes_token, encrypted_kubernetes_token_iv, username, encrypted_password, encrypted_password_iv, gcp_project_id, gcp_cluster_zone, gcp_cluster_name, gcp_machine_type, gcp_operation_id, encrypted_gcp_token, encrypted_gcp_token_iv)
+ VALUES (#{project_id}, #{user_id}, #{service_id}, #{status}, #{gcp_cluster_size}, #{created_at}, #{updated_at}, #{enabled}, #{status_reason}, #{project_namespace}, #{endpoint}, #{ca_cert}, #{encrypted_kubernetes_token}, #{encrypted_kubernetes_token_iv}, #{username}, #{encrypted_password}, #{encrypted_password_iv}, #{gcp_project_id}, #{gcp_cluster_zone}, #{gcp_cluster_name}, #{gcp_machine_type}, #{gcp_operation_id}, #{encrypted_gcp_token}, #{encrypted_gcp_token_iv});
+ SQL
+ end
+
+ it 'correctly migrate to new clusters architectures' do
+ migrate!
+
+ expect(Clusters::Cluster.count).to eq(1)
+ expect(Clusters::Project.count).to eq(1)
+ expect(Clusters::Providers::Gcp.count).to eq(1)
+ expect(Clusters::Platforms::Kubernetes.count).to eq(1)
+
+ expect(cluster.user).to eq(user)
+ expect(cluster.enabled).to be_truthy
+ expect(cluster.name).to eq(tr(gcp_cluster_name))
+ expect(cluster.provider_type).to eq('gcp')
+ expect(cluster.platform_type).to eq('kubernetes')
+
+ expect(cluster.project).to eq(project)
+ expect(project.cluster).to eq(cluster)
+
+ expect(cluster.provider_gcp.cluster).to eq(cluster)
+ expect(cluster.provider_gcp.status).to eq(status)
+ expect(cluster.provider_gcp.status_reason).to eq(tr(status_reason))
+ expect(cluster.provider_gcp.gcp_project_id).to eq(tr(gcp_project_id))
+ expect(cluster.provider_gcp.zone).to eq(tr(gcp_cluster_zone))
+ expect(cluster.provider_gcp.num_nodes).to eq(gcp_cluster_size)
+ expect(cluster.provider_gcp.machine_type).to eq(tr(gcp_machine_type))
+ expect(cluster.provider_gcp.operation_id).to eq(tr(gcp_operation_id))
+ expect(cluster.provider_gcp.endpoint).to eq(tr(endpoint))
+ expect(cluster.provider_gcp.encrypted_access_token).to eq(tr(encrypted_gcp_token))
+ expect(cluster.provider_gcp.encrypted_access_token_iv).to eq(tr(encrypted_gcp_token_iv))
+
+ expect(cluster.platform_kubernetes.cluster).to eq(cluster)
+ expect(cluster.platform_kubernetes.api_url).to eq('https://' + tr(endpoint))
+ expect(cluster.platform_kubernetes.ca_cert).to eq(tr(ca_cert))
+ expect(cluster.platform_kubernetes.namespace).to eq(tr(project_namespace))
+ expect(cluster.platform_kubernetes.username).to eq(tr(username))
+ expect(cluster.platform_kubernetes.encrypted_password).to eq(tr(encrypted_password))
+ expect(cluster.platform_kubernetes.encrypted_password_iv).to eq(tr(encrypted_password_iv))
+ expect(cluster.platform_kubernetes.encrypted_token).to eq(tr(encrypted_kubernetes_token))
+ expect(cluster.platform_kubernetes.encrypted_token_iv).to eq(tr(encrypted_kubernetes_token_iv))
+ end
+ end
+
+ def tr(s)
+ s.delete("'")
+ end
+end
diff --git a/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb b/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb
new file mode 100644
index 00000000000..b4834705011
--- /dev/null
+++ b/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20171012125712_migrate_user_authentication_token_to_personal_access_token.rb')
+
+describe MigrateUserAuthenticationTokenToPersonalAccessToken, :migration do
+ let(:users) { table(:users) }
+ let(:personal_access_tokens) { table(:personal_access_tokens) }
+
+ let!(:user) { users.create!(id: 1, email: 'user@example.com', authentication_token: 'user-token', admin: false) }
+ let!(:admin) { users.create!(id: 2, email: 'admin@example.com', authentication_token: 'admin-token', admin: true) }
+
+ it 'migrates private tokens to Personal Access Tokens' do
+ migrate!
+
+ expect(personal_access_tokens.count).to eq(2)
+
+ user_token = personal_access_tokens.find_by(user_id: user.id)
+ admin_token = personal_access_tokens.find_by(user_id: admin.id)
+
+ expect(user_token.token).to eq('user-token')
+ expect(admin_token.token).to eq('admin-token')
+
+ expect(user_token.scopes).to eq(%w[api].to_yaml)
+ expect(admin_token.scopes).to eq(%w[api sudo].to_yaml)
+ end
+end
diff --git a/spec/migrations/migrate_user_project_view_spec.rb b/spec/migrations/migrate_user_project_view_spec.rb
index afaa5d836a7..5e16769d63a 100644
--- a/spec/migrations/migrate_user_project_view_spec.rb
+++ b/spec/migrations/migrate_user_project_view_spec.rb
@@ -5,12 +5,7 @@ require Rails.root.join('db', 'post_migrate', '20170406142253_migrate_user_proje
describe MigrateUserProjectView, :truncate do
let(:migration) { described_class.new }
- let!(:user) { create(:user) }
-
- before do
- # 0 is the numeric value for the old 'readme' option
- user.update_column(:project_view, 0)
- end
+ let!(:user) { create(:user, project_view: 'readme') }
describe '#up' do
it 'updates project view setting with new value' do
diff --git a/spec/migrations/remove_empty_fork_networks_spec.rb b/spec/migrations/remove_empty_fork_networks_spec.rb
new file mode 100644
index 00000000000..cf6ae5cda74
--- /dev/null
+++ b/spec/migrations/remove_empty_fork_networks_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171114104051_remove_empty_fork_networks.rb')
+
+describe RemoveEmptyForkNetworks, :migration do
+ let!(:fork_networks) { table(:fork_networks) }
+
+ let(:deleted_project) { create(:project) }
+ let!(:empty_network) { create(:fork_network, id: 1, root_project_id: deleted_project.id) }
+ let!(:other_network) { create(:fork_network, id: 2, root_project_id: create(:project).id) }
+
+ before do
+ deleted_project.destroy!
+ end
+
+ it 'deletes only the fork network without members' do
+ expect(fork_networks.count).to eq(2)
+
+ migrate!
+
+ expect(fork_networks.find_by(id: empty_network.id)).to be_nil
+ expect(fork_networks.find_by(id: other_network.id)).not_to be_nil
+ expect(fork_networks.count).to eq(1)
+ end
+end
diff --git a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb
index f95bd6e3511..76afb6c19cf 100644
--- a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb
+++ b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb
@@ -2,19 +2,6 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170703130158_schedule_merge_request_diff_migrations')
describe ScheduleMergeRequestDiffMigrations, :migration, :sidekiq do
- matcher :be_scheduled_migration do |time, *expected|
- match do |migration|
- BackgroundMigrationWorker.jobs.any? do |job|
- job['args'] == [migration, expected] &&
- job['at'].to_i == time.to_i
- end
- end
-
- failure_message do |migration|
- "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
- end
- end
-
let(:merge_request_diffs) { table(:merge_request_diffs) }
let(:merge_requests) { table(:merge_requests) }
let(:projects) { table(:projects) }
@@ -37,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrations, :migration, :sidekiq do
Timecop.freeze do
migrate!
- expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes.from_now, 1, 1)
- expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes.from_now, 2, 2)
- expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes.from_now, 4, 4)
+ expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 2, 2)
+ expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, 4, 4)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
diff --git a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb
index 4ab1bb67058..cf323973384 100644
--- a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb
+++ b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb
@@ -2,19 +2,6 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170926150348_schedule_merge_request_diff_migrations_take_two')
describe ScheduleMergeRequestDiffMigrationsTakeTwo, :migration, :sidekiq do
- matcher :be_scheduled_migration do |time, *expected|
- match do |migration|
- BackgroundMigrationWorker.jobs.any? do |job|
- job['args'] == [migration, expected] &&
- job['at'].to_i == time.to_i
- end
- end
-
- failure_message do |migration|
- "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
- end
- end
-
let(:merge_request_diffs) { table(:merge_request_diffs) }
let(:merge_requests) { table(:merge_requests) }
let(:projects) { table(:projects) }
@@ -37,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrationsTakeTwo, :migration, :sidekiq do
Timecop.freeze do
migrate!
- expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes.from_now, 1, 1)
- expect(described_class::MIGRATION).to be_scheduled_migration(20.minutes.from_now, 2, 2)
- expect(described_class::MIGRATION).to be_scheduled_migration(30.minutes.from_now, 4, 4)
+ expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_migration(20.minutes, 2, 2)
+ expect(described_class::MIGRATION).to be_scheduled_migration(30.minutes, 4, 4)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
diff --git a/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb
new file mode 100644
index 00000000000..158d0bc02ed
--- /dev/null
+++ b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations')
+
+describe ScheduleMergeRequestLatestMergeRequestDiffIdMigrations, :migration, :sidekiq do
+ let(:projects_table) { table(:projects) }
+ let(:merge_requests_table) { table(:merge_requests) }
+ let(:merge_request_diffs_table) { table(:merge_request_diffs) }
+
+ let(:project) { projects_table.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce') }
+
+ let!(:merge_request_1) { create_mr!('mr_1', diffs: 1) }
+ let!(:merge_request_2) { create_mr!('mr_2', diffs: 2) }
+ let!(:merge_request_migrated) { create_mr!('merge_request_migrated', diffs: 3) }
+ let!(:merge_request_4) { create_mr!('mr_4', diffs: 3) }
+
+ def create_mr!(name, diffs: 0)
+ merge_request =
+ merge_requests_table.create!(target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: name,
+ title: name)
+
+ diffs.times do
+ merge_request_diffs_table.create!(merge_request_id: merge_request.id)
+ end
+
+ merge_request
+ end
+
+ def diffs_for(merge_request)
+ merge_request_diffs_table.where(merge_request_id: merge_request.id)
+ end
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 1)
+
+ diff_id = diffs_for(merge_request_migrated).minimum(:id)
+ merge_request_migrated.update!(latest_merge_request_diff_id: diff_id)
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, merge_request_1.id, merge_request_1.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, merge_request_2.id, merge_request_2.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, merge_request_4.id, merge_request_4.id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+
+ it 'schedules background migrations' do
+ Sidekiq::Testing.inline! do
+ expect(merge_requests_table.where(latest_merge_request_diff_id: nil).count).to eq 3
+
+ migrate!
+
+ expect(merge_requests_table.where(latest_merge_request_diff_id: nil).count).to eq 0
+ end
+ end
+end
diff --git a/spec/migrations/update_upload_paths_to_system_spec.rb b/spec/migrations/update_upload_paths_to_system_spec.rb
index 0a45c5ea32d..d4a1553fb0e 100644
--- a/spec/migrations/update_upload_paths_to_system_spec.rb
+++ b/spec/migrations/update_upload_paths_to_system_spec.rb
@@ -31,7 +31,7 @@ describe UpdateUploadPathsToSystem do
end
end
- describe "#up", truncate: true do
+ describe "#up", :truncate do
it "updates old upload records to the new path" do
old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg")
@@ -41,7 +41,7 @@ describe UpdateUploadPathsToSystem do
end
end
- describe "#down", truncate: true do
+ describe "#down", :truncate do
it "updates the new system patsh to the old paths" do
new_upload = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg")
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 78cacf9ff5d..0b7e16cc33c 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -114,6 +114,30 @@ describe ApplicationSetting do
it { expect(setting.repository_storages).to eq(['default']) }
end
+ context 'circuitbreaker settings' do
+ [:circuitbreaker_backoff_threshold,
+ :circuitbreaker_failure_count_threshold,
+ :circuitbreaker_failure_wait_time,
+ :circuitbreaker_failure_reset_time,
+ :circuitbreaker_storage_timeout].each do |field|
+ it "Validates #{field} as number" do
+ is_expected.to validate_numericality_of(field)
+ .only_integer
+ .is_greater_than_or_equal_to(0)
+ end
+ end
+
+ it 'requires the `backoff_threshold` to be lower than the `failure_count_threshold`' do
+ setting.circuitbreaker_failure_count_threshold = 10
+ setting.circuitbreaker_backoff_threshold = 15
+ failure_message = "The circuitbreaker backoff threshold should be lower "\
+ "than the failure count threshold"
+
+ expect(setting).not_to be_valid
+ expect(setting.errors[:circuitbreaker_backoff_threshold]).to include(failure_message)
+ end
+ end
+
context 'repository storages' do
before do
storages = {
@@ -195,6 +219,65 @@ describe ApplicationSetting do
expect(subject).to be_valid
end
end
+
+ context 'gitaly timeouts' do
+ [:gitaly_timeout_default, :gitaly_timeout_medium, :gitaly_timeout_fast].each do |timeout_name|
+ it do
+ is_expected.to validate_presence_of(timeout_name)
+ is_expected.to validate_numericality_of(timeout_name).only_integer
+ .is_greater_than_or_equal_to(0)
+ end
+ end
+
+ [:gitaly_timeout_medium, :gitaly_timeout_fast].each do |timeout_name|
+ it "validates that #{timeout_name} is lower than timeout_default" do
+ subject[:gitaly_timeout_default] = 50
+ subject[timeout_name] = 100
+
+ expect(subject).to be_invalid
+ end
+ end
+
+ it 'accepts all timeouts equal' do
+ subject.gitaly_timeout_default = 0
+ subject.gitaly_timeout_medium = 0
+ subject.gitaly_timeout_fast = 0
+
+ expect(subject).to be_valid
+ end
+
+ it 'accepts timeouts in descending order' do
+ subject.gitaly_timeout_default = 50
+ subject.gitaly_timeout_medium = 30
+ subject.gitaly_timeout_fast = 20
+
+ expect(subject).to be_valid
+ end
+
+ it 'rejects timeouts in ascending order' do
+ subject.gitaly_timeout_default = 20
+ subject.gitaly_timeout_medium = 30
+ subject.gitaly_timeout_fast = 50
+
+ expect(subject).to be_invalid
+ end
+
+ it 'rejects medium timeout larger than default' do
+ subject.gitaly_timeout_default = 30
+ subject.gitaly_timeout_medium = 50
+ subject.gitaly_timeout_fast = 20
+
+ expect(subject).to be_invalid
+ end
+
+ it 'rejects medium timeout smaller than fast' do
+ subject.gitaly_timeout_default = 30
+ subject.gitaly_timeout_medium = 15
+ subject.gitaly_timeout_fast = 20
+
+ expect(subject).to be_invalid
+ end
+ end
end
describe '.current' do
@@ -207,6 +290,31 @@ describe ApplicationSetting do
expect(described_class.current).to eq(:last)
end
end
+
+ context 'when an ApplicationSetting is not yet present' do
+ it 'does not cache nil object' do
+ # when missing settings a nil object is returned, but not cached
+ allow(described_class).to receive(:last).and_return(nil).twice
+ expect(described_class.current).to be_nil
+
+ # when the settings are set the method returns a valid object
+ allow(described_class).to receive(:last).and_return(:last)
+ expect(described_class.current).to eq(:last)
+
+ # subsequent calls get everything from cache
+ expect(described_class.current).to eq(:last)
+ end
+ end
+ end
+
+ context 'restrict creating duplicates' do
+ before do
+ described_class.create_from_defaults
+ end
+
+ it 'raises an record creation violation if already created' do
+ expect { described_class.create_from_defaults }.to raise_error(ActiveRecord::RecordNotUnique)
+ end
end
context 'restricted signup domains' do
@@ -515,4 +623,22 @@ describe ApplicationSetting do
expect(setting.key_restriction_for(:foo)).to eq(described_class::FORBIDDEN_KEY_VALUE)
end
end
+
+ describe '#allow_signup?' do
+ it 'returns true' do
+ expect(setting.allow_signup?).to be_truthy
+ end
+
+ it 'returns false if signup is disabled' do
+ allow(setting).to receive(:signup_enabled?).and_return(false)
+
+ expect(setting.allow_signup?).to be_falsey
+ end
+
+ it 'returns false if password authentication is disabled for the web interface' do
+ allow(setting).to receive(:password_authentication_enabled_for_web?).and_return(false)
+
+ expect(setting.allow_signup?).to be_falsey
+ end
+ end
end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 47342f98283..81e35e6c931 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -16,6 +16,23 @@ describe Blob do
end
end
+ describe '.lazy' do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit_by(oid: 'e63f41fe459e62e1228fcef60d7189127aeba95a') }
+
+ it 'fetches all blobs when the first is accessed' do
+ changelog = described_class.lazy(project, commit.id, 'CHANGELOG')
+ contributing = described_class.lazy(project, commit.id, 'CONTRIBUTING.md')
+
+ expect(Gitlab::Git::Blob).to receive(:batch).once.and_call_original
+ expect(Gitlab::Git::Blob).not_to receive(:find)
+
+ # Access property so the values are loaded
+ changelog.id
+ contributing.id
+ end
+ end
+
describe '#data' do
context 'using a binary blob' do
it 'returns the data as-is' do
diff --git a/spec/models/blob_viewer/readme_spec.rb b/spec/models/blob_viewer/readme_spec.rb
index 926df21ffda..b9946c0315a 100644
--- a/spec/models/blob_viewer/readme_spec.rb
+++ b/spec/models/blob_viewer/readme_spec.rb
@@ -37,7 +37,7 @@ describe BlobViewer::Readme do
context 'when the wiki is not empty' do
before do
- WikiPages::CreateService.new(project, project.owner, title: 'home', content: 'Home page').execute
+ create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: 'Home page' })
end
it 'returns nil' do
diff --git a/spec/models/ci/artifact_blob_spec.rb b/spec/models/ci/artifact_blob_spec.rb
index d5ba088af53..4e72d9d748e 100644
--- a/spec/models/ci/artifact_blob_spec.rb
+++ b/spec/models/ci/artifact_blob_spec.rb
@@ -56,15 +56,14 @@ describe Ci::ArtifactBlob do
end
context 'txt extensions' do
- let(:entry) { build.artifacts_metadata_entry('other_artifacts_0.1.2/doc_sample.txt') }
+ let(:path) { 'other_artifacts_0.1.2/doc_sample.txt' }
+ let(:entry) { build.artifacts_metadata_entry(path) }
it 'returns a URL' do
url = subject.external_url(build.project, build)
expect(url).not_to be_nil
- expect(url).to start_with("http")
- expect(url).to match Gitlab.config.pages.host
- expect(url).to end_with(entry.path)
+ expect(url).to eq("http://#{project.namespace.path}.#{Gitlab.config.pages.host}/-/#{project.path}/-/jobs/#{build.id}/artifacts/#{path}")
end
end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 06f76b5501e..1795ee8e9a4 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -270,6 +270,23 @@ describe Ci::Build do
end
end
+ describe '#triggered_by?' do
+ subject { build.triggered_by?(user) }
+
+ context 'when user is owner' do
+ let(:build) { create(:ci_build, pipeline: pipeline, user: user) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when user is not owner' do
+ let(:another_user) { create(:user) }
+ let(:build) { create(:ci_build, pipeline: pipeline, user: another_user) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
describe '#detailed_status' do
it 'returns a detailed status' do
expect(build.detailed_status(user))
@@ -1271,6 +1288,7 @@ describe Ci::Build do
{ key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true },
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: project.web_url, public: true },
+ { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true },
{ key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
{ key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true },
{ key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
@@ -1743,19 +1761,34 @@ describe Ci::Build do
end
describe 'state transition when build fails' do
+ let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user) }
+
+ before do
+ allow(MergeRequests::AddTodoWhenBuildFailsService).to receive(:new).and_return(service)
+ allow(service).to receive(:close)
+ end
+
context 'when build is configured to be retried' do
- subject { create(:ci_build, :running, options: { retry: 3 }) }
+ subject { create(:ci_build, :running, options: { retry: 3 }, project: project, user: user) }
- it 'retries builds and assigns a same user to it' do
+ it 'retries build and assigns the same user to it' do
expect(described_class).to receive(:retry)
- .with(subject, subject.user)
+ .with(subject, user)
+
+ subject.drop!
+ end
+
+ it 'does not try to create a todo' do
+ project.add_developer(user)
+
+ expect(service).not_to receive(:commit_status_merge_requests)
subject.drop!
end
end
context 'when build is not configured to be retried' do
- subject { create(:ci_build, :running) }
+ subject { create(:ci_build, :running, project: project, user: user) }
it 'does not retry build' do
expect(described_class).not_to receive(:retry)
@@ -1770,6 +1803,14 @@ describe Ci::Build do
subject.drop!
end
+
+ it 'creates a todo' do
+ project.add_developer(user)
+
+ expect(service).to receive(:commit_status_merge_requests)
+
+ subject.drop!
+ end
end
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 2c9e7013b77..4cf0088ac9c 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -557,10 +557,23 @@ describe Ci::Pipeline, :mailer do
describe '#has_kubernetes_active?' do
context 'when kubernetes is active' do
- let(:project) { create(:kubernetes_project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns true' do
+ expect(pipeline).to have_kubernetes_active
+ end
+ end
- it 'returns true' do
- expect(pipeline).to have_kubernetes_active
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
@@ -625,38 +638,29 @@ describe Ci::Pipeline, :mailer do
shared_context 'with some outdated pipelines' do
before do
- create_pipeline(:canceled, 'ref', 'A')
- create_pipeline(:success, 'ref', 'A')
- create_pipeline(:failed, 'ref', 'B')
- create_pipeline(:skipped, 'feature', 'C')
+ create_pipeline(:canceled, 'ref', 'A', project)
+ create_pipeline(:success, 'ref', 'A', project)
+ create_pipeline(:failed, 'ref', 'B', project)
+ create_pipeline(:skipped, 'feature', 'C', project)
end
- def create_pipeline(status, ref, sha)
- create(:ci_empty_pipeline, status: status, ref: ref, sha: sha)
+ def create_pipeline(status, ref, sha, project)
+ create(
+ :ci_empty_pipeline,
+ status: status,
+ ref: ref,
+ sha: sha,
+ project: project
+ )
end
end
- describe '.latest' do
+ describe '.newest_first' do
include_context 'with some outdated pipelines'
- context 'when no ref is specified' do
- let(:pipelines) { described_class.latest.all }
-
- it 'returns the latest pipeline for the same ref and different sha' do
- expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C')
- expect(pipelines.map(&:status))
- .to contain_exactly('success', 'failed', 'skipped')
- end
- end
-
- context 'when ref is specified' do
- let(:pipelines) { described_class.latest('ref').all }
-
- it 'returns the latest pipeline for ref and different sha' do
- expect(pipelines.map(&:sha)).to contain_exactly('A', 'B')
- expect(pipelines.map(&:status))
- .to contain_exactly('success', 'failed')
- end
+ it 'returns the pipelines from new to old' do
+ expect(described_class.newest_first.pluck(:status))
+ .to eq(%w[skipped failed success canceled])
end
end
@@ -664,20 +668,14 @@ describe Ci::Pipeline, :mailer do
include_context 'with some outdated pipelines'
context 'when no ref is specified' do
- let(:latest_status) { described_class.latest_status }
-
- it 'returns the latest status for the same ref and different sha' do
- expect(latest_status).to eq(described_class.latest.status)
- expect(latest_status).to eq('failed')
+ it 'returns the status of the latest pipeline' do
+ expect(described_class.latest_status).to eq('skipped')
end
end
context 'when ref is specified' do
- let(:latest_status) { described_class.latest_status('ref') }
-
- it 'returns the latest status for ref and different sha' do
- expect(latest_status).to eq(described_class.latest_status('ref'))
- expect(latest_status).to eq('failed')
+ it 'returns the status of the latest pipeline for the given ref' do
+ expect(described_class.latest_status('ref')).to eq('failed')
end
end
end
@@ -686,7 +684,7 @@ describe Ci::Pipeline, :mailer do
include_context 'with some outdated pipelines'
let!(:latest_successful_pipeline) do
- create_pipeline(:success, 'ref', 'D')
+ create_pipeline(:success, 'ref', 'D', project)
end
it 'returns the latest successful pipeline' do
@@ -698,8 +696,13 @@ describe Ci::Pipeline, :mailer do
describe '.latest_successful_for_refs' do
include_context 'with some outdated pipelines'
- let!(:latest_successful_pipeline1) { create_pipeline(:success, 'ref1', 'D') }
- let!(:latest_successful_pipeline2) { create_pipeline(:success, 'ref2', 'D') }
+ let!(:latest_successful_pipeline1) do
+ create_pipeline(:success, 'ref1', 'D', project)
+ end
+
+ let!(:latest_successful_pipeline2) do
+ create_pipeline(:success, 'ref2', 'D', project)
+ end
it 'returns the latest successful pipeline for both refs' do
refs = %w(ref1 ref2 ref3)
@@ -708,6 +711,62 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '.latest_status_per_commit' do
+ let(:project) { create(:project) }
+
+ before do
+ pairs = [
+ %w[success ref1 123],
+ %w[manual master 123],
+ %w[failed ref 456]
+ ]
+
+ pairs.each do |(status, ref, sha)|
+ create(
+ :ci_empty_pipeline,
+ status: status,
+ ref: ref,
+ sha: sha,
+ project: project
+ )
+ end
+ end
+
+ context 'without a ref' do
+ it 'returns a Hash containing the latest status per commit for all refs' do
+ expect(described_class.latest_status_per_commit(%w[123 456]))
+ .to eq({ '123' => 'manual', '456' => 'failed' })
+ end
+
+ it 'only includes the status of the given commit SHAs' do
+ expect(described_class.latest_status_per_commit(%w[123]))
+ .to eq({ '123' => 'manual' })
+ end
+
+ context 'when there are two pipelines for a ref and SHA' do
+ it 'returns the status of the latest pipeline' do
+ create(
+ :ci_empty_pipeline,
+ status: 'failed',
+ ref: 'master',
+ sha: '123',
+ project: project
+ )
+
+ expect(described_class.latest_status_per_commit(%w[123]))
+ .to eq({ '123' => 'failed' })
+ end
+ end
+ end
+
+ context 'with a ref' do
+ it 'only includes the pipelines for the given ref' do
+ expect(described_class.latest_status_per_commit(%w[123 456], 'master'))
+ .to eq({ '123' => 'manual' })
+ end
+ end
+ end
+
describe '.internal_sources' do
subject { described_class.internal_sources }
@@ -1456,6 +1515,10 @@ describe Ci::Pipeline, :mailer do
create(:ci_build, :success, :artifacts, pipeline: pipeline)
end
+ it 'returns an Array' do
+ expect(pipeline.latest_builds_with_artifacts).to be_an_instance_of(Array)
+ end
+
it 'returns the latest builds' do
expect(pipeline.latest_builds_with_artifacts).to eq([build])
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 584dfe9a5c1..a93e7e233a8 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -473,7 +473,7 @@ describe Ci::Runner do
end
describe '.search' do
- let(:runner) { create(:ci_runner, token: '123abc') }
+ let(:runner) { create(:ci_runner, token: '123abc', description: 'test runner') }
it 'returns runners with a matching token' do
expect(described_class.search(runner.token)).to eq([runner])
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
new file mode 100644
index 00000000000..f8855079842
--- /dev/null
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -0,0 +1,102 @@
+require 'rails_helper'
+
+describe Clusters::Applications::Helm do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to validate_presence_of(:cluster) }
+
+ describe '#name' do
+ it 'is .application_name' do
+ expect(subject.name).to eq(described_class.application_name)
+ end
+
+ it 'is recorded in Clusters::Cluster::APPLICATIONS' do
+ expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class)
+ end
+ end
+
+ describe '#version' do
+ it 'defaults to Gitlab::Kubernetes::Helm::HELM_VERSION' do
+ expect(subject.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION)
+ end
+ end
+
+ describe '#status' do
+ let(:cluster) { create(:cluster) }
+
+ subject { described_class.new(cluster: cluster) }
+
+ it 'defaults to :not_installable' do
+ expect(subject.status_name).to be(:not_installable)
+ end
+
+ context 'when platform kubernetes is defined' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ it 'defaults to :installable' do
+ expect(subject.status_name).to be(:installable)
+ end
+ end
+ end
+
+ describe '#install_command' do
+ it 'has all the needed information' do
+ expect(subject.install_command).to have_attributes(name: subject.name, install_helm: true, chart: nil)
+ end
+ end
+
+ describe 'status state machine' do
+ describe '#make_installing' do
+ subject { create(:cluster_applications_helm, :scheduled) }
+
+ it 'is installing' do
+ subject.make_installing!
+
+ expect(subject).to be_installing
+ end
+ end
+
+ describe '#make_installed' do
+ subject { create(:cluster_applications_helm, :installing) }
+
+ it 'is installed' do
+ subject.make_installed
+
+ expect(subject).to be_installed
+ end
+ end
+
+ describe '#make_errored' do
+ subject { create(:cluster_applications_helm, :installing) }
+ let(:reason) { 'some errors' }
+
+ it 'is errored' do
+ subject.make_errored(reason)
+
+ expect(subject).to be_errored
+ expect(subject.status_reason).to eq(reason)
+ end
+ end
+
+ describe '#make_scheduled' do
+ subject { create(:cluster_applications_helm, :installable) }
+
+ it 'is scheduled' do
+ subject.make_scheduled
+
+ expect(subject).to be_scheduled
+ end
+
+ describe 'when was errored' do
+ subject { create(:cluster_applications_helm, :errored) }
+
+ it 'clears #status_reason' do
+ expect(subject.status_reason).not_to be_nil
+
+ subject.make_scheduled!
+
+ expect(subject.status_reason).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
new file mode 100644
index 00000000000..b83472e1944
--- /dev/null
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -0,0 +1,108 @@
+require 'rails_helper'
+
+describe Clusters::Applications::Ingress do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to validate_presence_of(:cluster) }
+
+ describe '#name' do
+ it 'is .application_name' do
+ expect(subject.name).to eq(described_class.application_name)
+ end
+
+ it 'is recorded in Clusters::Cluster::APPLICATIONS' do
+ expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class)
+ end
+ end
+
+ describe '#status' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ subject { described_class.new(cluster: cluster) }
+
+ it 'defaults to :not_installable' do
+ expect(subject.status_name).to be(:not_installable)
+ end
+
+ context 'when application helm is scheduled' do
+ before do
+ create(:cluster_applications_helm, :scheduled, cluster: cluster)
+ end
+
+ it 'defaults to :not_installable' do
+ expect(subject.status_name).to be(:not_installable)
+ end
+ end
+
+ context 'when application helm is installed' do
+ before do
+ create(:cluster_applications_helm, :installed, cluster: cluster)
+ end
+
+ it 'defaults to :installable' do
+ expect(subject.status_name).to be(:installable)
+ end
+ end
+ end
+
+ describe '#install_command' do
+ it 'has all the needed information' do
+ expect(subject.install_command).to have_attributes(name: subject.name, install_helm: false, chart: subject.chart)
+ end
+ end
+
+ describe 'status state machine' do
+ describe '#make_installing' do
+ subject { create(:cluster_applications_ingress, :scheduled) }
+
+ it 'is installing' do
+ subject.make_installing!
+
+ expect(subject).to be_installing
+ end
+ end
+
+ describe '#make_installed' do
+ subject { create(:cluster_applications_ingress, :installing) }
+
+ it 'is installed' do
+ subject.make_installed
+
+ expect(subject).to be_installed
+ end
+ end
+
+ describe '#make_errored' do
+ subject { create(:cluster_applications_ingress, :installing) }
+ let(:reason) { 'some errors' }
+
+ it 'is errored' do
+ subject.make_errored(reason)
+
+ expect(subject).to be_errored
+ expect(subject.status_reason).to eq(reason)
+ end
+ end
+
+ describe '#make_scheduled' do
+ subject { create(:cluster_applications_ingress, :installable) }
+
+ it 'is scheduled' do
+ subject.make_scheduled
+
+ expect(subject).to be_scheduled
+ end
+
+ describe 'when was errored' do
+ subject { create(:cluster_applications_ingress, :errored) }
+
+ it 'clears #status_reason' do
+ expect(subject.status_reason).not_to be_nil
+
+ subject.make_scheduled!
+
+ expect(subject.status_reason).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
new file mode 100644
index 00000000000..7f43e747000
--- /dev/null
+++ b/spec/models/clusters/cluster_spec.rb
@@ -0,0 +1,201 @@
+require 'spec_helper'
+
+describe Clusters::Cluster do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_many(:projects) }
+ it { is_expected.to have_one(:provider_gcp) }
+ it { is_expected.to have_one(:platform_kubernetes) }
+ it { is_expected.to delegate_method(:status).to(:provider) }
+ it { is_expected.to delegate_method(:status_reason).to(:provider) }
+ it { is_expected.to delegate_method(:status_name).to(:provider) }
+ it { is_expected.to delegate_method(:on_creation?).to(:provider) }
+ it { is_expected.to respond_to :project }
+
+ describe '.enabled' do
+ subject { described_class.enabled }
+
+ let!(:cluster) { create(:cluster, enabled: true) }
+
+ before do
+ create(:cluster, enabled: false)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe '.disabled' do
+ subject { described_class.disabled }
+
+ let!(:cluster) { create(:cluster, enabled: false) }
+
+ before do
+ create(:cluster, enabled: true)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe 'validation' do
+ subject { cluster.valid? }
+
+ context 'when validates name' do
+ context 'when provided by user' do
+ let!(:cluster) { build(:cluster, :provided_by_user, name: name) }
+
+ context 'when name is empty' do
+ let(:name) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is nil' do
+ let(:name) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is present' do
+ let(:name) { 'cluster-name-1' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when provided by gcp' do
+ let!(:cluster) { build(:cluster, :provided_by_gcp, name: name) }
+
+ context 'when name is shorter than 1' do
+ let(:name) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is longer than 63' do
+ let(:name) { 'a' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name includes invalid character' do
+ let(:name) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is present' do
+ let(:name) { 'cluster-name-1' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when record is persisted' do
+ let(:name) { 'cluster-name-1' }
+
+ before do
+ cluster.save!
+ end
+
+ context 'when name is changed' do
+ before do
+ cluster.name = 'new-cluster-name'
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when name is same' do
+ before do
+ cluster.name = name
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+ end
+
+ context 'when validates restrict_modification' do
+ context 'when creation is on going' do
+ let!(:cluster) { create(:cluster, :providing_by_gcp) }
+
+ it { expect(cluster.update(enabled: false)).to be_falsey }
+ end
+
+ context 'when creation is done' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ it { expect(cluster.update(enabled: false)).to be_truthy }
+ end
+ end
+ end
+
+ describe '#provider' do
+ subject { cluster.provider }
+
+ context 'when provider is gcp' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ it 'returns a provider' do
+ is_expected.to eq(cluster.provider_gcp)
+ expect(subject.class.name.deconstantize).to eq(Clusters::Providers.to_s)
+ end
+ end
+
+ context 'when provider is user' do
+ let(:cluster) { create(:cluster, :provided_by_user) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#platform' do
+ subject { cluster.platform }
+
+ context 'when platform is kubernetes' do
+ let(:cluster) { create(:cluster, :provided_by_user) }
+
+ it 'returns a platform' do
+ is_expected.to eq(cluster.platform_kubernetes)
+ expect(subject.class.name.deconstantize).to eq(Clusters::Platforms.to_s)
+ end
+ end
+ end
+
+ describe '#first_project' do
+ subject { cluster.first_project }
+
+ context 'when cluster belongs to a project' do
+ let(:cluster) { create(:cluster, :project) }
+ let(:project) { Clusters::Project.find_by_cluster_id(cluster.id).project }
+
+ it { is_expected.to eq(project) }
+ end
+
+ context 'when cluster does not belong to projects' do
+ let(:cluster) { create(:cluster) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#applications' do
+ set(:cluster) { create(:cluster) }
+
+ subject { cluster.applications }
+
+ context 'when none of applications are created' do
+ it 'returns a list of a new objects' do
+ is_expected.not_to be_empty
+ end
+ end
+
+ context 'when applications are created' do
+ let!(:helm) { create(:cluster_applications_helm, cluster: cluster) }
+ let!(:ingress) { create(:cluster_applications_ingress, cluster: cluster) }
+
+ it 'returns a list of created applications' do
+ is_expected.to contain_exactly(helm, ingress)
+ end
+ end
+ end
+end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
new file mode 100644
index 00000000000..53a4e545ff6
--- /dev/null
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -0,0 +1,266 @@
+require 'spec_helper'
+
+describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to be_kind_of(Gitlab::Kubernetes) }
+ it { is_expected.to be_kind_of(ReactiveCaching) }
+ it { is_expected.to respond_to :ca_pem }
+
+ describe 'before_validation' do
+ context 'when namespace includes upper case' do
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
+ let(:namespace) { 'ABC' }
+
+ it 'converts to lower case' do
+ expect(kubernetes.namespace).to eq('abc')
+ end
+ end
+ end
+
+ describe 'validation' do
+ subject { kubernetes.valid? }
+
+ context 'when validates namespace' do
+ let(:kubernetes) { build(:cluster_platform_kubernetes, :configured, namespace: namespace) }
+
+ context 'when namespace is blank' do
+ let(:namespace) { '' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when namespace is longer than 63' do
+ let(:namespace) { 'a' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when namespace includes invalid character' do
+ let(:namespace) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when namespace is vaild' do
+ let(:namespace) { 'namespace-123' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when validates api_url' do
+ let(:kubernetes) { build(:cluster_platform_kubernetes, :configured) }
+
+ before do
+ kubernetes.api_url = api_url
+ end
+
+ context 'when api_url is invalid url' do
+ let(:api_url) { '!!!!!!' }
+
+ it { expect(kubernetes.save).to be_falsey }
+ end
+
+ context 'when api_url is nil' do
+ let(:api_url) { nil }
+
+ it { expect(kubernetes.save).to be_falsey }
+ end
+
+ context 'when api_url is valid url' do
+ let(:api_url) { 'https://111.111.111.111' }
+
+ it { expect(kubernetes.save).to be_truthy }
+ end
+ end
+
+ context 'when validates token' do
+ let(:kubernetes) { build(:cluster_platform_kubernetes, :configured) }
+
+ before do
+ kubernetes.token = token
+ end
+
+ context 'when token is nil' do
+ let(:token) { nil }
+
+ it { expect(kubernetes.save).to be_falsey }
+ end
+ end
+ end
+
+ describe '#actual_namespace' do
+ subject { kubernetes.actual_namespace }
+
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:project) { cluster.project }
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
+
+ context 'when namespace is present' do
+ let(:namespace) { 'namespace-123' }
+
+ it { is_expected.to eq(namespace) }
+ end
+
+ context 'when namespace is not present' do
+ let(:namespace) { nil }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+ end
+
+ describe '#default_namespace' do
+ subject { kubernetes.send(:default_namespace) }
+
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured) }
+
+ context 'when cluster belongs to a project' do
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:project) { cluster.project }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+
+ context 'when cluster belongs to nothing' do
+ let!(:cluster) { create(:cluster, platform_kubernetes: kubernetes) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#predefined_variables' do
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:kubernetes) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem, token: token) }
+ let(:api_url) { 'https://kube.domain.com' }
+ let(:ca_pem) { 'CA PEM DATA' }
+ let(:token) { 'token' }
+
+ let(:kubeconfig) do
+ 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.strict_encode64(ca_pem)
+
+ YAML.dump(config)
+ end
+
+ shared_examples 'setting variables' do
+ it 'sets the variables' do
+ expect(kubernetes.predefined_variables).to include(
+ { key: 'KUBE_URL', value: api_url, public: true },
+ { key: 'KUBE_TOKEN', value: token, public: false },
+ { key: 'KUBE_NAMESPACE', value: namespace, public: true },
+ { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true },
+ { key: 'KUBE_CA_PEM', value: ca_pem, public: true },
+ { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
+ )
+ end
+ end
+
+ context 'namespace is provided' do
+ let(:namespace) { 'my-project' }
+
+ before do
+ kubernetes.namespace = namespace
+ end
+
+ it_behaves_like 'setting variables'
+ end
+
+ context 'no namespace provided' do
+ let(:namespace) { kubernetes.actual_namespace }
+
+ it_behaves_like 'setting variables'
+
+ it 'sets the KUBE_NAMESPACE' do
+ kube_namespace = kubernetes.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
+
+ expect(kube_namespace).not_to be_nil
+ expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
+ end
+ end
+ end
+
+ describe '#terminals' do
+ subject { service.terminals(environment) }
+
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
+ let(:project) { cluster.project }
+ let(:service) { create(:cluster_platform_kubernetes, :configured) }
+ let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
+
+ context 'with invalid pods' do
+ it 'returns no terminals' do
+ stub_reactive_cache(service, pods: [{ "bad" => "pod" }])
+
+ is_expected.to be_empty
+ end
+ end
+
+ context 'with valid pods' do
+ let(:pod) { kube_pod(app: environment.slug) }
+ let(:terminals) { kube_terminals(service, pod) }
+
+ before do
+ stub_reactive_cache(
+ service,
+ pods: [pod, pod, kube_pod(app: "should-be-filtered-out")]
+ )
+ end
+
+ it 'returns terminals' do
+ is_expected.to eq(terminals + terminals)
+ end
+
+ it 'uses max session time from settings' do
+ stub_application_setting(terminal_max_session_time: 600)
+
+ times = subject.map { |terminal| terminal[:max_session_time] }
+ expect(times).to eq [600, 600, 600, 600]
+ end
+ end
+ end
+
+ describe '#calculate_reactive_cache' do
+ subject { service.calculate_reactive_cache }
+
+ let!(:cluster) { create(:cluster, :project, enabled: enabled, platform_kubernetes: service) }
+ let(:service) { create(:cluster_platform_kubernetes, :configured) }
+ let(:enabled) { true }
+
+ context 'when cluster is disabled' do
+ let(:enabled) { false }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when kubernetes responds with valid pods' do
+ before do
+ stub_kubeclient_pods
+ end
+
+ it { is_expected.to eq(pods: [kube_pod]) }
+ end
+
+ context 'when kubernetes responds with 500s' do
+ before do
+ stub_kubeclient_pods(status: 500)
+ end
+
+ it { expect { subject }.to raise_error(KubeException) }
+ end
+
+ context 'when kubernetes responds with 404s' do
+ before do
+ stub_kubeclient_pods(status: 404)
+ end
+
+ it { is_expected.to eq(pods: []) }
+ end
+ end
+end
diff --git a/spec/models/clusters/project_spec.rb b/spec/models/clusters/project_spec.rb
new file mode 100644
index 00000000000..7d75d6ab345
--- /dev/null
+++ b/spec/models/clusters/project_spec.rb
@@ -0,0 +1,6 @@
+require 'spec_helper'
+
+describe Clusters::Project do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to belong_to(:project) }
+end
diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb
new file mode 100644
index 00000000000..b38b5e6bcad
--- /dev/null
+++ b/spec/models/clusters/providers/gcp_spec.rb
@@ -0,0 +1,183 @@
+require 'spec_helper'
+
+describe Clusters::Providers::Gcp do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to validate_presence_of(:zone) }
+
+ describe 'default_value_for' do
+ let(:gcp) { build(:cluster_provider_gcp) }
+
+ it "has default value" do
+ expect(gcp.zone).to eq('us-central1-a')
+ expect(gcp.num_nodes).to eq(3)
+ expect(gcp.machine_type).to eq('n1-standard-2')
+ end
+ end
+
+ describe 'validation' do
+ subject { gcp.valid? }
+
+ context 'when validates gcp_project_id' do
+ let(:gcp) { build(:cluster_provider_gcp, gcp_project_id: gcp_project_id) }
+
+ context 'when gcp_project_id is shorter than 1' do
+ let(:gcp_project_id) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when gcp_project_id is longer than 63' do
+ let(:gcp_project_id) { 'a' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when gcp_project_id includes invalid character' do
+ let(:gcp_project_id) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when gcp_project_id is valid' do
+ let(:gcp_project_id) { 'gcp-project-1' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when validates num_nodes' do
+ let(:gcp) { build(:cluster_provider_gcp, num_nodes: num_nodes) }
+
+ context 'when num_nodes is string' do
+ let(:num_nodes) { 'A3' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when num_nodes is nil' do
+ let(:num_nodes) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when num_nodes is smaller than 1' do
+ let(:num_nodes) { 0 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when num_nodes is valid' do
+ let(:num_nodes) { 3 }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ describe '#state_machine' do
+ context 'when any => [:created]' do
+ let(:gcp) { build(:cluster_provider_gcp, :creating) }
+
+ before do
+ gcp.make_created
+ end
+
+ it 'nullify access_token and operation_id' do
+ expect(gcp.access_token).to be_nil
+ expect(gcp.operation_id).to be_nil
+ expect(gcp).to be_created
+ end
+ end
+
+ context 'when any => [:creating]' do
+ let(:gcp) { build(:cluster_provider_gcp) }
+
+ context 'when operation_id is present' do
+ let(:operation_id) { 'operation-xxx' }
+
+ before do
+ gcp.make_creating(operation_id)
+ end
+
+ it 'sets operation_id' do
+ expect(gcp.operation_id).to eq(operation_id)
+ expect(gcp).to be_creating
+ end
+ end
+
+ context 'when operation_id is nil' do
+ let(:operation_id) { nil }
+
+ it 'raises an error' do
+ expect { gcp.make_creating(operation_id) }
+ .to raise_error('operation_id is required')
+ end
+ end
+ end
+
+ context 'when any => [:errored]' do
+ let(:gcp) { build(:cluster_provider_gcp, :creating) }
+ let(:status_reason) { 'err msg' }
+
+ it 'nullify access_token and operation_id' do
+ gcp.make_errored(status_reason)
+
+ expect(gcp.access_token).to be_nil
+ expect(gcp.operation_id).to be_nil
+ expect(gcp.status_reason).to eq(status_reason)
+ expect(gcp).to be_errored
+ end
+
+ context 'when status_reason is nil' do
+ let(:gcp) { build(:cluster_provider_gcp, :errored) }
+
+ it 'does not set status_reason' do
+ gcp.make_errored(nil)
+
+ expect(gcp.status_reason).not_to be_nil
+ end
+ end
+ end
+ end
+
+ describe '#on_creation?' do
+ subject { gcp.on_creation? }
+
+ context 'when status is creating' do
+ let(:gcp) { create(:cluster_provider_gcp, :creating) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when status is created' do
+ let(:gcp) { create(:cluster_provider_gcp, :created) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#api_client' do
+ subject { gcp.api_client }
+
+ context 'when status is creating' do
+ let(:gcp) { build(:cluster_provider_gcp, :creating) }
+
+ it 'returns Cloud Platform API clinet' do
+ expect(subject).to be_an_instance_of(GoogleApi::CloudPlatform::Client)
+ expect(subject.access_token).to eq(gcp.access_token)
+ end
+ end
+
+ context 'when status is created' do
+ let(:gcp) { build(:cluster_provider_gcp, :created) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when status is errored' do
+ let(:gcp) { build(:cluster_provider_gcp, :errored) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb
new file mode 100644
index 00000000000..066fe7d154e
--- /dev/null
+++ b/spec/models/commit_collection_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe CommitCollection do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit }
+
+ describe '#each' do
+ it 'yields every commit' do
+ collection = described_class.new(project, [commit])
+
+ expect { |b| collection.each(&b) }.to yield_with_args(commit)
+ end
+ end
+
+ describe '#with_pipeline_status' do
+ it 'sets the pipeline status for every commit so no additional queries are necessary' do
+ create(
+ :ci_empty_pipeline,
+ ref: 'master',
+ sha: commit.id,
+ status: 'success',
+ project: project
+ )
+
+ collection = described_class.new(project, [commit])
+ collection.with_pipeline_status
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ expect(commit.status).to eq('success')
+ end
+
+ expect(recorder.count).to be_zero
+ end
+ end
+
+ describe '#respond_to_missing?' do
+ it 'returns true when the underlying Array responds to the message' do
+ collection = described_class.new(project, [])
+
+ expect(collection.respond_to?(:last)).to eq(true)
+ end
+
+ it 'returns false when the underlying Array does not respond to the message' do
+ collection = described_class.new(project, [])
+
+ expect(collection.respond_to?(:foo)).to eq(false)
+ end
+ end
+
+ describe '#method_missing' do
+ it 'delegates undefined methods to the underlying Array' do
+ collection = described_class.new(project, [commit])
+
+ expect(collection.length).to eq(1)
+ expect(collection.last).to eq(commit)
+ expect(collection).not_to be_empty
+ end
+ end
+end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index e3cfa149e3a..d18a5c9dfa6 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -351,12 +351,19 @@ eos
end
it 'gives compound status from latest pipelines if ref is nil' do
- expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status)
- expect(commit.status(nil)).to eq('failed')
+ expect(commit.status(nil)).to eq(pipeline_from_fix.status)
end
end
end
+ describe '#set_status_for_ref' do
+ it 'sets the status for a given reference' do
+ commit.set_status_for_ref('master', 'failed')
+
+ expect(commit.status('master')).to eq('failed')
+ end
+ end
+
describe '#participants' do
let(:user1) { build(:user) }
let(:user2) { build(:user) }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 858ec831200..c536dab2681 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe CommitStatus do
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
- let(:pipeline) do
+ set(:pipeline) do
create(:ci_pipeline, project: project, sha: project.commit.id)
end
@@ -464,4 +464,73 @@ describe CommitStatus do
it { is_expected.to be_script_failure }
end
end
+
+ describe 'ensure stage assignment' do
+ context 'when commit status has a stage_id assigned' do
+ let!(:stage) do
+ create(:ci_stage_entity, project: project, pipeline: pipeline)
+ end
+
+ let(:commit_status) do
+ create(:commit_status, stage_id: stage.id, name: 'rspec', stage: 'test')
+ end
+
+ it 'does not create a new stage' do
+ expect { commit_status }.not_to change { Ci::Stage.count }
+ expect(commit_status.stage_id).to eq stage.id
+ end
+ end
+
+ context 'when commit status does not have a stage_id assigned' do
+ let(:commit_status) do
+ create(:commit_status, name: 'rspec', stage: 'test', status: :success)
+ end
+
+ let(:stage) { Ci::Stage.first }
+
+ it 'creates a new stage' do
+ expect { commit_status }.to change { Ci::Stage.count }.by(1)
+
+ expect(stage.name).to eq 'test'
+ expect(stage.project).to eq commit_status.project
+ expect(stage.pipeline).to eq commit_status.pipeline
+ expect(stage.status).to eq commit_status.status
+ expect(commit_status.stage_id).to eq stage.id
+ end
+ end
+
+ context 'when commit status does not have stage but it exists' do
+ let!(:stage) do
+ create(:ci_stage_entity, project: project,
+ pipeline: pipeline,
+ name: 'test')
+ end
+
+ let(:commit_status) do
+ create(:commit_status, project: project,
+ pipeline: pipeline,
+ name: 'rspec',
+ stage: 'test',
+ status: :success)
+ end
+
+ it 'uses existing stage' do
+ expect { commit_status }.not_to change { Ci::Stage.count }
+
+ expect(commit_status.stage_id).to eq stage.id
+ expect(stage.reload.status).to eq commit_status.status
+ end
+ end
+
+ context 'when commit status is being imported' do
+ let(:commit_status) do
+ create(:commit_status, name: 'rspec', stage: 'test', importing: true)
+ end
+
+ it 'does not create a new stage' do
+ expect { commit_status }.not_to change { Ci::Stage.count }
+ expect(commit_status.stage_id).not_to be_present
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/avatarable_spec.rb b/spec/models/concerns/avatarable_spec.rb
new file mode 100644
index 00000000000..cbdc438be0b
--- /dev/null
+++ b/spec/models/concerns/avatarable_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Avatarable do
+ subject { create(:project, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
+
+ let(:gitlab_host) { "https://gitlab.example.com" }
+ let(:relative_url_root) { "/gitlab" }
+ let(:asset_host) { "https://gitlab-assets.example.com" }
+
+ before do
+ stub_config_setting(base_url: gitlab_host)
+ stub_config_setting(relative_url_root: relative_url_root)
+ end
+
+ describe '#avatar_path' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:has_asset_host, :visibility_level, :only_path, :avatar_path) do
+ true | Project::PRIVATE | true | [gitlab_host, relative_url_root, subject.avatar.url]
+ true | Project::PRIVATE | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ true | Project::INTERNAL | true | [gitlab_host, relative_url_root, subject.avatar.url]
+ true | Project::INTERNAL | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ true | Project::PUBLIC | true | [subject.avatar.url]
+ true | Project::PUBLIC | false | [asset_host, subject.avatar.url]
+ false | Project::PRIVATE | true | [relative_url_root, subject.avatar.url]
+ false | Project::PRIVATE | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ false | Project::INTERNAL | true | [relative_url_root, subject.avatar.url]
+ false | Project::INTERNAL | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ false | Project::PUBLIC | true | [relative_url_root, subject.avatar.url]
+ false | Project::PUBLIC | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ end
+
+ with_them do
+ before do
+ allow(ActionController::Base).to receive(:asset_host).and_return(has_asset_host ? asset_host : nil)
+ subject.visibility_level = visibility_level
+ end
+
+ it 'returns the expected avatar path' do
+ expect(subject.avatar_path(only_path: only_path)).to eq(avatar_path.join)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/group_descendant_spec.rb b/spec/models/concerns/group_descendant_spec.rb
new file mode 100644
index 00000000000..c163fb01a81
--- /dev/null
+++ b/spec/models/concerns/group_descendant_spec.rb
@@ -0,0 +1,166 @@
+require 'spec_helper'
+
+describe GroupDescendant, :nested_groups do
+ let(:parent) { create(:group) }
+ let(:subgroup) { create(:group, parent: parent) }
+ let(:subsub_group) { create(:group, parent: subgroup) }
+
+ def all_preloaded_groups(*groups)
+ groups + [parent, subgroup, subsub_group]
+ end
+
+ context 'for a group' do
+ describe '#hierarchy' do
+ it 'only queries once for the ancestors' do
+ # make sure the subsub_group does not have anything cached
+ test_group = create(:group, parent: subsub_group).reload
+
+ query_count = ActiveRecord::QueryRecorder.new { test_group.hierarchy }.count
+
+ expect(query_count).to eq(1)
+ end
+
+ it 'only queries once for the ancestors when a top is given' do
+ test_group = create(:group, parent: subsub_group).reload
+
+ recorder = ActiveRecord::QueryRecorder.new { test_group.hierarchy(subgroup) }
+ expect(recorder.count).to eq(1)
+ end
+
+ it 'builds a hierarchy for a group' do
+ expected_hierarchy = { parent => { subgroup => subsub_group } }
+
+ expect(subsub_group.hierarchy).to eq(expected_hierarchy)
+ end
+
+ it 'builds a hierarchy upto a specified parent' do
+ expected_hierarchy = { subgroup => subsub_group }
+
+ expect(subsub_group.hierarchy(parent)).to eq(expected_hierarchy)
+ end
+
+ it 'raises an error if specifying a base that is not part of the tree' do
+ expect { subsub_group.hierarchy(build_stubbed(:group)) }
+ .to raise_error('specified top is not part of the tree')
+ end
+ end
+
+ describe '.build_hierarchy' do
+ it 'combines hierarchies until the top' do
+ other_subgroup = create(:group, parent: parent)
+ other_subsub_group = create(:group, parent: subgroup)
+
+ groups = all_preloaded_groups(other_subgroup, subsub_group, other_subsub_group)
+
+ expected_hierarchy = { parent => [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }] }
+
+ expect(described_class.build_hierarchy(groups)).to eq(expected_hierarchy)
+ end
+
+ it 'combines upto a given parent' do
+ other_subgroup = create(:group, parent: parent)
+ other_subsub_group = create(:group, parent: subgroup)
+
+ groups = [other_subgroup, subsub_group, other_subsub_group]
+ groups << subgroup # Add the parent as if it was preloaded
+
+ expected_hierarchy = [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }]
+ expect(described_class.build_hierarchy(groups, parent)).to eq(expected_hierarchy)
+ end
+
+ it 'handles building a tree out of order' do
+ other_subgroup = create(:group, parent: parent)
+ other_subgroup2 = create(:group, parent: parent)
+ other_subsub_group = create(:group, parent: other_subgroup)
+
+ groups = all_preloaded_groups(subsub_group, other_subgroup2, other_subsub_group, other_subgroup)
+ expected_hierarchy = { parent => [{ subgroup => subsub_group }, other_subgroup2, { other_subgroup => other_subsub_group }] }
+
+ expect(described_class.build_hierarchy(groups)).to eq(expected_hierarchy)
+ end
+
+ it 'raises an error if not all elements were preloaded' do
+ expect { described_class.build_hierarchy([subsub_group]) }
+ .to raise_error('parent was not preloaded')
+ end
+ end
+ end
+
+ context 'for a project' do
+ let(:project) { create(:project, namespace: subsub_group) }
+
+ describe '#hierarchy' do
+ it 'builds a hierarchy for a project' do
+ expected_hierarchy = { parent => { subgroup => { subsub_group => project } } }
+
+ expect(project.hierarchy).to eq(expected_hierarchy)
+ end
+
+ it 'builds a hierarchy upto a specified parent' do
+ expected_hierarchy = { subsub_group => project }
+
+ expect(project.hierarchy(subgroup)).to eq(expected_hierarchy)
+ end
+ end
+
+ describe '.build_hierarchy' do
+ it 'combines hierarchies until the top' do
+ other_project = create(:project, namespace: parent)
+ other_subgroup_project = create(:project, namespace: subgroup)
+
+ elements = all_preloaded_groups(other_project, subsub_group, other_subgroup_project)
+
+ expected_hierarchy = { parent => [other_project, { subgroup => [subsub_group, other_subgroup_project] }] }
+
+ expect(described_class.build_hierarchy(elements)).to eq(expected_hierarchy)
+ end
+
+ it 'combines upto a given parent' do
+ other_project = create(:project, namespace: parent)
+ other_subgroup_project = create(:project, namespace: subgroup)
+
+ elements = [other_project, subsub_group, other_subgroup_project]
+ elements << subgroup # Added as if it was preloaded
+
+ expected_hierarchy = [other_project, { subgroup => [subsub_group, other_subgroup_project] }]
+
+ expect(described_class.build_hierarchy(elements, parent)).to eq(expected_hierarchy)
+ end
+
+ it 'merges to elements in the same hierarchy' do
+ expected_hierarchy = { parent => subgroup }
+
+ expect(described_class.build_hierarchy([parent, subgroup])).to eq(expected_hierarchy)
+ end
+
+ it 'merges complex hierarchies' do
+ project = create(:project, namespace: parent)
+ sub_project = create(:project, namespace: subgroup)
+ subsubsub_group = create(:group, parent: subsub_group)
+ subsub_project = create(:project, namespace: subsub_group)
+ subsubsub_project = create(:project, namespace: subsubsub_group)
+ other_subgroup = create(:group, parent: parent)
+ other_subproject = create(:project, namespace: other_subgroup)
+
+ elements = [project, subsubsub_project, sub_project, other_subproject, subsub_project]
+ # Add parent groups as if they were preloaded
+ elements += [other_subgroup, subsubsub_group, subsub_group, subgroup]
+
+ expected_hierarchy = [
+ project,
+ {
+ subgroup => [
+ { subsub_group => [{ subsubsub_group => subsubsub_project }, subsub_project] },
+ sub_project
+ ]
+ },
+ { other_subgroup => other_subproject }
+ ]
+
+ actual_hierarchy = described_class.build_hierarchy(elements, parent)
+
+ expect(actual_hierarchy).to eq(expected_hierarchy)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/has_variable_spec.rb b/spec/models/concerns/has_variable_spec.rb
index f4b24e6d1d9..f87869a2fdc 100644
--- a/spec/models/concerns/has_variable_spec.rb
+++ b/spec/models/concerns/has_variable_spec.rb
@@ -9,6 +9,24 @@ describe HasVariable do
it { is_expected.not_to allow_value('foo bar').for(:key) }
it { is_expected.not_to allow_value('foo/bar').for(:key) }
+ describe '#key=' do
+ context 'when the new key is nil' do
+ it 'strips leading and trailing whitespaces' do
+ subject.key = nil
+
+ expect(subject.key).to eq('')
+ end
+ end
+
+ context 'when the new key has leadind and trailing whitespaces' do
+ it 'strips leading and trailing whitespaces' do
+ subject.key = ' my key '
+
+ expect(subject.key).to eq('my key')
+ end
+ end
+ end
+
describe '#value' do
before do
subject.value = 'secret'
diff --git a/spec/models/concerns/ignorable_column_spec.rb b/spec/models/concerns/ignorable_column_spec.rb
index dba9fe43327..b70f2331a0e 100644
--- a/spec/models/concerns/ignorable_column_spec.rb
+++ b/spec/models/concerns/ignorable_column_spec.rb
@@ -5,7 +5,11 @@ describe IgnorableColumn do
Class.new do
def self.columns
# This method does not have access to "double"
- [Struct.new(:name).new('id'), Struct.new(:name).new('title')]
+ [
+ Struct.new(:name).new('id'),
+ Struct.new(:name).new('title'),
+ Struct.new(:name).new('date')
+ ]
end
end
end
@@ -18,7 +22,7 @@ describe IgnorableColumn do
describe '.columns' do
it 'returns the columns, excluding the ignored ones' do
- model.ignore_column(:title)
+ model.ignore_column(:title, :date)
expect(model.columns.map(&:name)).to eq(%w(id))
end
@@ -30,9 +34,9 @@ describe IgnorableColumn do
end
it 'returns the names of the ignored columns' do
- model.ignore_column(:title)
+ model.ignore_column(:title, :date)
- expect(model.ignored_columns).to eq(Set.new(%w(title)))
+ expect(model.ignored_columns).to eq(Set.new(%w(title date)))
end
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index fb5fb7daaab..a53b59c4e08 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Issuable do
let(:issuable_class) { Issue }
- let(:issue) { create(:issue) }
+ let(:issue) { create(:issue, title: 'An issue', description: 'A description') }
let(:user) { create(:user) }
describe "Associations" do
@@ -67,6 +67,7 @@ describe Issuable do
describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
+ let!(:searchable_issue2) { create(:issue, title: 'Aw') }
it 'returns issues with a matching title' do
expect(issuable_class.search(searchable_issue.title))
@@ -86,8 +87,8 @@ describe Issuable 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)
+ it 'returns issues with a matching title for a query shorter than 3 chars' do
+ expect(issuable_class.search(searchable_issue2.title.downcase)).to eq([searchable_issue2])
end
end
@@ -95,6 +96,7 @@ describe Issuable do
let!(:searchable_issue) do
create(:issue, title: "Searchable awesome issue", description: 'Many cute kittens')
end
+ let!(:searchable_issue2) { create(:issue, title: "Aw", description: "Cu") }
it 'returns issues with a matching title' do
expect(issuable_class.full_search(searchable_issue.title))
@@ -133,8 +135,8 @@ describe Issuable 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)
+ it 'returns issues with a matching description for a query shorter than 3 chars' do
+ expect(issuable_class.full_search(searchable_issue2.description.downcase)).to eq([searchable_issue2])
end
end
@@ -264,55 +266,90 @@ describe Issuable do
end
end
- describe "#to_hook_data" do
- let(:data) { issue.to_hook_data(user) }
- let(:project) { issue.project }
+ describe '#to_hook_data' do
+ let(:builder) { double }
+
+ context 'labels are updated' do
+ let(:labels) { create_list(:label, 2) }
- it "returns correct hook data" do
- expect(data[:object_kind]).to eq("issue")
- expect(data[:user]).to eq(user.hook_attrs)
- expect(data[:object_attributes]).to eq(issue.hook_attrs)
- expect(data).not_to have_key(:assignee)
+ before do
+ issue.update(labels: [labels[1]])
+ expect(Gitlab::HookData::IssuableBuilder)
+ .to receive(:new).with(issue).and_return(builder)
+ end
+
+ it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
+ expect(builder).to receive(:build).with(
+ user: user,
+ changes: hash_including(
+ 'labels' => [[labels[0].hook_attrs], [labels[1].hook_attrs]]
+ ))
+
+ issue.to_hook_data(user, old_associations: { labels: [labels[0]] })
+ end
end
- context "issue is assigned" do
+ context 'total_time_spent is updated' do
before do
- issue.assignees << user
+ issue.spend_time(duration: 2, user: user, spent_at: Time.now)
+ issue.save
+ expect(Gitlab::HookData::IssuableBuilder)
+ .to receive(:new).with(issue).and_return(builder)
end
- it "returns correct hook data" do
- expect(data[:assignees].first).to eq(user.hook_attrs)
+ it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
+ expect(builder).to receive(:build).with(
+ user: user,
+ changes: hash_including(
+ 'total_time_spent' => [1, 2]
+ ))
+
+ issue.to_hook_data(user, old_associations: { total_time_spent: 1 })
end
end
- context "merge_request is assigned" do
- let(:merge_request) { create(:merge_request) }
- let(:data) { merge_request.to_hook_data(user) }
+ context 'issue is assigned' do
+ let(:user2) { create(:user) }
before do
- merge_request.update_attribute(:assignee, user)
+ issue.assignees << user << user2
+ expect(Gitlab::HookData::IssuableBuilder)
+ .to receive(:new).with(issue).and_return(builder)
end
- it "returns correct hook data" do
- expect(data[:object_attributes]['assignee_id']).to eq(user.id)
- expect(data[:assignee]).to eq(user.hook_attrs)
+ it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
+ expect(builder).to receive(:build).with(
+ user: user,
+ changes: hash_including(
+ 'assignees' => [[user.hook_attrs], [user.hook_attrs, user2.hook_attrs]]
+ ))
+
+ issue.to_hook_data(user, old_associations: { assignees: [user] })
end
end
- context 'issue has labels' do
- let(:labels) { [create(:label), create(:label)] }
+ context 'merge_request is assigned' do
+ let(:merge_request) { create(:merge_request) }
+ let(:user2) { create(:user) }
before do
- issue.update_attribute(:labels, labels)
+ merge_request.update(assignee: user)
+ merge_request.update(assignee: user2)
+ expect(Gitlab::HookData::IssuableBuilder)
+ .to receive(:new).with(merge_request).and_return(builder)
end
- it 'includes labels in the hook data' do
- expect(data[:labels]).to eq(labels.map(&:hook_attrs))
+ it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
+ expect(builder).to receive(:build).with(
+ user: user,
+ changes: hash_including(
+ 'assignee_id' => [user.id, user2.id],
+ 'assignee' => [user.hook_attrs, user2.hook_attrs]
+ ))
+
+ merge_request.to_hook_data(user, old_associations: { assignees: [user] })
end
end
-
- include_examples 'project hook data'
- include_examples 'deprecated repository hook data'
end
describe '#labels_array' do
diff --git a/spec/models/concerns/loaded_in_group_list_spec.rb b/spec/models/concerns/loaded_in_group_list_spec.rb
new file mode 100644
index 00000000000..7a279547a3a
--- /dev/null
+++ b/spec/models/concerns/loaded_in_group_list_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe LoadedInGroupList do
+ let(:parent) { create(:group) }
+ subject(:found_group) { Group.with_selects_for_list.find_by(id: parent.id) }
+
+ describe '.with_selects_for_list' do
+ it 'includes the preloaded counts for groups' do
+ create(:group, parent: parent)
+ create(:project, namespace: parent)
+ parent.add_developer(create(:user))
+
+ found_group = Group.with_selects_for_list.find_by(id: parent.id)
+
+ expect(found_group.preloaded_project_count).to eq(1)
+ expect(found_group.preloaded_subgroup_count).to eq(1)
+ expect(found_group.preloaded_member_count).to eq(1)
+ end
+
+ context 'with archived projects' do
+ it 'counts including archived projects when `true` is passed' do
+ create(:project, namespace: parent, archived: true)
+ create(:project, namespace: parent)
+
+ found_group = Group.with_selects_for_list(archived: 'true').find_by(id: parent.id)
+
+ expect(found_group.preloaded_project_count).to eq(2)
+ end
+
+ it 'counts only archived projects when `only` is passed' do
+ create_list(:project, 2, namespace: parent, archived: true)
+ create(:project, namespace: parent)
+
+ found_group = Group.with_selects_for_list(archived: 'only').find_by(id: parent.id)
+
+ expect(found_group.preloaded_project_count).to eq(2)
+ end
+ end
+ end
+
+ describe '#children_count' do
+ it 'counts groups and projects' do
+ create(:group, parent: parent)
+ create(:project, namespace: parent)
+
+ expect(found_group.children_count).to eq(2)
+ end
+ end
+end
diff --git a/spec/models/concerns/manual_inverse_association_spec.rb b/spec/models/concerns/manual_inverse_association_spec.rb
new file mode 100644
index 00000000000..aad40883854
--- /dev/null
+++ b/spec/models/concerns/manual_inverse_association_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe ManualInverseAssociation do
+ let(:model) do
+ Class.new(MergeRequest) do
+ belongs_to :manual_association, class_name: 'MergeRequestDiff', foreign_key: :latest_merge_request_diff_id
+ manual_inverse_association :manual_association, :merge_request
+ end
+ end
+
+ before do
+ stub_const("#{described_class}::Model", model)
+ end
+
+ let(:instance) { create(:merge_request).becomes(model) }
+
+ describe '.manual_inverse_association' do
+ context 'when the relation exists' do
+ before do
+ instance.create_merge_request_diff
+ instance.reload
+ end
+
+ it 'loads the relation' do
+ expect(instance.manual_association).to be_an_instance_of(MergeRequestDiff)
+ end
+
+ it 'does not perform extra queries after loading' do
+ instance.manual_association
+
+ expect { instance.manual_association.merge_request }
+ .not_to exceed_query_limit(0)
+ end
+
+ it 'passes arguments to the default association method, to allow reloading' do
+ query_count = ActiveRecord::QueryRecorder.new do
+ instance.manual_association
+ instance.manual_association(true)
+ end.count
+
+ expect(query_count).to eq(2)
+ end
+ end
+
+ context 'when the relation does not return a value' do
+ it 'does not try to set an inverse' do
+ expect(instance.manual_association).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 66353935427..9048da0c73d 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -186,4 +186,21 @@ describe Milestone, 'Milestoneish' do
expect(milestone.elapsed_days).to eq(2)
end
end
+
+ describe '#total_issue_time_spent' do
+ it 'calculates total issue time spent' do
+ closed_issue_1.spend_time(duration: 300, user: author)
+ closed_issue_1.save!
+ closed_issue_2.spend_time(duration: 600, user: assignee)
+ closed_issue_2.save!
+
+ expect(milestone.total_issue_time_spent).to eq(900)
+ end
+ end
+
+ describe '#human_total_issue_time_spent' do
+ it 'returns nil if no time has been spent' do
+ expect(milestone.human_total_issue_time_spent).to be_nil
+ end
+ end
end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index ab8773b7ede..3106207811a 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -134,6 +134,7 @@ describe Group, 'Routable' do
context 'with RequestStore active', :request_store do
it 'does not load the route table more than once' do
+ group.expires_full_path_cache
expect(group).to receive(:uncached_full_path).once.and_call_original
3.times { group.full_path }
diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb
index 28ff8158e0e..45dfb136aea 100644
--- a/spec/models/concerns/subscribable_spec.rb
+++ b/spec/models/concerns/subscribable_spec.rb
@@ -6,6 +6,12 @@ describe Subscribable, 'Subscribable' do
let(:user_1) { create(:user) }
describe '#subscribed?' do
+ context 'without user' do
+ it 'returns false' do
+ expect(resource.subscribed?(nil, project)).to be_falsey
+ end
+ end
+
context 'without project' do
it 'returns false when no subscription exists' do
expect(resource.subscribed?(user_1)).to be_falsey
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 882afeccfc6..dfb83578fce 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -12,7 +12,7 @@ shared_examples 'TokenAuthenticatable' do
end
describe User, 'TokenAuthenticatable' do
- let(:token_field) { :authentication_token }
+ let(:token_field) { :rss_token }
it_behaves_like 'TokenAuthenticatable'
describe 'ensures authentication token' do
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index eb0a3e9e0d3..8389d5c5430 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -105,7 +105,7 @@ describe DiffNote do
describe "#line_code" do
it "returns the correct line code" do
- line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.formatter.new_line, 15)
+ line_code = Gitlab::Git.diff_line_code(position.file_path, position.formatter.new_line, 15)
expect(subject.line_code).to eq(line_code)
end
@@ -283,6 +283,12 @@ describe DiffNote do
expect(diff_line).to be nil
expect(subject).to be_valid
end
+
+ it "does not update the position" do
+ expect(subject).not_to receive(:update_position)
+
+ subject.save
+ end
end
it "returns true for on_image?" do
diff --git a/spec/models/diff_viewer/base_spec.rb b/spec/models/diff_viewer/base_spec.rb
index b26de3f3b97..c90b32c5d77 100644
--- a/spec/models/diff_viewer/base_spec.rb
+++ b/spec/models/diff_viewer/base_spec.rb
@@ -32,10 +32,8 @@ describe DiffViewer::Base do
end
context 'when the binaryness does not match' do
- before do
- allow(diff_file.old_blob).to receive(:binary?).and_return(false)
- allow(diff_file.new_blob).to receive(:binary?).and_return(false)
- end
+ let(:commit) { project.commit_by(oid: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('Gemfile.zip') }
it 'returns false' do
expect(viewer_class.can_render?(diff_file)).to be_falsey
@@ -60,8 +58,7 @@ describe DiffViewer::Base do
context 'when the binaryness does not match' do
before do
- allow(diff_file.old_blob).to receive(:binary?).and_return(true)
- allow(diff_file.new_blob).to receive(:binary?).and_return(true)
+ allow_any_instance_of(Blob).to receive(:binary?).and_return(true)
end
it 'returns false' do
@@ -77,12 +74,12 @@ describe DiffViewer::Base do
end
context 'when the file was renamed and only the old blob is supported' do
- let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+ let(:commit) { project.commit_by(oid: '2f63565e7aac07bcdadb654e253078b727143ec4') }
let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') }
before do
allow(diff_file).to receive(:renamed_file?).and_return(true)
- allow(diff_file.new_blob).to receive(:extension).and_return('jpeg')
+ viewer_class.extensions = %w(notjpg)
end
it 'returns false' do
@@ -94,8 +91,7 @@ describe DiffViewer::Base do
describe '#collapsed?' do
context 'when the combined blob size is larger than the collapse limit' do
before do
- allow(diff_file.old_blob).to receive(:raw_size).and_return(512.kilobytes)
- allow(diff_file.new_blob).to receive(:raw_size).and_return(513.kilobytes)
+ allow(diff_file).to receive(:raw_size).and_return(1025.kilobytes)
end
it 'returns true' do
@@ -113,8 +109,7 @@ describe DiffViewer::Base do
describe '#too_large?' do
context 'when the combined blob size is larger than the size limit' do
before do
- allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes)
- allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes)
+ allow(diff_file).to receive(:raw_size).and_return(6.megabytes)
end
it 'returns true' do
@@ -132,8 +127,7 @@ describe DiffViewer::Base do
describe '#render_error' do
context 'when the combined blob size is larger than the size limit' do
before do
- allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes)
- allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes)
+ allow(diff_file).to receive(:raw_size).and_return(6.megabytes)
end
it 'returns :too_large' do
diff --git a/spec/models/diff_viewer/server_side_spec.rb b/spec/models/diff_viewer/server_side_spec.rb
index 92e613f92de..98a8f6d4cc9 100644
--- a/spec/models/diff_viewer/server_side_spec.rb
+++ b/spec/models/diff_viewer/server_side_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe DiffViewer::ServerSide do
- let(:project) { create(:project, :repository) }
- let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
- let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') }
+ set(:project) { create(:project, :repository) }
+ let(:commit) { project.commit_by(oid: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
+ let!(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') }
let(:viewer_class) do
Class.new(DiffViewer::Base) do
@@ -15,8 +15,7 @@ describe DiffViewer::ServerSide do
describe '#prepare!' do
it 'loads all diff file data' do
- expect(diff_file.old_blob).to receive(:load_all_data!)
- expect(diff_file.new_blob).to receive(:load_all_data!)
+ expect(Blob).to receive(:lazy).at_least(:twice)
subject.prepare!
end
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index b32dd31ae6d..47eb0717c0c 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -40,4 +40,12 @@ describe Email do
expect(user.emails.confirmed.count).to eq 1
end
end
+
+ describe 'delegation' do
+ let(:user) { create(:user) }
+
+ it 'delegates to :user' do
+ expect(build(:email, user: user).username).to eq user.username
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 25e5d155894..6f24a039998 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -18,7 +18,6 @@ describe Environment do
it { is_expected.to validate_length_of(:slug).is_at_most(24) }
it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
- it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) }
describe '.order_by_last_deployed_at' do
let(:project) { create(:project, :repository) }
@@ -328,15 +327,28 @@ describe Environment do
context 'when the enviroment is available' do
context 'with a deployment service' do
- let(:project) { create(:kubernetes_project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ context 'and a deployment' do
+ let!(:deployment) { create(:deployment, environment: environment) }
+ it { is_expected.to be_truthy }
+ end
- context 'and a deployment' do
- let!(:deployment) { create(:deployment, environment: environment) }
- it { is_expected.to be_truthy }
+ context 'but no deployments' do
+ it { is_expected.to be_falsy }
+ end
end
- context 'but no deployments' do
- it { is_expected.to be_falsy }
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
@@ -357,7 +369,6 @@ describe Environment do
end
describe '#terminals' do
- let(:project) { create(:kubernetes_project) }
subject { environment.terminals }
context 'when the environment has terminals' do
@@ -365,12 +376,27 @@ describe Environment do
allow(environment).to receive(:has_terminals?).and_return(true)
end
- it 'returns the terminals from the deployment service' do
- expect(project.deployment_service)
- .to receive(:terminals).with(environment)
- .and_return(:fake_terminals)
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns the terminals from the deployment service' do
+ expect(project.deployment_platform)
+ .to receive(:terminals).with(environment)
+ .and_return(:fake_terminals)
+
+ is_expected.to eq(:fake_terminals)
+ end
+ end
+
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- is_expected.to eq(:fake_terminals)
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
@@ -547,6 +573,15 @@ describe Environment do
expect(environment.slug).to eq(original_slug)
end
+
+ it "regenerates the slug if nil" do
+ environment = build(:environment, slug: nil)
+
+ new_slug = environment.slug
+
+ expect(new_slug).not_to be_nil
+ expect(environment.slug).to eq(new_slug)
+ end
end
describe '#generate_slug' do
@@ -575,6 +610,22 @@ describe Environment do
end
end
+ describe '#ref_path' do
+ subject(:environment) do
+ create(:environment, name: 'staging / review-1')
+ end
+
+ it 'returns a path that uses the slug and does not have spaces' do
+ expect(environment.ref_path).to start_with('refs/environments/staging-review-1-')
+ end
+
+ it "doesn't change when the slug is nil initially" do
+ environment.slug = nil
+
+ expect(environment.ref_path).to eq(environment.ref_path)
+ end
+ end
+
describe '#external_url_for' do
let(:source_path) { 'source/file.html' }
let(:sha) { RepoHelpers.sample_commit.id }
diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb
index 532ca1fca8c..25bf596fddc 100644
--- a/spec/models/fork_network_member_spec.rb
+++ b/spec/models/fork_network_member_spec.rb
@@ -5,4 +5,22 @@ describe ForkNetworkMember do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:fork_network) }
end
+
+ describe 'destroying a ForkNetworkMember' do
+ let(:fork_network_member) { create(:fork_network_member) }
+ let(:fork_network) { fork_network_member.fork_network }
+
+ it 'removes the fork network if it was the last member' do
+ fork_network.fork_network_members.destroy_all
+
+ expect(ForkNetwork.count).to eq(0)
+ end
+
+ it 'does not destroy the fork network if there are members left' do
+ fork_network_member.destroy!
+
+ # The root of the fork network is left
+ expect(ForkNetwork.count).to eq(1)
+ end
+ end
end
diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb
index 605ccd6db06..a43baf1820a 100644
--- a/spec/models/fork_network_spec.rb
+++ b/spec/models/fork_network_spec.rb
@@ -24,6 +24,16 @@ describe ForkNetwork do
end
end
+ describe '#merge_requests' do
+ it 'finds merge requests within the fork network' do
+ project = create(:project)
+ forked_project = fork_project(project)
+ merge_request = create(:merge_request, source_project: forked_project, target_project: project)
+
+ expect(project.fork_network.merge_requests).to include(merge_request)
+ end
+ end
+
context 'for a deleted project' do
it 'keeps the fork network' do
project = create(:project, :public)
diff --git a/spec/models/gcp/cluster_spec.rb b/spec/models/gcp/cluster_spec.rb
deleted file mode 100644
index 350fbc257d9..00000000000
--- a/spec/models/gcp/cluster_spec.rb
+++ /dev/null
@@ -1,240 +0,0 @@
-require 'spec_helper'
-
-describe Gcp::Cluster do
- it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:user) }
- it { is_expected.to belong_to(:service) }
-
- it { is_expected.to validate_presence_of(:gcp_cluster_zone) }
-
- describe '#default_value_for' do
- let(:cluster) { described_class.new }
-
- it { expect(cluster.gcp_cluster_zone).to eq('us-central1-a') }
- it { expect(cluster.gcp_cluster_size).to eq(3) }
- it { expect(cluster.gcp_machine_type).to eq('n1-standard-4') }
- end
-
- describe '#validates' do
- subject { cluster.valid? }
-
- context 'when validates gcp_project_id' do
- let(:cluster) { build(:gcp_cluster, gcp_project_id: gcp_project_id) }
-
- context 'when valid' do
- let(:gcp_project_id) { 'gcp-project-12345' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when empty' do
- let(:gcp_project_id) { '' }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when too long' do
- let(:gcp_project_id) { 'A' * 64 }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when includes abnormal character' do
- let(:gcp_project_id) { '!!!!!!' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates gcp_cluster_name' do
- let(:cluster) { build(:gcp_cluster, gcp_cluster_name: gcp_cluster_name) }
-
- context 'when valid' do
- let(:gcp_cluster_name) { 'test-cluster' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when empty' do
- let(:gcp_cluster_name) { '' }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when too long' do
- let(:gcp_cluster_name) { 'A' * 64 }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when includes abnormal character' do
- let(:gcp_cluster_name) { '!!!!!!' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates gcp_cluster_size' do
- let(:cluster) { build(:gcp_cluster, gcp_cluster_size: gcp_cluster_size) }
-
- context 'when valid' do
- let(:gcp_cluster_size) { 1 }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when zero' do
- let(:gcp_cluster_size) { 0 }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates project_namespace' do
- let(:cluster) { build(:gcp_cluster, project_namespace: project_namespace) }
-
- context 'when valid' do
- let(:project_namespace) { 'default-namespace' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when empty' do
- let(:project_namespace) { '' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when too long' do
- let(:project_namespace) { 'A' * 64 }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when includes abnormal character' do
- let(:project_namespace) { '!!!!!!' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when validates restrict_modification' do
- let(:cluster) { create(:gcp_cluster) }
-
- before do
- cluster.make_creating!
- end
-
- context 'when created' do
- before do
- cluster.make_created!
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when creating' do
- it { is_expected.to be_falsey }
- end
- end
- end
-
- describe '#state_machine' do
- let(:cluster) { build(:gcp_cluster) }
-
- context 'when transits to created state' do
- before do
- cluster.gcp_token = 'tmp'
- cluster.gcp_operation_id = 'tmp'
- cluster.make_created!
- end
-
- it 'nullify gcp_token and gcp_operation_id' do
- expect(cluster.gcp_token).to be_nil
- expect(cluster.gcp_operation_id).to be_nil
- expect(cluster).to be_created
- end
- end
-
- context 'when transits to errored state' do
- let(:reason) { 'something wrong' }
-
- before do
- cluster.make_errored!(reason)
- end
-
- it 'sets status_reason' do
- expect(cluster.status_reason).to eq(reason)
- expect(cluster).to be_errored
- end
- end
- end
-
- describe '#project_namespace_placeholder' do
- subject { cluster.project_namespace_placeholder }
-
- let(:cluster) { create(:gcp_cluster) }
-
- it 'returns a placeholder' do
- is_expected.to eq("#{cluster.project.path}-#{cluster.project.id}")
- end
- end
-
- describe '#on_creation?' do
- subject { cluster.on_creation? }
-
- let(:cluster) { create(:gcp_cluster) }
-
- context 'when status is creating' do
- before do
- cluster.make_creating!
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when status is created' do
- before do
- cluster.make_created!
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#api_url' do
- subject { cluster.api_url }
-
- let(:cluster) { create(:gcp_cluster, :created_on_gke) }
- let(:api_url) { 'https://' + cluster.endpoint }
-
- it { is_expected.to eq(api_url) }
- end
-
- describe '#restrict_modification' do
- subject { cluster.restrict_modification }
-
- let(:cluster) { create(:gcp_cluster) }
-
- context 'when status is created' do
- before do
- cluster.make_created!
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when status is creating' do
- before do
- cluster.make_creating!
- end
-
- it { is_expected.to be_falsey }
-
- it 'sets error' do
- is_expected.to be_falsey
- expect(cluster.errors).not_to be_empty
- end
- end
- end
-end
diff --git a/spec/models/group_custom_attribute_spec.rb b/spec/models/group_custom_attribute_spec.rb
new file mode 100644
index 00000000000..7ecb2022567
--- /dev/null
+++ b/spec/models/group_custom_attribute_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe GroupCustomAttribute do
+ describe 'assocations' do
+ it { is_expected.to belong_to(:group) }
+ end
+
+ describe 'validations' do
+ subject { build :group_custom_attribute }
+
+ it { is_expected.to validate_presence_of(:group) }
+ it { is_expected.to validate_presence_of(:key) }
+ it { is_expected.to validate_presence_of(:value) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id) }
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index f36d6eeb327..5e82a2988ce 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -17,6 +17,7 @@ describe Group do
it { is_expected.to have_many(:variables).class_name('Ci::GroupVariable') }
it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_one(:chat_team) }
+ it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') }
describe '#members & #requesters' do
let(:requester) { create(:user) }
@@ -64,12 +65,6 @@ describe Group do
expect(group).not_to be_valid
end
-
- it 'rejects reserved group paths' do
- group = build(:group, path: 'activity', parent: create(:group))
-
- expect(group).not_to be_valid
- end
end
describe '#visibility_level_allowed_by_parent' do
@@ -252,8 +247,6 @@ describe Group do
describe '#avatar_url' do
let!(:group) { create(:group, :access_requestable, :with_avatar) }
let(:user) { create(:user) }
- let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
- let(:avatar_path) { "/uploads/-/system/group/avatar/#{group.id}/dk.png" }
context 'when avatar file is uploaded' do
before do
@@ -261,12 +254,8 @@ describe Group do
end
it 'shows correct avatar url' do
- expect(group.avatar_url).to eq(avatar_path)
- expect(group.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
-
- allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
-
- expect(group.avatar_url).to eq([gitlab_host, avatar_path].join)
+ expect(group.avatar_url).to eq(group.avatar.url)
+ expect(group.avatar_url(only_path: false)).to eq([Gitlab.config.gitlab.url, group.avatar.url].join)
end
end
end
@@ -488,6 +477,47 @@ describe Group do
end
end
+ describe '#path_changed_hook' do
+ let(:system_hook_service) { SystemHooksService.new }
+
+ context 'for a new group' do
+ let(:group) { build(:group) }
+
+ before do
+ expect(group).to receive(:system_hook_service).and_return(system_hook_service)
+ end
+
+ it 'does not trigger system hook' do
+ expect(system_hook_service).to receive(:execute_hooks_for).with(group, :create)
+
+ group.save!
+ end
+ end
+
+ context 'for an existing group' do
+ let(:group) { create(:group, path: 'old-path') }
+
+ context 'when the path is changed' do
+ let(:new_path) { 'very-new-path' }
+
+ it 'triggers the rename system hook' do
+ expect(group).to receive(:system_hook_service).and_return(system_hook_service)
+ expect(system_hook_service).to receive(:execute_hooks_for).with(group, :rename)
+
+ group.update_attributes!(path: new_path)
+ end
+ end
+
+ context 'when the path is not changed' do
+ it 'does not trigger system hook' do
+ expect(group).not_to receive(:system_hook_service)
+
+ group.update_attributes!(name: 'new name')
+ end
+ end
+ end
+ end
+
describe '#secret_variables_for' do
let(:project) { create(:project, group: group) }
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
index 4ca6556d0f4..a45a6088831 100644
--- a/spec/models/identity_spec.rb
+++ b/spec/models/identity_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-RSpec.describe Identity do
+describe Identity do
describe 'relations' do
it { is_expected.to belong_to(:user) }
end
@@ -22,4 +22,26 @@ RSpec.describe Identity do
expect(other_identity.ldap?).to be_falsey
end
end
+
+ describe '.with_extern_uid' do
+ context 'LDAP identity' do
+ let!(:ldap_identity) { create(:identity, provider: 'ldapmain', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com') }
+
+ it 'finds the identity when the DN is formatted differently' do
+ identity = described_class.with_extern_uid('ldapmain', 'uid=John Smith, ou=People, dc=example, dc=com').first
+
+ expect(identity).to eq(ldap_identity)
+ end
+ end
+
+ context 'any other provider' do
+ let!(:test_entity) { create(:identity, provider: 'test_provider', extern_uid: 'test_uid') }
+
+ it 'the extern_uid lookup is case insensitive' do
+ identity = described_class.with_extern_uid('test_provider', 'TEST_UID').first
+
+ expect(identity).to eq(test_entity)
+ end
+ end
+ end
end
diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb
new file mode 100644
index 00000000000..8548fff5c76
--- /dev/null
+++ b/spec/models/instance_configuration_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+RSpec.describe InstanceConfiguration do
+ context 'without cache' do
+ describe '#settings' do
+ describe '#ssh_algorithms_hashes' do
+ let(:md5) { '54:e0:f8:70:d6:4f:4c:b1:b3:02:44:77:cf:cd:0d:fc' }
+ let(:sha256) { '9327f0d15a48c4d9f6a3aee65a1825baf9a3412001c98169c5fd022ac27762fc' }
+
+ it 'does not return anything if file does not exist' do
+ stub_pub_file(exist: false)
+
+ expect(subject.settings[:ssh_algorithms_hashes]).to be_empty
+ end
+
+ it 'does not return anything if file is empty' do
+ stub_pub_file
+
+ allow(File).to receive(:read).and_return('')
+
+ expect(subject.settings[:ssh_algorithms_hashes]).to be_empty
+ end
+
+ it 'returns the md5 and sha256 if file valid and exists' do
+ stub_pub_file
+
+ result = subject.settings[:ssh_algorithms_hashes].select { |o| o[:md5] == md5 && o[:sha256] == sha256 }
+
+ expect(result.size).to eq(InstanceConfiguration::SSH_ALGORITHMS.size)
+ end
+
+ def stub_pub_file(exist: true)
+ path = 'spec/fixtures/ssh_host_example_key.pub'
+ path << 'random' unless exist
+ allow(subject).to receive(:ssh_algorithm_file).and_return(Rails.root.join(path))
+ end
+ end
+
+ describe '#host' do
+ it 'returns current instance host' do
+ allow(Settings.gitlab).to receive(:host).and_return('exampledomain')
+
+ expect(subject.settings[:host]).to eq(Settings.gitlab.host)
+ end
+ end
+
+ describe '#gitlab_pages' do
+ let(:gitlab_pages) { subject.settings[:gitlab_pages] }
+ it 'returns Settings.pages' do
+ gitlab_pages.delete(:ip_address)
+
+ expect(gitlab_pages).to eq(Settings.pages.symbolize_keys)
+ end
+
+ it 'returns the Gitlab\'s pages host ip address' do
+ expect(gitlab_pages.keys).to include(:ip_address)
+ end
+
+ it 'returns the ip address as nil if the domain is invalid' do
+ allow(Settings.pages).to receive(:host).and_return('exampledomain')
+
+ expect(gitlab_pages[:ip_address]).to eq nil
+ end
+
+ it 'returns the ip address of the domain' do
+ allow(Settings.pages).to receive(:host).and_return('localhost')
+
+ expect(gitlab_pages[:ip_address]).to eq('127.0.0.1').or eq('::1')
+ end
+ end
+
+ describe '#gitlab_ci' do
+ let(:gitlab_ci) { subject.settings[:gitlab_ci] }
+ it 'returns Settings.gitalb_ci' do
+ gitlab_ci.delete(:artifacts_max_size)
+
+ expect(gitlab_ci).to eq(Settings.gitlab_ci.symbolize_keys)
+ end
+
+ it 'returns the key artifacts_max_size' do
+ expect(gitlab_ci.keys).to include(:artifacts_max_size)
+ end
+ end
+ end
+ end
+
+ context 'with cache', :use_clean_rails_memory_store_caching do
+ it 'caches settings content' do
+ expect(Rails.cache.read(described_class::CACHE_KEY)).to be_nil
+
+ settings = subject.settings
+
+ expect(Rails.cache.read(described_class::CACHE_KEY)).to eq(settings)
+ end
+
+ describe 'cached settings' do
+ before do
+ subject.settings
+ end
+
+ it 'expires after EXPIRATION_TIME' do
+ allow(Time).to receive(:now).and_return(Time.now + described_class::EXPIRATION_TIME)
+ Rails.cache.cleanup
+
+ expect(Rails.cache.read(described_class::CACHE_KEY)).to eq(nil)
+ end
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index e547da0cfbe..5f901262598 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -700,18 +700,14 @@ describe Issue do
end
describe '#hook_attrs' do
- let(:attrs_hash) { subject.hook_attrs }
+ it 'delegates to Gitlab::HookData::IssueBuilder#build' do
+ builder = double
- it 'includes time tracking attrs' do
- expect(attrs_hash).to include(:total_time_spent)
- expect(attrs_hash).to include(:human_time_estimate)
- expect(attrs_hash).to include(:human_total_time_spent)
- expect(attrs_hash).to include('time_estimate')
- end
+ expect(Gitlab::HookData::IssueBuilder)
+ .to receive(:new).with(subject).and_return(builder)
+ expect(builder).to receive(:build)
- it 'includes assignee_ids and deprecated assignee_id' do
- expect(attrs_hash).to include(:assignee_id)
- expect(attrs_hash).to include(:assignee_ids)
+ subject.hook_attrs
end
end
@@ -769,22 +765,4 @@ 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 81c2057e175..4cd9e3f4f1d 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -166,4 +166,27 @@ describe Key, :mailer do
expect(key.public_key.key_text).to eq(valid_key)
end
end
+
+ describe '#refresh_user_cache', :use_clean_rails_memory_store_caching do
+ context 'when the key belongs to a user' do
+ it 'refreshes the keys count cache for the user' do
+ expect_any_instance_of(Users::KeysCountService)
+ .to receive(:refresh_cache)
+ .and_call_original
+
+ key = create(:personal_key)
+
+ expect(Users::KeysCountService.new(key.user).count).to eq(1)
+ end
+ end
+
+ context 'when the key does not belong to a user' do
+ it 'does nothing' do
+ expect_any_instance_of(Users::KeysCountService)
+ .not_to receive(:refresh_cache)
+
+ create(:key)
+ end
+ end
+ end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index a07ce05a865..0a017c068ad 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -488,7 +488,7 @@ describe Member do
member.accept_invite!(user)
end
- it "refreshes user's authorized projects", truncate: true do
+ it "refreshes user's authorized projects", :truncate do
project = member.source
expect(user.authorized_projects).not_to include(project)
@@ -523,7 +523,7 @@ describe Member do
end
end
- describe "destroying a record", truncate: true do
+ describe "destroying a record", :truncate do
it "refreshes user's authorized projects" do
project = create(:project, :private)
user = create(:user)
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
index 9d4a0ecf8c0..7709cf43200 100644
--- a/spec/models/merge_request_diff_commit_spec.rb
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -2,14 +2,93 @@ require 'rails_helper'
describe MergeRequestDiffCommit do
let(:merge_request) { create(:merge_request) }
- subject { merge_request.commits.first }
+ let(:project) { merge_request.project }
describe '#to_hash' do
+ subject { merge_request.commits.first }
+
it 'returns the same results as Commit#to_hash, except for parent_ids' do
- commit_from_repo = merge_request.project.repository.commit(subject.sha)
+ commit_from_repo = project.repository.commit(subject.sha)
commit_from_repo_hash = commit_from_repo.to_hash.merge(parent_ids: [])
expect(subject.to_hash).to eq(commit_from_repo_hash)
end
end
+
+ describe '.create_bulk' do
+ let(:sha_attribute) { Gitlab::Database::ShaAttribute.new }
+ let(:merge_request_diff_id) { merge_request.merge_request_diff.id }
+ let(:commits) do
+ [
+ project.commit('5937ac0a7beb003549fc5fd26fc247adbce4a52e'),
+ project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
+ ]
+ end
+ let(:rows) do
+ [
+ {
+ "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "authored_date": "2014-02-27T10:01:38.000+01:00".to_time,
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T10:01:38.000+01:00".to_time,
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com",
+ "merge_request_diff_id": merge_request_diff_id,
+ "relative_order": 0,
+ "sha": sha_attribute.type_cast_for_database('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ },
+ {
+ "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "authored_date": "2014-02-27T09:57:31.000+01:00".to_time,
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:57:31.000+01:00".to_time,
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com",
+ "merge_request_diff_id": merge_request_diff_id,
+ "relative_order": 1,
+ "sha": sha_attribute.type_cast_for_database('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
+ }
+ ]
+ end
+
+ subject { described_class.create_bulk(merge_request_diff_id, commits) }
+
+ it 'inserts the commits into the database en masse' do
+ expect(Gitlab::Database).to receive(:bulk_insert)
+ .with(described_class.table_name, rows)
+
+ subject
+ end
+
+ context 'with dates larger than the DB limit' do
+ let(:commits) do
+ # This commit's date is "Sun Aug 17 07:12:55 292278994 +0000"
+ [project.commit('ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69')]
+ end
+ let(:timestamp) { Time.at((1 << 31) - 1) }
+ let(:rows) do
+ [{
+ "message": "Weird commit date\n",
+ "authored_date": timestamp,
+ "author_name": "Alejandro Rodríguez",
+ "author_email": "alejorro70@gmail.com",
+ "committed_date": timestamp,
+ "committer_name": "Alejandro Rodríguez",
+ "committer_email": "alejorro70@gmail.com",
+ "merge_request_diff_id": merge_request_diff_id,
+ "relative_order": 0,
+ "sha": sha_attribute.type_cast_for_database('ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69')
+ }]
+ end
+
+ it 'uses a sanitized date' do
+ expect(Gitlab::Database).to receive(:bulk_insert)
+ .with(described_class.table_name, rows)
+
+ subject
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 0cfaa17676e..d556004eccf 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -1,8 +1,10 @@
require 'spec_helper'
describe MergeRequestDiff do
+ let(:diff_with_commits) { create(:merge_request).merge_request_diff }
+
describe 'create new record' do
- subject { create(:merge_request).merge_request_diff }
+ subject { diff_with_commits }
it { expect(subject).to be_valid }
it { expect(subject).to be_persisted }
@@ -18,62 +20,46 @@ describe MergeRequestDiff do
let!(:first_diff) { mr.merge_request_diff }
let!(:last_diff) { mr.create_merge_request_diff }
- it { expect(last_diff.latest?).to be_truthy }
- it { expect(first_diff.latest?).to be_falsey }
+ it { expect(last_diff.reload).to be_latest }
+ it { expect(first_diff.reload).not_to be_latest }
end
describe '#diffs' do
- let(:mr) { create(:merge_request, :with_diffs) }
- let(:mr_diff) { mr.merge_request_diff }
-
context 'when the :ignore_whitespace_change option is set' do
it 'creates a new compare object instead of loading from the DB' do
- expect(mr_diff).not_to receive(:load_diffs)
- expect(Gitlab::Git::Compare).to receive(:new).and_call_original
+ expect(diff_with_commits).not_to receive(:load_diffs)
+ expect(diff_with_commits.compare).to receive(:diffs).and_call_original
- mr_diff.raw_diffs(ignore_whitespace_change: true)
+ diff_with_commits.raw_diffs(ignore_whitespace_change: true)
end
end
context 'when the raw diffs are empty' do
before do
- MergeRequestDiffFile.delete_all(merge_request_diff_id: mr_diff.id)
- end
-
- it 'returns an empty DiffCollection' do
- expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
- expect(mr_diff.raw_diffs).to be_empty
- end
- end
-
- context 'when the raw diffs have invalid content' do
- before do
- MergeRequestDiffFile.delete_all(merge_request_diff_id: mr_diff.id)
- mr_diff.update_attributes(st_diffs: ["--broken-diff"])
+ MergeRequestDiffFile.delete_all(merge_request_diff_id: diff_with_commits.id)
end
it 'returns an empty DiffCollection' do
- expect(mr_diff.raw_diffs.to_a).to be_empty
- expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
- expect(mr_diff.raw_diffs).to be_empty
+ expect(diff_with_commits.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+ expect(diff_with_commits.raw_diffs).to be_empty
end
end
context 'when the raw diffs exist' do
it 'returns the diffs' do
- expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
- expect(mr_diff.raw_diffs).not_to be_empty
+ expect(diff_with_commits.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+ expect(diff_with_commits.raw_diffs).not_to be_empty
end
context 'when the :paths option is set' do
- let(:diffs) { mr_diff.raw_diffs(paths: ['files/ruby/popen.rb', 'files/ruby/popen.rb']) }
+ let(:diffs) { diff_with_commits.raw_diffs(paths: ['files/ruby/popen.rb', 'files/ruby/popen.rb']) }
it 'only returns diffs that match the (old path, new path) given' do
expect(diffs.map(&:new_path)).to contain_exactly('files/ruby/popen.rb')
end
it 'uses the diffs from the DB' do
- expect(mr_diff).to receive(:load_diffs)
+ expect(diff_with_commits).to receive(:load_diffs)
diffs
end
@@ -117,51 +103,29 @@ describe MergeRequestDiff do
end
describe '#commit_shas' do
- it 'returns all commits SHA using serialized commits' do
- subject.st_commits = [
- { id: 'sha1' },
- { id: 'sha2' }
- ]
-
- expect(subject.commit_shas).to eq(%w(sha1 sha2))
+ it 'returns all commit SHAs using commits from the DB' do
+ expect(diff_with_commits.commit_shas).not_to be_empty
+ expect(diff_with_commits.commit_shas).to all(match(/\h{40}/))
end
end
describe '#compare_with' do
- subject { create(:merge_request, source_branch: 'fix').merge_request_diff }
-
it 'delegates compare to the service' do
expect(CompareService).to receive(:new).and_call_original
- subject.compare_with(nil)
+ diff_with_commits.compare_with(nil)
end
it 'uses git diff A..B approach by default' do
- diffs = subject.compare_with('0b4bc9a49b562e85de7cc9e834518ea6828729b9').diffs
+ diffs = diff_with_commits.compare_with('0b4bc9a49b562e85de7cc9e834518ea6828729b9').diffs
- expect(diffs.size).to eq(3)
+ expect(diffs.size).to eq(21)
end
end
describe '#commits_count' do
it 'returns number of commits using serialized commits' do
- subject.st_commits = [
- { id: 'sha1' },
- { id: 'sha2' }
- ]
-
- expect(subject.commits_count).to eq 2
- end
- end
-
- describe '#utf8_st_diffs' do
- it 'does not raise error when a hash value is in binary' do
- subject.st_diffs = [
- { diff: "\0" },
- { diff: "\x05\x00\x68\x65\x6c\x6c\x6f" }
- ]
-
- expect { subject.utf8_st_diffs }.not_to raise_error
+ expect(diff_with_commits.commits_count).to eq(29)
end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 950af653c80..728028746d8 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -79,6 +79,43 @@ describe MergeRequest do
end
end
+ describe '.set_latest_merge_request_diff_ids!' do
+ def create_merge_request_with_diffs(source_branch, diffs: 2)
+ params = {
+ target_project: project,
+ target_branch: 'master',
+ source_project: project,
+ source_branch: source_branch
+ }
+
+ create(:merge_request, params).tap do |mr|
+ diffs.times { mr.merge_request_diffs.create }
+ end
+ end
+
+ let(:project) { create(:project) }
+
+ it 'sets IDs for merge requests, whether they are already set or not' do
+ merge_requests = [
+ create_merge_request_with_diffs('feature'),
+ create_merge_request_with_diffs('feature-conflict'),
+ create_merge_request_with_diffs('wip', diffs: 0),
+ create_merge_request_with_diffs('csv')
+ ]
+
+ merge_requests.take(2).each do |merge_request|
+ merge_request.update_column(:latest_merge_request_diff_id, nil)
+ end
+
+ expected = merge_requests.map do |merge_request|
+ merge_request.merge_request_diffs.maximum(:id)
+ end
+
+ expect { project.merge_requests.set_latest_merge_request_diff_ids! }
+ .to change { merge_requests.map { |mr| mr.reload.latest_merge_request_diff_id } }.to(expected)
+ end
+ end
+
describe '#target_branch_sha' do
let(:project) { create(:project, :repository) }
@@ -86,7 +123,7 @@ describe MergeRequest do
context 'when the target branch does not exist' do
before do
- project.repository.raw_repository.delete_branch(subject.target_branch)
+ project.repository.rm_branch(subject.author, subject.target_branch)
end
it 'returns nil' do
@@ -222,7 +259,7 @@ describe MergeRequest do
end
describe '#source_branch_sha' do
- let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) }
+ let(:last_branch_commit) { subject.source_project.repository.commit(Gitlab::Git::BRANCH_REF_PREFIX + subject.source_branch) }
context 'with diffs' do
subject { create(:merge_request, :with_diffs) }
@@ -236,6 +273,21 @@ describe MergeRequest do
it 'returns the sha of the source branch last commit' do
expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
end
+
+ context 'when there is a tag name matching the branch name' do
+ let(:tag_name) { subject.source_branch }
+
+ it 'returns the sha of the source branch last commit' do
+ subject.source_project.repository.add_tag(subject.author,
+ tag_name,
+ subject.target_branch_sha,
+ 'Add a tag')
+
+ expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
+
+ subject.source_project.repository.rm_tag(subject.author, tag_name)
+ end
+ end
end
context 'when the merge request is being created' do
@@ -660,27 +712,15 @@ describe MergeRequest do
end
end
- describe "#hook_attrs" do
- let(:attrs_hash) { subject.hook_attrs }
+ describe '#hook_attrs' do
+ it 'delegates to Gitlab::HookData::MergeRequestBuilder#build' do
+ builder = double
- [:source, :target].each do |key|
- describe "#{key} key" do
- include_examples 'project hook data', project_key: key do
- let(:data) { attrs_hash }
- let(:project) { subject.send("#{key}_project") }
- end
- end
- end
+ expect(Gitlab::HookData::MergeRequestBuilder)
+ .to receive(:new).with(subject).and_return(builder)
+ expect(builder).to receive(:build)
- it "has all the required keys" do
- expect(attrs_hash).to include(:source)
- expect(attrs_hash).to include(:target)
- expect(attrs_hash).to include(:last_commit)
- expect(attrs_hash).to include(:work_in_progress)
- expect(attrs_hash).to include(:total_time_spent)
- expect(attrs_hash).to include(:human_time_estimate)
- expect(attrs_hash).to include(:human_total_time_spent)
- expect(attrs_hash).to include('time_estimate')
+ subject.hook_attrs
end
end
@@ -908,7 +948,7 @@ describe MergeRequest do
context 'with a completely different branch' do
before do
- subject.update(target_branch: 'v1.0.0')
+ subject.update(target_branch: 'csv')
end
it_behaves_like 'returning all SHA'
@@ -916,7 +956,7 @@ describe MergeRequest do
context 'with a branch having no difference' do
before do
- subject.update(target_branch: 'v1.1.0')
+ subject.update(target_branch: 'branch-merged')
subject.reload # make sure commits were not cached
end
@@ -1400,7 +1440,7 @@ describe MergeRequest do
context 'when the target branch does not exist' do
before do
- subject.project.repository.raw_repository.delete_branch(subject.target_branch)
+ subject.project.repository.rm_branch(subject.author, subject.target_branch)
end
it 'returns nil' do
@@ -1472,11 +1512,37 @@ describe MergeRequest do
end
describe '#merge_ongoing?' do
- it 'returns true when merge_id is present and MR is not merged' do
+ it 'returns true when the merge request is locked' do
+ merge_request = build_stubbed(:merge_request, state: :locked)
+
+ expect(merge_request.merge_ongoing?).to be(true)
+ end
+
+ it 'returns true when merge_id, MR is not merged and it has no running job' do
merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo')
+ allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { true }
expect(merge_request.merge_ongoing?).to be(true)
end
+
+ it 'returns false when merge_jid is nil' do
+ merge_request = build_stubbed(:merge_request, state: :open, merge_jid: nil)
+
+ expect(merge_request.merge_ongoing?).to be(false)
+ end
+
+ it 'returns false if MR is merged' do
+ merge_request = build_stubbed(:merge_request, state: :merged, merge_jid: 'foo')
+
+ expect(merge_request.merge_ongoing?).to be(false)
+ end
+
+ it 'returns false if there is no merge job running' do
+ merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo')
+ allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { false }
+
+ expect(merge_request.merge_ongoing?).to be(false)
+ end
end
describe "#closed_without_fork?" do
@@ -1741,39 +1807,12 @@ describe MergeRequest do
end
end
- describe '#fetch_ref' do
- it 'sets "ref_fetched" flag to true' do
- subject.update!(ref_fetched: nil)
-
- subject.fetch_ref
+ describe '#fetch_ref!' do
+ it 'fetches the ref correctly' do
+ expect { subject.target_project.repository.delete_refs(subject.ref_path) }.not_to raise_error
- expect(subject.reload.ref_fetched).to be_truthy
- end
- end
-
- describe '#ref_fetched?' do
- it 'does not perform git operation when value is cached' do
- subject.ref_fetched = true
-
- expect_any_instance_of(Repository).not_to receive(:ref_exists?)
- expect(subject.ref_fetched?).to be_truthy
- end
-
- it 'caches the value when ref exists but value is not cached' do
- subject.update!(ref_fetched: nil)
- allow_any_instance_of(Repository).to receive(:ref_exists?)
- .and_return(true)
-
- expect(subject.ref_fetched?).to be_truthy
- expect(subject.reload.ref_fetched).to be_truthy
- end
-
- it 'returns false when ref does not exist' do
- subject.update!(ref_fetched: nil)
- allow_any_instance_of(Repository).to receive(:ref_exists?)
- .and_return(false)
-
- expect(subject.ref_fetched?).to be_falsey
+ subject.fetch_ref!
+ expect(subject.target_project.repository.ref_exists?(subject.ref_path)).to be_truthy
end
end
@@ -1785,16 +1824,4 @@ 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/milestone_spec.rb b/spec/models/milestone_spec.rb
index 13e37fffa4e..47f4a792e5c 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -11,7 +11,7 @@ describe Milestone do
milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday)
expect(milestone).not_to be_valid
- expect(milestone.errors[:start_date]).to include("Can't be greater than due date")
+ expect(milestone.errors[:due_date]).to include("must be greater than start date")
end
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 2ebf6acd42a..90b768f595e 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -4,6 +4,7 @@ describe Namespace do
include ProjectForksHelper
let!(:namespace) { create(:namespace) }
+ let(:gitlab_shell) { Gitlab::Shell.new }
describe 'associations' do
it { is_expected.to have_many :projects }
@@ -153,25 +154,32 @@ describe Namespace do
end
end
- describe '#move_dir' do
- let(:namespace) { create(:namespace) }
- let!(:project) { create(:project_empty_repo, namespace: namespace) }
+ describe '#ancestors_upto', :nested_groups do
+ let(:parent) { create(:group) }
+ let(:child) { create(:group, parent: parent) }
+ let(:child2) { create(:group, parent: child) }
- before do
- allow(namespace).to receive(:path_changed?).and_return(true)
+ it 'returns all ancestors when no namespace is given' do
+ expect(child2.ancestors_upto).to contain_exactly(child, parent)
+ end
+
+ it 'includes ancestors upto but excluding the given ancestor' do
+ expect(child2.ancestors_upto(parent)).to contain_exactly(child)
end
+ end
+
+ describe '#move_dir', :request_store do
+ let(:namespace) { create(:namespace) }
+ let!(:project) { create(:project_empty_repo, namespace: namespace) }
it "raises error when directory exists" do
expect { namespace.move_dir }.to raise_error("namespace directory cannot be moved")
end
it "moves dir if path changed" do
- new_path = namespace.full_path + "_new"
+ namespace.update_attributes(path: namespace.full_path + '_new')
- allow(namespace).to receive(:full_path_was).and_return(namespace.full_path)
- allow(namespace).to receive(:full_path).and_return(new_path)
- expect(namespace).to receive(:remove_exports!)
- expect(namespace.move_dir).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{namespace.path}/#{project.path}.git")).to be_truthy
end
context "when any project has container images" do
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 1ecb50586c7..6e7e8c4c570 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -231,6 +231,37 @@ describe Note do
end
end
+ describe '#cross_reference?' do
+ it 'falsey for user-generated notes' do
+ note = create(:note, system: false)
+
+ expect(note.cross_reference?).to be_falsy
+ end
+
+ context 'when the note might contain cross references' do
+ SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.each do |type|
+ let(:note) { create(:note, :system) }
+ let!(:metadata) { create(:system_note_metadata, note: note, action: type) }
+
+ it 'delegates to the cross-reference regex' do
+ expect(note).to receive(:matches_cross_reference_regex?).and_return(false)
+
+ note.cross_reference?
+ end
+ end
+ end
+
+ context 'when the note cannot contain cross references' do
+ let(:commit_note) { build(:note, note: 'mentioned in 1312312313 something else.', system: true) }
+ let(:label_note) { build(:note, note: 'added ~2323232323', system: true) }
+
+ it 'scan for a `mentioned in` prefix' do
+ expect(commit_note.cross_reference?).to be_truthy
+ expect(label_note.cross_reference?).to be_falsy
+ end
+ end
+ end
+
describe 'clear_blank_line_code!' do
it 'clears a blank line code before validation' do
note = build(:note, line_code: ' ')
diff --git a/spec/models/project_custom_attribute_spec.rb b/spec/models/project_custom_attribute_spec.rb
new file mode 100644
index 00000000000..669de5506bc
--- /dev/null
+++ b/spec/models/project_custom_attribute_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe ProjectCustomAttribute do
+ describe 'assocations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ subject { build :project_custom_attribute }
+
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:key) }
+ it { is_expected.to validate_presence_of(:value) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) }
+ end
+end
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index b3513c80150..41e2ab20d69 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -30,7 +30,7 @@ describe ProjectGroupLink do
end
end
- describe "destroying a record", truncate: true do
+ describe "destroying a record", :truncate do
it "refreshes group users' authorized projects" do
project = create(:project, :private)
group = create(:group)
diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb
index d37726dc3f1..f7a35fdc88a 100644
--- a/spec/models/project_services/chat_message/issue_message_spec.rb
+++ b/spec/models/project_services/chat_message/issue_message_spec.rb
@@ -66,6 +66,19 @@ describe ChatMessage::IssueMessage do
expect(subject.attachments).to be_empty
end
end
+
+ context 'reopen' do
+ before do
+ args[:object_attributes][:action] = 'reopen'
+ args[:object_attributes][:state] = 'opened'
+ end
+
+ it 'returns a message regarding reopening of issues' do
+ expect(subject.pretext)
+ .to eq('[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> opened by Test User (test.user)')
+ expect(subject.attachments).to be_empty
+ end
+ end
end
context 'with markdown' do
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index 5e8e880985e..fabcb142858 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -46,6 +46,7 @@ describe FlowdockService do
@sample_data[:commits].each do |commit|
# One request to Flowdock per new commit
next if commit[:id] == @sample_data[:before]
+
expect(WebMock).to have_requested(:post, @api_url).with(
body: /#{commit[:id]}.*#{project.path}/
).once
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 63bf131cfc5..ad22fb2a386 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -24,6 +24,8 @@ describe JiraService do
end
it { is_expected.not_to validate_presence_of(:url) }
+ it { is_expected.not_to validate_presence_of(:username) }
+ it { is_expected.not_to validate_presence_of(:password) }
end
context 'validating urls' do
@@ -54,6 +56,18 @@ describe JiraService do
expect(service).not_to be_valid
end
+ it 'is not valid when username is missing' do
+ service.username = nil
+
+ expect(service).not_to be_valid
+ end
+
+ it 'is not valid when password is missing' do
+ service.password = nil
+
+ expect(service).not_to be_valid
+ end
+
it 'is valid when api url is a valid url' do
service.api_url = 'http://jira.test.com/api'
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 2298dcab55f..f037ee77a94 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -4,10 +4,10 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
include KubernetesHelpers
include ReactiveCachingHelpers
- let(:project) { build_stubbed(:kubernetes_project) }
- let(:service) { project.kubernetes_service }
+ let(:project) { create(:kubernetes_project) }
+ let(:service) { project.deployment_platform }
- describe "Associations" do
+ describe 'Associations' do
it { is_expected.to belong_to :project }
end
@@ -99,45 +99,34 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
describe '#actual_namespace' do
subject { service.actual_namespace }
- it "returns the default namespace" do
- is_expected.to eq(service.send(:default_namespace))
- end
-
- context 'when namespace is specified' do
- before do
- service.namespace = 'my-namespace'
+ shared_examples 'a correctly formatted namespace' do
+ it 'returns a valid Kubernetes namespace name' do
+ expect(subject).to match(Gitlab::Regex.kubernetes_namespace_regex)
+ expect(subject).to eq(expected_namespace)
end
+ end
- it "returns the user-namespace" do
- is_expected.to eq('my-namespace')
- end
+ it_behaves_like 'a correctly formatted namespace' do
+ let(:expected_namespace) { service.send(:default_namespace) }
end
- context 'when service is not assigned to project' do
+ context 'when the project path contains forbidden characters' do
before do
- service.project = nil
+ project.path = '-a_Strange.Path--forSure'
end
- it "does not return namespace" do
- is_expected.to be_nil
+ it_behaves_like 'a correctly formatted namespace' do
+ let(:expected_namespace) { "a-strange-path--forsure-#{project.id}" }
end
end
- end
-
- describe '#actual_namespace' do
- subject { service.actual_namespace }
-
- it "returns the default namespace" do
- is_expected.to eq(service.send(:default_namespace))
- end
context 'when namespace is specified' do
before do
service.namespace = 'my-namespace'
end
- it "returns the user-namespace" do
- is_expected.to eq('my-namespace')
+ it_behaves_like 'a correctly formatted namespace' do
+ let(:expected_namespace) { 'my-namespace' }
end
end
@@ -146,7 +135,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
service.project = nil
end
- it "does not return namespace" do
+ it 'does not return namespace' do
is_expected.to be_nil
end
end
@@ -156,7 +145,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
let(:discovery_url) { 'https://kubernetes.example.com/api/v1' }
before do
- stub_kubeclient_discover
+ stub_kubeclient_discover(service.api_url)
end
context 'with path prefix in api_url' do
@@ -164,7 +153,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
it 'tests with the prefix' do
service.api_url = 'https://kubernetes.example.com/prefix'
- stub_kubeclient_discover
+ stub_kubeclient_discover(service.api_url)
expect(service.test[:success]).to be_truthy
expect(WebMock).to have_requested(:get, discovery_url).once
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index f89be20ad78..6a5d0decfec 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -108,12 +108,8 @@ describe MicrosoftTeamsService do
message: "user created page: Awesome wiki_page"
}
end
-
- let(:wiki_page_sample_data) do
- service = WikiPages::CreateService.new(project, user, opts)
- wiki_page = service.execute
- Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create')
- end
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: opts) }
+ let(:wiki_page_sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') }
it "calls Microsoft Teams API" do
chat_service.execute(wiki_page_sample_data)
diff --git a/spec/models/project_services/packagist_service_spec.rb b/spec/models/project_services/packagist_service_spec.rb
new file mode 100644
index 00000000000..6acee311700
--- /dev/null
+++ b/spec/models/project_services/packagist_service_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe PackagistService do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ let(:project) { create(:project) }
+
+ let(:packagist_server) { 'https://packagist.example.com' }
+ let(:packagist_username) { 'theUser' }
+ let(:packagist_token) { 'verySecret' }
+ let(:packagist_hook_url) do
+ "#{packagist_server}/api/update-package?username=#{packagist_username}&apiToken=#{packagist_token}"
+ end
+
+ let(:packagist_params) do
+ {
+ active: true,
+ project: project,
+ properties: {
+ username: packagist_username,
+ token: packagist_token,
+ server: packagist_server
+ }
+ }
+ end
+
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
+ let(:packagist_service) { described_class.create(packagist_params) }
+
+ before do
+ stub_request(:post, packagist_hook_url)
+ end
+
+ it 'calls Packagist API' do
+ packagist_service.execute(push_sample_data)
+
+ expect(a_request(:post, packagist_hook_url)).to have_been_made.once
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index a26c71e5155..a4abcc49a0d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -24,6 +24,7 @@ describe Project do
it { is_expected.to have_one(:slack_service) }
it { is_expected.to have_one(:microsoft_teams_service) }
it { is_expected.to have_one(:mattermost_service) }
+ it { is_expected.to have_one(:packagist_service) }
it { is_expected.to have_one(:pushover_service) }
it { is_expected.to have_one(:asana_service) }
it { is_expected.to have_many(:boards) }
@@ -78,6 +79,7 @@ describe Project do
it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_one(:cluster) }
+ it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
context 'after initialized' do
it "has a project_feature" do
@@ -275,6 +277,12 @@ describe Project do
expect(project).to be_valid
end
+
+ it 'allows a path ending in a period' do
+ project = build(:project, path: 'foo.')
+
+ expect(project).to be_valid
+ end
end
end
@@ -874,21 +882,15 @@ describe Project do
let(:project) { create(:project) }
context 'when avatar file is uploaded' do
- let(:project) { create(:project, :with_avatar) }
- let(:avatar_path) { "/uploads/-/system/project/avatar/#{project.id}/dk.png" }
- let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
+ let(:project) { create(:project, :public, :with_avatar) }
it 'shows correct url' do
- expect(project.avatar_url).to eq(avatar_path)
- expect(project.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
-
- allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
-
- expect(project.avatar_url).to eq([gitlab_host, avatar_path].join)
+ expect(project.avatar_url).to eq(project.avatar.url)
+ expect(project.avatar_url(only_path: false)).to eq([Gitlab.config.gitlab.url, project.avatar.url].join)
end
end
- context 'When avatar file in git' do
+ context 'when avatar file in git' do
before do
allow(project).to receive(:avatar_in_git) { true }
end
@@ -1252,24 +1254,6 @@ describe Project do
expect(described_class.search(project.path.upcase)).to eq([project])
end
- it 'returns projects with a matching namespace name' do
- expect(described_class.search(project.namespace.name)).to eq([project])
- end
-
- it 'returns projects with a partially matching namespace name' do
- expect(described_class.search(project.namespace.name[0..2])).to eq([project])
- end
-
- it 'returns projects with a matching namespace name regardless of the casing' do
- expect(described_class.search(project.namespace.name.upcase)).to eq([project])
- end
-
- it 'returns projects when eager loading namespaces' do
- relation = described_class.all.includes(:namespace)
-
- expect(relation.search(project.namespace.name)).to eq([project])
- end
-
describe 'with pending_delete project' do
let(:pending_delete_project) { create(:project, pending_delete: true) }
@@ -1761,6 +1745,21 @@ describe Project do
it { expect(project.gitea_import?).to be true }
end
+ describe '#ancestors_upto', :nested_groups do
+ let(:parent) { create(:group) }
+ let(:child) { create(:group, parent: parent) }
+ let(:child2) { create(:group, parent: child) }
+ let(:project) { create(:project, namespace: child2) }
+
+ it 'returns all ancestors when no namespace is given' do
+ expect(project.ancestors_upto).to contain_exactly(child2, child, parent)
+ end
+
+ it 'includes ancestors upto but excluding the given ancestor' do
+ expect(project.ancestors_upto(parent)).to contain_exactly(child2, child)
+ end
+ end
+
describe '#lfs_enabled?' do
let(:project) { create(:project) }
@@ -1907,6 +1906,38 @@ describe Project do
expect(forked_project.in_fork_network_of?(other_project)).to be_falsy
end
end
+
+ describe '#fork_source' do
+ let!(:second_fork) { fork_project(forked_project) }
+
+ it 'returns the direct source if it exists' do
+ expect(second_fork.fork_source).to eq(forked_project)
+ end
+
+ it 'returns the root of the fork network when the directs source was deleted' do
+ forked_project.destroy
+
+ expect(second_fork.fork_source).to eq(project)
+ end
+ end
+
+ describe '#lfs_storage_project' do
+ it 'returns self for non-forks' do
+ expect(project.lfs_storage_project).to eq project
+ end
+
+ it 'returns the fork network root for forks' do
+ second_fork = fork_project(forked_project)
+
+ expect(second_fork.lfs_storage_project).to eq project
+ end
+
+ it 'returns self when fork_source is nil' do
+ expect(forked_project).to receive(:fork_source).and_return(nil)
+
+ expect(forked_project.lfs_storage_project).to eq forked_project
+ end
+ end
end
describe '#pushes_since_gc' do
@@ -1971,12 +2002,25 @@ describe Project do
end
context 'when project has a deployment service' do
- let(:project) { create(:kubernetes_project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns variables from this service' do
+ expect(project.deployment_variables).to include(
+ { key: 'KUBE_TOKEN', value: project.deployment_platform.token, public: false }
+ )
+ end
+ end
+
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
- it 'returns variables from this service' do
- expect(project.deployment_variables).to include(
- { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false }
- )
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
end
@@ -2178,6 +2222,12 @@ describe Project do
it { expect(project.parent).to eq(project.namespace) }
end
+ describe '#parent_id' do
+ let(:project) { create(:project) }
+
+ it { expect(project.parent_id).to eq(project.namespace_id) }
+ end
+
describe '#parent_changed?' do
let(:project) { create(:project) }
@@ -2431,6 +2481,7 @@ describe Project do
context 'legacy storage' do
let(:project) { create(:project, :repository) }
let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:project_storage) { project.send(:storage) }
before do
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
@@ -2472,7 +2523,7 @@ describe Project do
describe '#hashed_storage?' do
it 'returns false' do
- expect(project.hashed_storage?).to be_falsey
+ expect(project.hashed_storage?(:repository)).to be_falsey
end
end
@@ -2525,6 +2576,30 @@ describe Project do
it { expect { subject }.to raise_error(StandardError) }
end
+
+ context 'gitlab pages' do
+ before do
+ expect(project_storage).to receive(:rename_repo) { true }
+ end
+
+ it 'moves pages folder to new location' do
+ expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project)
+
+ project.rename_repo
+ end
+ end
+
+ context 'attachments' do
+ before do
+ expect(project_storage).to receive(:rename_repo) { true }
+ end
+
+ it 'moves uploads folder to new location' do
+ expect_any_instance_of(Gitlab::UploadsTransfer).to receive(:rename_project)
+
+ project.rename_repo
+ end
+ end
end
describe '#pages_path' do
@@ -2584,8 +2659,14 @@ describe Project do
end
describe '#hashed_storage?' do
- it 'returns true' do
- expect(project.hashed_storage?).to be_truthy
+ it 'returns true if rolled out' do
+ expect(project.hashed_storage?(:attachments)).to be_truthy
+ end
+
+ it 'returns false when not rolled out yet' do
+ project.storage_version = 1
+
+ expect(project.hashed_storage?(:attachments)).to be_falsey
end
end
@@ -2628,10 +2709,6 @@ describe Project do
.to receive(:execute_hooks_for)
.with(project, :rename)
- expect_any_instance_of(Gitlab::UploadsTransfer)
- .to receive(:rename_project)
- .with('foo', project.path, project.namespace.full_path)
-
expect(project).to receive(:expire_caches_before_rename)
expect(project).to receive(:expires_full_path_cache)
@@ -2652,6 +2729,32 @@ describe Project do
it { expect { subject }.to raise_error(StandardError) }
end
+
+ context 'gitlab pages' do
+ it 'moves pages folder to new location' do
+ expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project)
+
+ project.rename_repo
+ end
+ end
+
+ context 'attachments' do
+ it 'keeps uploads folder location unchanged' do
+ expect_any_instance_of(Gitlab::UploadsTransfer).not_to receive(:rename_project)
+
+ project.rename_repo
+ end
+
+ context 'when not rolled out' do
+ let(:project) { create(:project, :repository, storage_version: 1) }
+
+ it 'moves pages folder to new location' do
+ expect_any_instance_of(Gitlab::UploadsTransfer).to receive(:rename_project)
+
+ project.rename_repo
+ end
+ end
+ end
end
describe '#pages_path' do
@@ -2920,4 +3023,96 @@ describe Project do
end
end
end
+
+ describe '#after_import' do
+ let(:project) { build(:project) }
+
+ it 'runs the correct hooks' do
+ expect(project.repository).to receive(:after_import)
+ expect(project).to receive(:import_finish)
+ expect(project).to receive(:update_project_counter_caches)
+ expect(project).to receive(:remove_import_jid)
+
+ project.after_import
+ end
+ end
+
+ describe '#update_project_counter_caches' do
+ let(:project) { create(:project) }
+
+ it 'updates all project counter caches' do
+ expect_any_instance_of(Projects::OpenIssuesCountService)
+ .to receive(:refresh_cache)
+ .and_call_original
+
+ expect_any_instance_of(Projects::OpenMergeRequestsCountService)
+ .to receive(:refresh_cache)
+ .and_call_original
+
+ project.update_project_counter_caches
+ end
+ end
+
+ describe '#remove_import_jid', :clean_gitlab_redis_cache do
+ let(:project) { }
+
+ context 'without an import JID' do
+ it 'does nothing' do
+ project = create(:project)
+
+ expect(Gitlab::SidekiqStatus)
+ .not_to receive(:unset)
+
+ project.remove_import_jid
+ end
+ end
+
+ context 'with an import JID' do
+ it 'unsets the import JID' do
+ project = create(:project, import_jid: '123')
+
+ expect(Gitlab::SidekiqStatus)
+ .to receive(:unset)
+ .with('123')
+ .and_call_original
+
+ project.remove_import_jid
+
+ expect(project.import_jid).to be_nil
+ end
+ end
+ end
+
+ describe '#wiki_repository_exists?' do
+ it 'returns true when the wiki repository exists' do
+ project = create(:project, :wiki_repo)
+
+ expect(project.wiki_repository_exists?).to eq(true)
+ end
+
+ it 'returns false when the wiki repository does not exist' do
+ project = create(:project)
+
+ expect(project.wiki_repository_exists?).to eq(false)
+ end
+ end
+
+ describe '#deployment_platform' do
+ subject { project.deployment_platform }
+
+ let(:project) { create(:project) }
+
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let!(:kubernetes_service) { create(:kubernetes_service, project: project) }
+
+ it { is_expected.to eq(kubernetes_service) }
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let(:platform_kubernetes) { cluster.platform_kubernetes }
+
+ it { is_expected.to eq(platform_kubernetes) }
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 78fb2df884a..929086305ba 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -10,6 +10,10 @@ describe ProjectWiki do
subject { project_wiki }
+ it { is_expected.to delegate_method(:empty?).to :pages }
+ it { is_expected.to delegate_method(:repository_storage_path).to :project }
+ it { is_expected.to delegate_method(:hashed_storage?).to :project }
+
describe "#path_with_namespace" do
it "returns the project path with namespace with the .wiki extension" do
expect(subject.path_with_namespace).to eq(project.full_path + '.wiki')
@@ -117,100 +121,119 @@ describe ProjectWiki do
end
describe "#find_page" do
- before do
- create_page("index page", "This is an awesome Gollum Wiki")
- end
+ shared_examples 'finding a wiki page' do
+ before do
+ create_page("index page", "This is an awesome Gollum Wiki")
+ end
- after do
- destroy_page(subject.pages.first.page)
- end
+ after do
+ destroy_page(subject.pages.first.page)
+ end
- it "returns the latest version of the page if it exists" do
- page = subject.find_page("index page")
- expect(page.title).to eq("index page")
- end
+ it "returns the latest version of the page if it exists" do
+ page = subject.find_page("index page")
+ expect(page.title).to eq("index page")
+ end
+
+ it "returns nil if the page does not exist" do
+ expect(subject.find_page("non-existant")).to eq(nil)
+ end
- it "returns nil if the page does not exist" do
- expect(subject.find_page("non-existant")).to eq(nil)
+ it "can find a page by slug" do
+ page = subject.find_page("index-page")
+ expect(page.title).to eq("index page")
+ end
+
+ it "returns a WikiPage instance" do
+ page = subject.find_page("index page")
+ expect(page).to be_a WikiPage
+ end
end
- it "can find a page by slug" do
- page = subject.find_page("index-page")
- expect(page.title).to eq("index page")
+ context 'when Gitaly wiki_find_page is enabled' do
+ it_behaves_like 'finding a wiki page'
end
- it "returns a WikiPage instance" do
- page = subject.find_page("index page")
- expect(page).to be_a WikiPage
+ context 'when Gitaly wiki_find_page is disabled', :skip_gitaly_mock do
+ it_behaves_like 'finding a wiki page'
end
end
describe '#find_file' do
- before do
- file = Gollum::File.new(subject.wiki)
- allow_any_instance_of(Gollum::Wiki)
- .to receive(:file).with('image.jpg', 'master')
- .and_return(file)
- allow_any_instance_of(Gollum::File)
- .to receive(:mime_type)
- .and_return('image/jpeg')
- allow_any_instance_of(Gollum::Wiki)
- .to receive(:file).with('non-existant', 'master')
- .and_return(nil)
- end
+ shared_examples 'finding a wiki file' do
+ before do
+ file = File.open(Rails.root.join('spec', 'fixtures', 'dk.png'))
+ subject.wiki # Make sure the wiki repo exists
- after do
- allow_any_instance_of(Gollum::Wiki).to receive(:file).and_call_original
- allow_any_instance_of(Gollum::File).to receive(:mime_type).and_call_original
- end
+ BareRepoOperations.new(subject.repository.path_to_repo).commit_file(file, 'image.png')
+ end
+
+ it 'returns the latest version of the file if it exists' do
+ file = subject.find_file('image.png')
+ expect(file.mime_type).to eq('image/png')
+ end
- it 'returns the latest version of the file if it exists' do
- file = subject.find_file('image.jpg')
- expect(file.mime_type).to eq('image/jpeg')
+ it 'returns nil if the page does not exist' do
+ expect(subject.find_file('non-existant')).to eq(nil)
+ end
+
+ it 'returns a Gitlab::Git::WikiFile instance' do
+ file = subject.find_file('image.png')
+ expect(file).to be_a Gitlab::Git::WikiFile
+ end
end
- it 'returns nil if the page does not exist' do
- expect(subject.find_file('non-existant')).to eq(nil)
+ context 'when Gitaly wiki_find_file is enabled' do
+ it_behaves_like 'finding a wiki file'
end
- it 'returns a Gitlab::Git::WikiFile instance' do
- file = subject.find_file('image.jpg')
- expect(file).to be_a Gitlab::Git::WikiFile
+ context 'when Gitaly wiki_find_file is disabled', :skip_gitaly_mock do
+ it_behaves_like 'finding a wiki file'
end
end
describe "#create_page" do
- after do
- destroy_page(subject.pages.first.page)
- end
+ shared_examples 'creating a wiki page' do
+ after do
+ destroy_page(subject.pages.first.page)
+ end
- it "creates a new wiki page" do
- expect(subject.create_page("test page", "this is content")).not_to eq(false)
- expect(subject.pages.count).to eq(1)
- end
+ it "creates a new wiki page" do
+ expect(subject.create_page("test page", "this is content")).not_to eq(false)
+ expect(subject.pages.count).to eq(1)
+ end
- it "returns false when a duplicate page exists" do
- subject.create_page("test page", "content")
- expect(subject.create_page("test page", "content")).to eq(false)
- end
+ it "returns false when a duplicate page exists" do
+ subject.create_page("test page", "content")
+ expect(subject.create_page("test page", "content")).to eq(false)
+ end
- it "stores an error message when a duplicate page exists" do
- 2.times { subject.create_page("test page", "content") }
- expect(subject.error_message).to match(/Duplicate page:/)
- end
+ it "stores an error message when a duplicate page exists" do
+ 2.times { subject.create_page("test page", "content") }
+ expect(subject.error_message).to match(/Duplicate page:/)
+ end
- it "sets the correct commit message" do
- subject.create_page("test page", "some content", :markdown, "commit message")
- expect(subject.pages.first.page.version.message).to eq("commit message")
- end
+ it "sets the correct commit message" do
+ subject.create_page("test page", "some content", :markdown, "commit message")
+ expect(subject.pages.first.page.version.message).to eq("commit message")
+ end
- it 'updates project activity' do
- subject.create_page('Test Page', 'This is content')
+ it 'updates project activity' do
+ subject.create_page('Test Page', 'This is content')
- project.reload
+ project.reload
- expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
- expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
+ end
+ end
+
+ context 'when Gitaly wiki_write_page is enabled' do
+ it_behaves_like 'creating a wiki page'
+ end
+
+ context 'when Gitaly wiki_write_page is disabled', :skip_gitaly_mock do
+ it_behaves_like 'creating a wiki page'
end
end
@@ -255,23 +278,33 @@ describe ProjectWiki do
end
describe "#delete_page" do
- before do
- create_page("index", "some content")
- @page = subject.wiki.page(title: "index")
- end
+ shared_examples 'deleting a wiki page' do
+ before do
+ create_page("index", "some content")
+ @page = subject.wiki.page(title: "index")
+ end
- it "deletes the page" do
- subject.delete_page(@page)
- expect(subject.pages.count).to eq(0)
- end
+ it "deletes the page" do
+ subject.delete_page(@page)
+ expect(subject.pages.count).to eq(0)
+ end
- it 'updates project activity' do
- subject.delete_page(@page)
+ it 'updates project activity' do
+ subject.delete_page(@page)
- project.reload
+ project.reload
- expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
- expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
+ end
+ end
+
+ context 'when Gitaly wiki_delete_page is enabled' do
+ it_behaves_like 'deleting a wiki page'
+ end
+
+ context 'when Gitaly wiki_delete_page is disabled', :skip_gitaly_mock do
+ it_behaves_like 'deleting a wiki page'
end
end
@@ -333,6 +366,6 @@ describe ProjectWiki do
end
def destroy_page(page)
- subject.delete_page(page, commit_details)
+ subject.delete_page(page, "test commit")
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 5764c451b67..27f0a99b2fa 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -40,7 +40,7 @@ describe Repository do
it { is_expected.not_to include('feature') }
it { is_expected.not_to include('fix') }
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.branch_names_contains(sample_commit.id)
@@ -158,7 +158,7 @@ describe Repository do
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore')
@@ -171,7 +171,7 @@ describe Repository do
it_behaves_like 'getting last commit for path'
end
- context 'when Gitaly feature last_commit_for_path is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do
it_behaves_like 'getting last commit for path'
end
end
@@ -192,7 +192,7 @@ describe Repository do
is_expected.to eq('c1acaa5')
end
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id
@@ -205,7 +205,7 @@ describe Repository do
it_behaves_like 'getting last commit ID for path'
end
- context 'when Gitaly feature last_commit_for_path is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do
it_behaves_like 'getting last commit ID for path'
end
end
@@ -255,11 +255,11 @@ describe Repository do
it_behaves_like 'finding commits by message'
end
- context 'when Gitaly commits_by_message feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly commits_by_message feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding commits by message'
end
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') }
end
@@ -589,7 +589,7 @@ describe Repository do
expect(results).to match_array([])
end
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.search_files_by_content('feature', 'master')
@@ -626,7 +626,7 @@ describe Repository do
expect(results).to match_array([])
end
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') }
end
@@ -638,7 +638,7 @@ describe Repository do
# before the actual test call
set(:broken_repository) { create(:project, :broken_storage).repository }
- describe 'when storage is broken', broken_storage: true do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2')
@@ -848,7 +848,7 @@ describe Repository do
end
end
- context 'with Gitaly disabled', skip_gitaly_mock: true do
+ context 'with Gitaly disabled', :skip_gitaly_mock do
context 'when pre hooks were successful' do
it 'runs without errors' do
hook = double(trigger: [true, nil])
@@ -1101,7 +1101,7 @@ describe Repository do
expect(repository.exists?).to eq(false)
end
- context 'with broken storage', broken_storage: true do
+ context 'with broken storage', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error { broken_repository.exists? }
end
@@ -1113,7 +1113,7 @@ describe Repository do
it_behaves_like 'repo exists check'
end
- context 'when repository_exists is enabled', skip_gitaly_mock: true do
+ context 'when repository_exists is enabled', :skip_gitaly_mock do
it_behaves_like 'repo exists check'
end
end
@@ -1148,6 +1148,31 @@ describe Repository do
end
end
+ describe '#branch_exists?' do
+ it 'uses branch_names' do
+ allow(repository).to receive(:branch_names).and_return(['foobar'])
+
+ expect(repository.branch_exists?('foobar')).to eq(true)
+ expect(repository.branch_exists?('master')).to eq(false)
+ end
+ end
+
+ describe '#branch_names', :use_clean_rails_memory_store_caching do
+ let(:fake_branch_names) { ['foobar'] }
+
+ it 'gets cached across Repository instances' do
+ allow(repository.raw_repository).to receive(:branch_names).once.and_return(fake_branch_names)
+
+ expect(repository.branch_names).to eq(fake_branch_names)
+
+ fresh_repository = Project.find(project.id).repository
+ expect(fresh_repository.object_id).not_to eq(repository.object_id)
+
+ expect(fresh_repository.raw_repository).not_to receive(:branch_names)
+ expect(fresh_repository.branch_names).to eq(fake_branch_names)
+ end
+ end
+
describe '#update_autocrlf_option' do
describe 'when autocrlf is not already set to :input' do
before do
@@ -1286,21 +1311,31 @@ describe Repository do
let(:message) { 'Test \r\n\r\n message' }
- it 'merges the code and returns the commit id' do
- expect(merge_commit).to be_present
- expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
- end
+ shared_examples '#merge' do
+ it 'merges the code and returns the commit id' do
+ expect(merge_commit).to be_present
+ expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
+ end
- it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
- merge_commit_id = merge(repository, user, merge_request, message)
+ it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
+ merge_commit_id = merge(repository, user, merge_request, message)
- expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
+ expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
+ end
+
+ it 'removes carriage returns from commit message' do
+ merge_commit_id = merge(repository, user, merge_request, message)
+
+ expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r"))
+ end
end
- it 'removes carriage returns from commit message' do
- merge_commit_id = merge(repository, user, merge_request, message)
+ context 'with gitaly' do
+ it_behaves_like '#merge'
+ end
- expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r"))
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like '#merge'
end
def merge(repository, user, merge_request, message)
@@ -1509,7 +1544,9 @@ describe Repository do
:gitignore,
:koding,
:gitlab_ci,
- :avatar
+ :avatar,
+ :issue_template,
+ :merge_request_template
])
repository.after_change_head
@@ -1676,7 +1713,7 @@ describe Repository do
it_behaves_like 'adding tag'
end
- context 'when Gitaly operation_user_add_tag feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly operation_user_add_tag feature is disabled', :skip_gitaly_mock do
it_behaves_like 'adding tag'
it 'passes commit SHA to pre-receive and update hooks and tag SHA to post-receive hook' do
@@ -1735,7 +1772,7 @@ describe Repository do
end
end
- context 'with gitaly disabled', skip_gitaly_mock: true do
+ context 'with gitaly disabled', :skip_gitaly_mock do
it_behaves_like "user deleting a branch"
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
@@ -1794,7 +1831,7 @@ describe Repository do
it_behaves_like 'removing tag'
end
- context 'when Gitaly operation_user_delete_tag feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly operation_user_delete_tag feature is disabled', :skip_gitaly_mock do
it_behaves_like 'removing tag'
end
end
@@ -2098,19 +2135,41 @@ describe Repository do
end
describe '#cache_method_output', :use_clean_rails_memory_store_caching do
+ let(:fallback) { 10 }
+
context 'with a non-existing repository' do
- let(:value) do
- repository.cache_method_output(:cats, fallback: 10) do
- raise Rugged::ReferenceError
+ let(:project) { create(:project) } # No repository
+
+ subject do
+ repository.cache_method_output(:cats, fallback: fallback) do
+ repository.cats_call_stub
+ end
+ end
+
+ it 'returns the fallback value' do
+ expect(subject).to eq(fallback)
+ end
+
+ it 'avoids calling the original method' do
+ expect(repository).not_to receive(:cats_call_stub)
+
+ subject
+ end
+ end
+
+ context 'with a method throwing a non-existing-repository error' do
+ subject do
+ repository.cache_method_output(:cats, fallback: fallback) do
+ raise Gitlab::Git::Repository::NoRepository
end
end
- it 'returns a fallback value' do
- expect(value).to eq(10)
+ it 'returns the fallback value' do
+ expect(subject).to eq(fallback)
end
it 'does not cache the data' do
- value
+ subject
expect(repository.instance_variable_defined?(:@cats)).to eq(false)
expect(repository.send(:cache).exist?(:cats)).to eq(false)
@@ -2226,4 +2285,44 @@ describe Repository do
end
end
end
+
+ describe 'commit cache' do
+ set(:project) { create(:project, :repository) }
+
+ it 'caches based on SHA' do
+ # Gets the commit oid, and warms the cache
+ oid = project.commit.id
+
+ expect(Gitlab::Git::Commit).not_to receive(:find).once
+
+ project.commit_by(oid: oid)
+ end
+
+ it 'caches nil values' do
+ expect(Gitlab::Git::Commit).to receive(:find).once
+
+ project.commit_by(oid: '1' * 40)
+ project.commit_by(oid: '1' * 40)
+ end
+ end
+
+ describe '#raw_repository' do
+ subject { repository.raw_repository }
+
+ it 'returns a Gitlab::Git::Repository representation of the repository' do
+ expect(subject).to be_a(Gitlab::Git::Repository)
+ expect(subject.relative_path).to eq(project.disk_path + '.git')
+ expect(subject.gl_repository).to eq("project-#{project.id}")
+ end
+
+ context 'with a wiki repository' do
+ let(:repository) { project.wiki.repository }
+
+ it 'creates a Gitlab::Git::Repository with the proper attributes' do
+ expect(subject).to be_a(Gitlab::Git::Repository)
+ expect(subject.relative_path).to eq(project.disk_path + '.wiki.git')
+ expect(subject.gl_repository).to eq("wiki-#{project.id}")
+ end
+ end
+ end
end
diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb
index 8f05deb8b15..5ec04b99957 100644
--- a/spec/models/sent_notification_spec.rb
+++ b/spec/models/sent_notification_spec.rb
@@ -1,6 +1,9 @@
require 'spec_helper'
describe SentNotification do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+
describe 'validation' do
describe 'note validity' do
context "when the project doesn't match the noteable's project" do
@@ -34,7 +37,6 @@ describe SentNotification do
end
describe '.record' do
- let(:user) { create(:user) }
let(:issue) { create(:issue) }
it 'creates a new SentNotification' do
@@ -43,7 +45,6 @@ describe SentNotification do
end
describe '.record_note' do
- let(:user) { create(:user) }
let(:note) { create(:diff_note_on_merge_request) }
it 'creates a new SentNotification' do
@@ -51,6 +52,123 @@ describe SentNotification do
end
end
+ describe '#unsubscribable?' do
+ shared_examples 'an unsubscribable notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for #{noteable_type}" do
+ it { expect(subject).to be_unsubscribable }
+ end
+ end
+
+ shared_examples 'a non-unsubscribable notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for a #{noteable_type}" do
+ it { expect(subject).not_to be_unsubscribable }
+ end
+ end
+
+ it_behaves_like 'an unsubscribable notification', 'issue' do
+ let(:noteable) { create(:issue, project: project) }
+ end
+
+ it_behaves_like 'an unsubscribable notification', 'merge request' do
+ let(:noteable) { create(:merge_request, source_project: project) }
+ end
+
+ it_behaves_like 'a non-unsubscribable notification', 'commit' do
+ let(:project) { create(:project, :repository) }
+ let(:noteable) { project.commit }
+ end
+
+ it_behaves_like 'a non-unsubscribable notification', 'personal snippet' do
+ let(:noteable) { create(:personal_snippet, project: project) }
+ end
+
+ it_behaves_like 'a non-unsubscribable notification', 'project snippet' do
+ let(:noteable) { create(:project_snippet, project: project) }
+ end
+ end
+
+ describe '#for_commit?' do
+ shared_examples 'a commit notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for #{noteable_type}" do
+ it { expect(subject).to be_for_commit }
+ end
+ end
+
+ shared_examples 'a non-commit notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for a #{noteable_type}" do
+ it { expect(subject).not_to be_for_commit }
+ end
+ end
+
+ it_behaves_like 'a non-commit notification', 'issue' do
+ let(:noteable) { create(:issue, project: project) }
+ end
+
+ it_behaves_like 'a non-commit notification', 'merge request' do
+ let(:noteable) { create(:merge_request, source_project: project) }
+ end
+
+ it_behaves_like 'a commit notification', 'commit' do
+ let(:project) { create(:project, :repository) }
+ let(:noteable) { project.commit }
+ end
+
+ it_behaves_like 'a non-commit notification', 'personal snippet' do
+ let(:noteable) { create(:personal_snippet, project: project) }
+ end
+
+ it_behaves_like 'a non-commit notification', 'project snippet' do
+ let(:noteable) { create(:project_snippet, project: project) }
+ end
+ end
+
+ describe '#for_snippet?' do
+ shared_examples 'a snippet notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for #{noteable_type}" do
+ it { expect(subject).to be_for_snippet }
+ end
+ end
+
+ shared_examples 'a non-snippet notification' do |noteable_type|
+ subject { described_class.record(noteable, user.id) }
+
+ context "for a #{noteable_type}" do
+ it { expect(subject).not_to be_for_snippet }
+ end
+ end
+
+ it_behaves_like 'a non-snippet notification', 'issue' do
+ let(:noteable) { create(:issue, project: project) }
+ end
+
+ it_behaves_like 'a non-snippet notification', 'merge request' do
+ let(:noteable) { create(:merge_request, source_project: project) }
+ end
+
+ it_behaves_like 'a non-snippet notification', 'commit' do
+ let(:project) { create(:project, :repository) }
+ let(:noteable) { project.commit }
+ end
+
+ it_behaves_like 'a snippet notification', 'personal snippet' do
+ let(:noteable) { create(:personal_snippet, project: project) }
+ end
+
+ it_behaves_like 'a snippet notification', 'project snippet' do
+ let(:noteable) { create(:project_snippet, project: project) }
+ end
+ end
+
describe '#create_reply' do
context 'for issue' do
let(:issue) { create(:issue) }
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index de3ca300ae3..e09d89d235d 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -88,7 +88,7 @@ describe Snippet do
end
describe '.search' do
- let(:snippet) { create(:snippet) }
+ let(:snippet) { create(:snippet, title: 'test snippet') }
it 'returns snippets with a matching title' do
expect(described_class.search(snippet.title)).to eq([snippet])
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index ece6968dde6..b27c1b2cd1a 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -346,7 +346,6 @@ describe User do
describe "Respond to" do
it { is_expected.to respond_to(:admin?) }
it { is_expected.to respond_to(:name) }
- it { is_expected.to respond_to(:private_token) }
it { is_expected.to respond_to(:external?) }
end
@@ -526,14 +525,6 @@ describe User do
end
end
- describe 'authentication token' do
- it "has authentication token" do
- user = create(:user)
-
- expect(user.authentication_token).not_to be_blank
- end
- end
-
describe 'ensure incoming email token' do
it 'has incoming email token' do
user = create(:user)
@@ -651,16 +642,40 @@ describe User do
end
describe 'groups' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+
before do
- @user = create :user
- @group = create :group
- @group.add_owner(@user)
+ group.add_owner(user)
end
- it { expect(@user.several_namespaces?).to be_truthy }
- it { expect(@user.authorized_groups).to eq([@group]) }
- it { expect(@user.owned_groups).to eq([@group]) }
- it { expect(@user.namespaces).to match_array([@user.namespace, @group]) }
+ it { expect(user.several_namespaces?).to be_truthy }
+ it { expect(user.authorized_groups).to eq([group]) }
+ it { expect(user.owned_groups).to eq([group]) }
+ it { expect(user.namespaces).to contain_exactly(user.namespace, group) }
+ it { expect(user.manageable_namespaces).to contain_exactly(user.namespace, group) }
+
+ context 'with child groups', :nested_groups do
+ let!(:subgroup) { create(:group, parent: group) }
+
+ describe '#manageable_namespaces' do
+ it 'includes all the namespaces the user can manage' do
+ expect(user.manageable_namespaces).to contain_exactly(user.namespace, group, subgroup)
+ end
+ end
+
+ describe '#manageable_groups' do
+ it 'includes all the namespaces the user can manage' do
+ expect(user.manageable_groups).to contain_exactly(group, subgroup)
+ end
+
+ it 'does not include duplicates if a membership was added for the subgroup' do
+ subgroup.add_owner(user)
+
+ expect(user.manageable_groups).to contain_exactly(group, subgroup)
+ end
+ end
+ end
end
describe 'group multiple owners' do
@@ -797,21 +812,23 @@ describe User do
end
it "creates external user by default" do
- user = build(:user)
+ user = create(:user)
expect(user.external).to be_truthy
+ expect(user.can_create_group).to be_falsey
+ expect(user.projects_limit).to be 0
end
describe 'with default overrides' do
it "creates a non-external user" do
- user = build(:user, external: false)
+ user = create(:user, external: false)
expect(user.external).to be_falsey
end
end
end
- describe '#require_ssh_key?' do
+ describe '#require_ssh_key?', :use_clean_rails_memory_store_caching do
protocol_and_expectation = {
'http' => false,
'ssh' => true,
@@ -826,6 +843,12 @@ describe User do
expect(user.require_ssh_key?).to eq(expected)
end
end
+
+ it 'returns false when the user has 1 or more SSH keys' do
+ key = create(:personal_key)
+
+ expect(key.user.require_ssh_key?).to eq(false)
+ end
end
end
@@ -848,6 +871,19 @@ describe User do
end
end
+ describe '.by_any_email' do
+ it 'returns an ActiveRecord::Relation' do
+ expect(described_class.by_any_email('foo@example.com'))
+ .to be_a_kind_of(ActiveRecord::Relation)
+ end
+
+ it 'returns a relation of users' do
+ user = create(:user)
+
+ expect(described_class.by_any_email(user.email)).to eq([user])
+ end
+ end
+
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') }
@@ -1143,16 +1179,9 @@ describe User do
let(:user) { create(:user, :with_avatar) }
context 'when avatar file is uploaded' do
- let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
- let(:avatar_path) { "/uploads/-/system/user/avatar/#{user.id}/dk.png" }
-
it 'shows correct avatar url' do
- expect(user.avatar_url).to eq(avatar_path)
- expect(user.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
-
- allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
-
- expect(user.avatar_url).to eq([gitlab_host, avatar_path].join)
+ expect(user.avatar_url).to eq(user.avatar.url)
+ expect(user.avatar_url(only_path: false)).to eq([Gitlab.config.gitlab.url, user.avatar.url].join)
end
end
end
@@ -1525,7 +1554,7 @@ describe User do
it { is_expected.to eq([private_group]) }
end
- describe '#authorized_projects', truncate: true do
+ describe '#authorized_projects', :truncate do
context 'with a minimum access level' do
it 'includes projects for which the user is an owner' do
user = create(:user)
@@ -1877,7 +1906,7 @@ describe User do
end
end
- describe '#refresh_authorized_projects', clean_gitlab_redis_shared_state: true do
+ describe '#refresh_authorized_projects', :clean_gitlab_redis_shared_state do
let(:project1) { create(:project) }
let(:project2) { create(:project) }
let(:user) { create(:user) }
@@ -2114,25 +2143,47 @@ describe User do
end
end
- describe '#allow_password_authentication?' do
+ describe '#allow_password_authentication_for_web?' do
context 'regular user' do
let(:user) { build(:user) }
- it 'returns true when sign-in is enabled' do
- expect(user.allow_password_authentication?).to be_truthy
+ it 'returns true when password authentication is enabled for the web interface' do
+ expect(user.allow_password_authentication_for_web?).to be_truthy
end
- it 'returns false when sign-in is disabled' do
- stub_application_setting(password_authentication_enabled: false)
+ it 'returns false when password authentication is disabled for the web interface' do
+ stub_application_setting(password_authentication_enabled_for_web: false)
- expect(user.allow_password_authentication?).to be_falsey
+ expect(user.allow_password_authentication_for_web?).to be_falsey
end
end
it 'returns false for ldap user' do
user = create(:omniauth_user, provider: 'ldapmain')
- expect(user.allow_password_authentication?).to be_falsey
+ expect(user.allow_password_authentication_for_web?).to be_falsey
+ end
+ end
+
+ describe '#allow_password_authentication_for_git?' do
+ context 'regular user' do
+ let(:user) { build(:user) }
+
+ it 'returns true when password authentication is enabled for Git' do
+ expect(user.allow_password_authentication_for_git?).to be_truthy
+ end
+
+ it 'returns false when password authentication is disabled Git' do
+ stub_application_setting(password_authentication_enabled_for_git: false)
+
+ expect(user.allow_password_authentication_for_git?).to be_falsey
+ end
+ end
+
+ it 'returns false for ldap user' do
+ user = create(:omniauth_user, provider: 'ldapmain')
+
+ expect(user.allow_password_authentication_for_git?).to be_falsey
end
end
@@ -2226,6 +2277,42 @@ describe User do
end
end
+ describe '#username_changed_hook' do
+ context 'for a new user' do
+ let(:user) { build(:user) }
+
+ it 'does not trigger system hook' do
+ expect(user).not_to receive(:system_hook_service)
+
+ user.save!
+ end
+ end
+
+ context 'for an existing user' do
+ let(:user) { create(:user, username: 'old-username') }
+
+ context 'when the username is changed' do
+ let(:new_username) { 'very-new-name' }
+
+ it 'triggers the rename system hook' do
+ system_hook_service = SystemHooksService.new
+ expect(system_hook_service).to receive(:execute_hooks_for).with(user, :rename)
+ expect(user).to receive(:system_hook_service).and_return(system_hook_service)
+
+ user.update_attributes!(username: new_username)
+ end
+ end
+
+ context 'when the username is not changed' do
+ it 'does not trigger system hook' do
+ expect(user).not_to receive(:system_hook_service)
+
+ user.update_attributes!(email: 'asdf@asdf.com')
+ end
+ end
+ end
+ end
+
describe '#sync_attribute?' do
let(:user) { described_class.new }
@@ -2316,7 +2403,8 @@ describe User do
let(:expected) { !(password_automatically_set || ldap_user || password_authentication_disabled) }
before do
- stub_application_setting(password_authentication_enabled: !password_authentication_disabled)
+ stub_application_setting(password_authentication_enabled_for_web: !password_authentication_disabled)
+ stub_application_setting(password_authentication_enabled_for_git: !password_authentication_disabled)
end
it 'returns false unless all inputs are true' do
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 1f14d06997e..ea75434e399 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -373,7 +373,7 @@ describe WikiPage do
end
it 'returns commit sha' do
- expect(@page.last_commit_sha).to eq @page.commit.sha
+ expect(@page.last_commit_sha).to eq @page.last_version.sha
end
it 'is changed after page updated' do
@@ -402,7 +402,7 @@ describe WikiPage do
def destroy_page(title)
page = wiki.wiki.page(title: title)
- wiki.delete_page(page, commit_details)
+ wiki.delete_page(page, "test commit")
end
def get_slugs(page_or_dir)
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 8e1bc3d1543..298a9d16425 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -150,5 +150,82 @@ describe Ci::BuildPolicy do
end
end
end
+
+ describe 'rules for erase build' do
+ let(:project) { create(:project, :repository) }
+ let(:build) { create(:ci_build, pipeline: pipeline, ref: 'some-ref', user: owner) }
+
+ context 'when a developer erases a build' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when developers can push to the branch' do
+ before do
+ create(:protected_branch, :developers_can_push,
+ name: build.ref, project: project)
+ end
+
+ context 'when the build was created by the developer' do
+ let(:owner) { user }
+
+ it { expect(policy).to be_allowed :erase_build }
+ end
+
+ context 'when the build was created by the other' do
+ let(:owner) { create(:user) }
+
+ it { expect(policy).to be_disallowed :erase_build }
+ end
+ end
+
+ context 'when no one can push or merge to the branch' do
+ let(:owner) { user }
+
+ before do
+ create(:protected_branch, :no_one_can_push, :no_one_can_merge,
+ name: build.ref, project: project)
+ end
+
+ it { expect(policy).to be_disallowed :erase_build }
+ end
+ end
+
+ context 'when a master erases a build' do
+ before do
+ project.add_master(user)
+ end
+
+ context 'when masters can push to the branch' do
+ before do
+ create(:protected_branch, :masters_can_push,
+ name: build.ref, project: project)
+ end
+
+ context 'when the build was created by the master' do
+ let(:owner) { user }
+
+ it { expect(policy).to be_allowed :erase_build }
+ end
+
+ context 'when the build was created by the other' do
+ let(:owner) { create(:user) }
+
+ it { expect(policy).to be_allowed :erase_build }
+ end
+ end
+
+ context 'when no one can push or merge to the branch' do
+ let(:owner) { user }
+
+ before do
+ create(:protected_branch, :no_one_can_push, :no_one_can_merge,
+ name: build.ref, project: project)
+ end
+
+ it { expect(policy).to be_disallowed :erase_build }
+ end
+ end
+ end
end
end
diff --git a/spec/policies/clusters/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb
new file mode 100644
index 00000000000..4207f42b07f
--- /dev/null
+++ b/spec/policies/clusters/cluster_policy_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Clusters::ClusterPolicy, :models do
+ let(:cluster) { create(:cluster, :project) }
+ let(:project) { cluster.project }
+ let(:user) { create(:user) }
+ let(:policy) { described_class.new(user, cluster) }
+
+ describe 'rules' do
+ context 'when developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when master' do
+ before do
+ project.add_master(user)
+ end
+
+ it { expect(policy).to be_allowed :update_cluster }
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+ end
+end
diff --git a/spec/policies/gcp/cluster_policy_spec.rb b/spec/policies/gcp/cluster_policy_spec.rb
deleted file mode 100644
index e213aa3d557..00000000000
--- a/spec/policies/gcp/cluster_policy_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-require 'spec_helper'
-
-describe Gcp::ClusterPolicy, :models do
- set(:project) { create(:project) }
- set(:cluster) { create(:gcp_cluster, project: project) }
- let(:user) { create(:user) }
- let(:policy) { described_class.new(user, cluster) }
-
- describe 'rules' do
- context 'when developer' do
- before do
- project.add_developer(user)
- end
-
- it { expect(policy).to be_disallowed :update_cluster }
- it { expect(policy).to be_disallowed :admin_cluster }
- end
-
- context 'when master' do
- before do
- project.add_master(user)
- end
-
- it { expect(policy).to be_allowed :update_cluster }
- it { expect(policy).to be_allowed :admin_cluster }
- end
- end
-end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 17dc3bb4f48..4f4e634829d 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -56,6 +56,7 @@ describe GroupPolicy do
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
+ expect_disallowed(:read_namespace)
end
end
@@ -63,7 +64,7 @@ describe GroupPolicy do
let(:current_user) { guest }
it do
- expect_allowed(:read_group)
+ expect_allowed(:read_group, :read_namespace)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -75,7 +76,7 @@ describe GroupPolicy do
let(:current_user) { reporter }
it do
- expect_allowed(:read_group)
+ expect_allowed(:read_group, :read_namespace)
expect_allowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -87,7 +88,7 @@ describe GroupPolicy do
let(:current_user) { developer }
it do
- expect_allowed(:read_group)
+ expect_allowed(:read_group, :read_namespace)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -99,7 +100,7 @@ describe GroupPolicy do
let(:current_user) { master }
it do
- expect_allowed(:read_group)
+ expect_allowed(:read_group, :read_namespace)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
@@ -113,7 +114,7 @@ describe GroupPolicy do
it do
allow(Group).to receive(:supports_nested_groups?).and_return(true)
- expect_allowed(:read_group)
+ expect_allowed(:read_group, :read_namespace)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
@@ -127,7 +128,7 @@ describe GroupPolicy do
it do
allow(Group).to receive(:supports_nested_groups?).and_return(true)
- expect_allowed(:read_group)
+ expect_allowed(:read_group, :read_namespace)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb
index e52ff02e5f0..1fdf95ad716 100644
--- a/spec/policies/namespace_policy_spec.rb
+++ b/spec/policies/namespace_policy_spec.rb
@@ -1,20 +1,42 @@
require 'spec_helper'
describe NamespacePolicy do
- let(:current_user) { create(:user) }
- let(:namespace) { current_user.namespace }
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, owner: owner) }
+
+ let(:owner_permissions) { [:create_projects, :admin_namespace, :read_namespace] }
subject { described_class.new(current_user, namespace) }
- context "create projects" do
- context "user namespace" do
- it { is_expected.to be_allowed(:create_projects) }
- end
+ context 'with no user' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_banned }
+ end
+
+ context 'regular user' do
+ let(:current_user) { user }
+
+ it { is_expected.to be_disallowed(*owner_permissions) }
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(*owner_permissions) }
- context "user who has exceeded project limit" do
- let(:current_user) { create(:user, projects_limit: 0) }
+ context 'user who has exceeded project limit' do
+ let(:owner) { create(:user, projects_limit: 0) }
it { is_expected.not_to be_allowed(:create_projects) }
end
end
+
+ context 'admin' do
+ let(:current_user) { admin }
+
+ it { is_expected.to be_allowed(*owner_permissions) }
+ end
end
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
new file mode 100644
index 00000000000..48d4f3671c5
--- /dev/null
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Clusters::ClusterPresenter do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ subject(:presenter) do
+ described_class.new(cluster)
+ end
+
+ it 'inherits from Gitlab::View::Presenter::Delegated' do
+ expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+ end
+
+ describe '#initialize' do
+ it 'takes a cluster and optional params' do
+ expect { presenter }.not_to raise_error
+ end
+
+ it 'exposes cluster' do
+ expect(presenter.cluster).to eq(cluster)
+ end
+
+ it 'forwards missing methods to cluster' do
+ expect(presenter.status).to eq(cluster.status)
+ end
+ end
+
+ describe '#gke_cluster_url' do
+ subject { described_class.new(cluster).gke_cluster_url }
+
+ it { is_expected.to include(cluster.provider.zone) }
+ it { is_expected.to include(cluster.name) }
+ end
+end
diff --git a/spec/presenters/gcp/cluster_presenter_spec.rb b/spec/presenters/gcp/cluster_presenter_spec.rb
deleted file mode 100644
index 8d86dc31582..00000000000
--- a/spec/presenters/gcp/cluster_presenter_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'spec_helper'
-
-describe Gcp::ClusterPresenter do
- let(:project) { create(:project) }
- let(:cluster) { create(:gcp_cluster, project: project) }
-
- subject(:presenter) do
- described_class.new(cluster)
- end
-
- it 'inherits from Gitlab::View::Presenter::Delegated' do
- expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
- end
-
- describe '#initialize' do
- it 'takes a cluster and optional params' do
- expect { presenter }.not_to raise_error
- end
-
- it 'exposes cluster' do
- expect(presenter.cluster).to eq(cluster)
- end
-
- it 'forwards missing methods to cluster' do
- expect(presenter.gcp_cluster_zone).to eq(cluster.gcp_cluster_zone)
- end
- end
-
- describe '#gke_cluster_url' do
- subject { described_class.new(cluster).gke_cluster_url }
-
- it { is_expected.to include(cluster.gcp_cluster_zone) }
- it { is_expected.to include(cluster.gcp_cluster_name) }
- end
-end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 50d0f72f6bc..35ca3635a9d 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -35,7 +35,7 @@ describe API::AccessRequests do
user = public_send(type)
get api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -45,7 +45,7 @@ describe API::AccessRequests do
it 'returns access requesters' do
get api("/#{source_type.pluralize}/#{source.id}/access_requests", master)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
@@ -68,7 +68,7 @@ describe API::AccessRequests do
user = public_send(type)
post api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end.not_to change { source.requesters.count }
end
end
@@ -80,7 +80,7 @@ describe API::AccessRequests do
expect do
post api("/#{source_type.pluralize}/#{source.id}/access_requests", access_requester)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end.not_to change { source.requesters.count }
end
end
@@ -95,7 +95,7 @@ describe API::AccessRequests do
expect do
post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end.not_to change { source.requesters.count }
end
end
@@ -104,7 +104,7 @@ describe API::AccessRequests do
expect do
post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end.to change { source.requesters.count }.by(1)
# User attributes
@@ -135,7 +135,7 @@ describe API::AccessRequests do
user = public_send(type)
put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -147,7 +147,7 @@ describe API::AccessRequests do
put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", master),
access_level: Member::MASTER
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end.to change { source.members.count }.by(1)
# User attributes
expect(json_response['id']).to eq(access_requester.id)
@@ -166,7 +166,7 @@ describe API::AccessRequests do
expect do
put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}/approve", master)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end.not_to change { source.members.count }
end
end
@@ -187,7 +187,7 @@ describe API::AccessRequests do
user = public_send(type)
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -198,7 +198,7 @@ describe API::AccessRequests do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { source.requesters.count }.by(-1)
end
end
@@ -208,7 +208,7 @@ describe API::AccessRequests do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { source.requesters.count }.by(-1)
end
@@ -217,7 +217,7 @@ describe API::AccessRequests do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{developer.id}", master)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end.not_to change { source.requesters.count }
end
end
@@ -227,7 +227,7 @@ describe API::AccessRequests do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}", master)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end.not_to change { source.requesters.count }
end
end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 7a0765c1fae..eaf12f71421 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -18,7 +18,7 @@ describe API::AwardEmoji do
it "returns an array of award_emoji" do
get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(award_emoji.name)
end
@@ -26,7 +26,7 @@ describe API::AwardEmoji do
it "returns a 404 error when issue id not found" do
get api("/projects/#{project.id}/issues/12345/award_emoji", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -34,7 +34,7 @@ describe API::AwardEmoji do
it "returns an array of award_emoji" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(downvote.name)
@@ -48,7 +48,7 @@ describe API::AwardEmoji do
it 'returns the awarded emoji' do
get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(award.name)
end
@@ -60,7 +60,7 @@ describe API::AwardEmoji do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -71,7 +71,7 @@ describe API::AwardEmoji do
it 'returns an array of award emoji' do
get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(rocket.name)
end
@@ -82,7 +82,7 @@ describe API::AwardEmoji do
it "returns the award emoji" do
get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(award_emoji.name)
expect(json_response['awardable_id']).to eq(issue.id)
expect(json_response['awardable_type']).to eq("Issue")
@@ -91,7 +91,7 @@ describe API::AwardEmoji do
it "returns a 404 error if the award is not found" do
get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -99,7 +99,7 @@ describe API::AwardEmoji do
it 'returns the award emoji' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(downvote.name)
expect(json_response['awardable_id']).to eq(merge_request.id)
expect(json_response['awardable_type']).to eq("MergeRequest")
@@ -113,7 +113,7 @@ describe API::AwardEmoji do
it 'returns the awarded emoji' do
get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(award.name)
expect(json_response['awardable_id']).to eq(snippet.id)
expect(json_response['awardable_type']).to eq("Snippet")
@@ -126,7 +126,7 @@ describe API::AwardEmoji do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -137,7 +137,7 @@ describe API::AwardEmoji do
it 'returns an award emoji' do
get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).not_to be_an Array
expect(json_response['name']).to eq(rocket.name)
end
@@ -150,7 +150,7 @@ describe API::AwardEmoji do
it "creates a new award emoji" do
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'blowfish'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq('blowfish')
expect(json_response['user']['username']).to eq(user.username)
end
@@ -158,19 +158,19 @@ describe API::AwardEmoji do
it "returns a 400 bad request error if the name is not given" do
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 401 unauthorized error if the user is not authenticated" do
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji"), name: 'thumbsup'
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it "returns a 404 error if the user authored issue" do
post api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "normalizes +1 as thumbsup award" do
@@ -184,7 +184,7 @@ describe API::AwardEmoji do
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'thumbsup'
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'thumbsup'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response["message"]).to match("has already been taken")
end
end
@@ -196,7 +196,7 @@ describe API::AwardEmoji do
post api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq('blowfish')
expect(json_response['user']['username']).to eq(user.username)
end
@@ -211,14 +211,14 @@ describe API::AwardEmoji do
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket'
end.to change { note.award_emoji.count }.from(0).to(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['user']['username']).to eq(user.username)
end
it "it returns 404 error when user authored note" do
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "normalizes +1 as thumbsup award" do
@@ -232,7 +232,7 @@ describe API::AwardEmoji do
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket'
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response["message"]).to match("has already been taken")
end
end
@@ -244,14 +244,14 @@ describe API::AwardEmoji do
expect do
delete api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { issue.award_emoji.count }.from(1).to(0)
end
it 'returns a 404 error when the award emoji can not be found' do
delete api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it_behaves_like '412 response' do
@@ -264,14 +264,14 @@ describe API::AwardEmoji do
expect do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { merge_request.award_emoji.count }.from(1).to(0)
end
it 'returns a 404 error when note id not found' do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it_behaves_like '412 response' do
@@ -287,7 +287,7 @@ describe API::AwardEmoji do
expect do
delete api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { snippet.award_emoji.count }.from(1).to(0)
end
@@ -304,7 +304,7 @@ describe API::AwardEmoji do
expect do
delete api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { note.award_emoji.count }.from(1).to(0)
end
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index fcfa4ddfbfe..546a1697e56 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -44,7 +44,7 @@ describe API::Boards do
it "returns authentication error" do
get api(base_url)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -52,7 +52,7 @@ describe API::Boards do
it "returns the project issue board" do
get api(base_url, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -70,7 +70,7 @@ describe API::Boards do
it 'returns issue board lists' do
get api(base_url, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
@@ -80,7 +80,7 @@ describe API::Boards do
it 'returns 404 if board not found' do
get api("/projects/#{project.id}/boards/22343/lists", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -90,7 +90,7 @@ describe API::Boards do
it 'returns a list' do
get api("#{base_url}/#{dev_list.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(dev_list.id)
expect(json_response['label']['name']).to eq(dev_label.title)
expect(json_response['position']).to eq(1)
@@ -99,7 +99,7 @@ describe API::Boards do
it 'returns 404 if list not found' do
get api("#{base_url}/5324", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -113,7 +113,7 @@ describe API::Boards do
post api(base_url, user), label_id: group_label.id
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['label']['name']).to eq(group_label.title)
expect(json_response['position']).to eq(3)
end
@@ -121,7 +121,7 @@ describe API::Boards do
it 'creates a new issue board list for project labels' do
post api(base_url, user), label_id: ux_label.id
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['label']['name']).to eq(ux_label.title)
expect(json_response['position']).to eq(3)
end
@@ -129,13 +129,13 @@ describe API::Boards do
it 'returns 400 when creating a new list if label_id is invalid' do
post api(base_url, user), label_id: 23423
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 403 for project members with guest role' do
put api("#{base_url}/#{test_list.id}", guest), position: 1
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -146,7 +146,7 @@ describe API::Boards do
put api("#{base_url}/#{test_list.id}", user),
position: 1
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['position']).to eq(1)
end
@@ -154,14 +154,14 @@ describe API::Boards do
put api("#{base_url}/44444", user),
position: 1
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 403 for project members with guest role" do
put api("#{base_url}/#{test_list.id}", guest),
position: 1
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -171,19 +171,19 @@ describe API::Boards do
it "rejects a non member from deleting a list" do
delete api("#{base_url}/#{dev_list.id}", non_member)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "rejects a user with guest role from deleting a list" do
delete api("#{base_url}/#{dev_list.id}", guest)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "returns 404 error if list id not found" do
delete api("#{base_url}/44444", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context "when the user is project owner" do
@@ -196,7 +196,7 @@ describe API::Boards do
it "deletes the list if an admin requests it" do
delete api("#{base_url}/#{dev_list.id}", owner)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end
it_behaves_like '412 response' do
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 16b12446ed4..e433597f58b 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -110,6 +110,15 @@ describe API::Branches do
end
end
+ context 'when the branch refname is invalid' do
+ let(:branch_name) { 'branch*' }
+ let(:message) { 'The branch refname is invalid' }
+
+ it_behaves_like '400 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
+
context 'when repository is disabled' do
include_context 'disabled repository'
@@ -234,6 +243,15 @@ describe API::Branches do
end
end
+ context 'when the branch refname is invalid' do
+ let(:branch_name) { 'branch*' }
+ let(:message) { 'The branch refname is invalid' }
+
+ it_behaves_like '400 response' do
+ let(:request) { put api(route, current_user) }
+ end
+ end
+
context 'when repository is disabled' do
include_context 'disabled repository'
@@ -359,6 +377,15 @@ describe API::Branches do
end
end
+ context 'when the branch refname is invalid' do
+ let(:branch_name) { 'branch*' }
+ let(:message) { 'The branch refname is invalid' }
+
+ it_behaves_like '400 response' do
+ let(:request) { put api(route, current_user) }
+ end
+ end
+
context 'when repository is disabled' do
include_context 'disabled repository'
@@ -520,6 +547,15 @@ describe API::Branches do
expect(response).to have_gitlab_http_status(404)
end
+ context 'when the branch refname is invalid' do
+ let(:branch_name) { 'branch*' }
+ let(:message) { 'The branch refname is invalid' }
+
+ it_behaves_like '400 response' do
+ let(:request) { delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user) }
+ end
+ end
+
it_behaves_like '412 response' do
let(:request) { api("/projects/#{project.id}/repository/branches/#{branch_name}", user) }
end
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index eacc575d97f..fe8a14fae9e 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -9,13 +9,13 @@ describe API::BroadcastMessages do
it 'returns a 401 for anonymous users' do
get api('/broadcast_messages')
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns a 403 for users' do
get api('/broadcast_messages', user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'returns an Array of BroadcastMessages for admins' do
@@ -23,7 +23,7 @@ describe API::BroadcastMessages do
get api('/broadcast_messages', admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_kind_of(Array)
expect(json_response.first.keys)
@@ -35,19 +35,19 @@ describe API::BroadcastMessages do
it 'returns a 401 for anonymous users' do
get api("/broadcast_messages/#{message.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns a 403 for users' do
get api("/broadcast_messages/#{message.id}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'returns the specified message for admins' do
get api("/broadcast_messages/#{message.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq message.id
expect(json_response.keys)
.to match_array(%w(id message starts_at ends_at color font active))
@@ -58,13 +58,13 @@ describe API::BroadcastMessages do
it 'returns a 401 for anonymous users' do
post api('/broadcast_messages'), attributes_for(:broadcast_message)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns a 403 for users' do
post api('/broadcast_messages', user), attributes_for(:broadcast_message)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
context 'as an admin' do
@@ -74,7 +74,7 @@ describe API::BroadcastMessages do
post api('/broadcast_messages', admin), attrs
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq 'message is missing'
end
@@ -83,7 +83,7 @@ describe API::BroadcastMessages do
travel_to(time) do
post api('/broadcast_messages', admin), message: 'Test message'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z'
expect(json_response['ends_at']).to eq '2016-07-02T11:11:12.000Z'
end
@@ -94,7 +94,7 @@ describe API::BroadcastMessages do
post api('/broadcast_messages', admin), attrs
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['color']).to eq attrs[:color]
expect(json_response['font']).to eq attrs[:font]
end
@@ -106,14 +106,14 @@ describe API::BroadcastMessages do
put api("/broadcast_messages/#{message.id}"),
attributes_for(:broadcast_message)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns a 403 for users' do
put api("/broadcast_messages/#{message.id}", user),
attributes_for(:broadcast_message)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
context 'as an admin' do
@@ -122,7 +122,7 @@ describe API::BroadcastMessages do
put api("/broadcast_messages/#{message.id}", admin), attrs
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['color']).to eq attrs[:color]
expect(json_response['font']).to eq attrs[:font]
end
@@ -134,7 +134,7 @@ describe API::BroadcastMessages do
put api("/broadcast_messages/#{message.id}", admin), attrs
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z'
expect(json_response['ends_at']).to eq '2016-07-02T13:11:12.000Z'
end
@@ -145,7 +145,7 @@ describe API::BroadcastMessages do
put api("/broadcast_messages/#{message.id}", admin), attrs
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect { message.reload }.to change { message.message }.to('new message')
end
end
@@ -156,14 +156,14 @@ describe API::BroadcastMessages do
delete api("/broadcast_messages/#{message.id}"),
attributes_for(:broadcast_message)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns a 403 for users' do
delete api("/broadcast_messages/#{message.id}", user),
attributes_for(:broadcast_message)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it_behaves_like '412 response' do
@@ -174,7 +174,7 @@ describe API::BroadcastMessages do
expect do
delete api("/broadcast_messages/#{message.id}", admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { BroadcastMessage.count }.by(-1)
end
end
diff --git a/spec/requests/api/circuit_breakers_spec.rb b/spec/requests/api/circuit_breakers_spec.rb
index 76521e55994..3b858c40fd6 100644
--- a/spec/requests/api/circuit_breakers_spec.rb
+++ b/spec/requests/api/circuit_breakers_spec.rb
@@ -8,13 +8,13 @@ describe API::CircuitBreakers do
it 'returns a 401 for anonymous users' do
get api('/circuit_breakers/repository_storage')
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns a 403 for users' do
get api('/circuit_breakers/repository_storage', user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'returns an Array of storages' do
@@ -24,7 +24,7 @@ describe API::CircuitBreakers do
get api('/circuit_breakers/repository_storage', admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_kind_of(Array)
expect(json_response.first['storage_name']).to eq('broken')
expect(json_response.first['failing_on_hosts']).to eq(['web01'])
@@ -39,7 +39,7 @@ describe API::CircuitBreakers do
get api('/circuit_breakers/repository_storage/failing', admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_kind_of(Array)
end
end
@@ -51,7 +51,7 @@ describe API::CircuitBreakers do
delete api('/circuit_breakers/repository_storage', admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end
end
end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index e4c73583545..ffa17d296e8 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -39,7 +39,7 @@ describe API::CommitStatuses do
end
it 'returns latest commit statuses' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
@@ -55,7 +55,7 @@ describe API::CommitStatuses do
end
it 'returns all commit statuses' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status1.id, status2.id,
@@ -70,7 +70,7 @@ describe API::CommitStatuses do
end
it 'returns latest commit statuses for specific ref' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status3.id, status5.id)
@@ -83,7 +83,7 @@ describe API::CommitStatuses do
end
it 'return latest commit statuses for specific name' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status4.id, status5.id)
@@ -110,7 +110,7 @@ describe API::CommitStatuses do
end
it "does not return project commits" do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -120,7 +120,7 @@ describe API::CommitStatuses do
end
it "does not return project commits" do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -135,7 +135,7 @@ describe API::CommitStatuses do
it 'creates commit status' do
post api(post_url, developer), state: status
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['sha']).to eq(commit.id)
expect(json_response['status']).to eq(status)
expect(json_response['name']).to eq('default')
@@ -159,7 +159,7 @@ describe API::CommitStatuses do
it "to #{status}" do
expect { post api(post_url, developer), state: status }.not_to change { CommitStatus.count }
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['status']).to eq(status)
end
end
@@ -181,7 +181,7 @@ describe API::CommitStatuses do
it 'creates commit status' do
subject
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['sha']).to eq(commit.id)
expect(json_response['status']).to eq('success')
expect(json_response['name']).to eq('coverage')
@@ -197,7 +197,7 @@ describe API::CommitStatuses do
it 'sets head pipeline' do
subject
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(merge_request.reload.head_pipeline).not_to be_nil
end
end
@@ -224,7 +224,7 @@ describe API::CommitStatuses do
end
it 'updates a commit status' do
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['sha']).to eq(commit.id)
expect(json_response['status']).to eq('success')
expect(json_response['name']).to eq('coverage')
@@ -250,7 +250,7 @@ describe API::CommitStatuses do
end
it 'correctly posts a new commit status' do
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['sha']).to eq(commit.id)
expect(json_response['status']).to eq('success')
end
@@ -268,7 +268,7 @@ describe API::CommitStatuses do
end
it 'does not create commit status' do
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -278,7 +278,7 @@ describe API::CommitStatuses do
end
it 'does not create commit status' do
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -290,7 +290,7 @@ describe API::CommitStatuses do
end
it 'returns not found error' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -301,7 +301,7 @@ describe API::CommitStatuses do
end
it 'responds with bad request status and validation errors' do
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['target_url'])
.to include 'must be a valid URL'
end
@@ -314,7 +314,7 @@ describe API::CommitStatuses do
end
it 'does not create commit status' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -324,7 +324,7 @@ describe API::CommitStatuses do
end
it 'does not create commit status' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -334,7 +334,7 @@ describe API::CommitStatuses do
end
it 'does not create commit status' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 94462b4572d..0d2bd3207c0 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -24,7 +24,7 @@ describe API::Commits do
get api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/commits')
expect(json_response.first['id']).to eq(commit.id)
expect(json_response.first['committer_name']).to eq(commit.committer_name)
@@ -119,7 +119,7 @@ describe API::Commits do
it "returns an invalid parameter error message" do
get api("/projects/#{project_id}/repository/commits?since=invalid-date", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('since is invalid')
end
end
@@ -198,13 +198,13 @@ describe API::Commits do
it 'returns a 403 unauthorized for user without permissions' do
post api(url, guest)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'returns a 400 bad request if no params are given' do
post api(url, user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
describe 'create' do
@@ -248,7 +248,7 @@ describe API::Commits do
it 'returns a 400 bad request if file exists' do
post api(url, user), invalid_c_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context 'with project path containing a dot in URL' do
@@ -257,7 +257,7 @@ describe API::Commits do
it 'a new file in project repo' do
post api(url, user), valid_c_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
end
end
@@ -292,14 +292,14 @@ describe API::Commits do
it 'an existing file in project repo' do
post api(url, user), valid_d_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq(message)
end
it 'returns a 400 bad request if file does not exist' do
post api(url, user), invalid_d_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -337,14 +337,14 @@ describe API::Commits do
it 'an existing file in project repo' do
post api(url, user), valid_m_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq(message)
end
it 'returns a 400 bad request if file does not exist' do
post api(url, user), invalid_m_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -380,14 +380,14 @@ describe API::Commits do
it 'an existing file in project repo' do
post api(url, user), valid_u_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq(message)
end
it 'returns a 400 bad request if file does not exist' do
post api(url, user), invalid_u_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -453,14 +453,14 @@ describe API::Commits do
it 'are commited as one in project repo' do
post api(url, user), valid_mo_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq(message)
end
it 'return a 400 bad request if there are any issues' do
post api(url, user), invalid_mo_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
end
@@ -571,7 +571,7 @@ describe API::Commits do
it 'includes a "created" status' do
get api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/commit/detail')
expect(json_response['status']).to eq('created')
expect(json_response['last_pipeline']['id']).to eq(pipeline.id)
@@ -588,7 +588,7 @@ describe API::Commits do
it 'includes a "success" status' do
get api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/commit/detail')
expect(json_response['status']).to eq('success')
end
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 684877c33c0..1f1e6ea17e4 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -48,7 +48,7 @@ describe API::DeployKeys do
it 'returns array of ssh keys' do
get api("/projects/#{project.id}/deploy_keys", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(deploy_key.title)
@@ -59,14 +59,14 @@ describe API::DeployKeys do
it 'returns a single key' do
get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(deploy_key.title)
end
it 'returns 404 Not Found with invalid ID' do
get api("/projects/#{project.id}/deploy_keys/404", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -74,14 +74,14 @@ describe API::DeployKeys do
it 'does not create an invalid ssh key' do
post api("/projects/#{project.id}/deploy_keys", admin), { title: 'invalid key' }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('key is missing')
end
it 'does not create a key without title' do
post api("/projects/#{project.id}/deploy_keys", admin), key: 'some key'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('title is missing')
end
@@ -98,7 +98,7 @@ describe API::DeployKeys do
post api("/projects/#{project.id}/deploy_keys", admin), { key: deploy_key.key, title: deploy_key.title }
end.not_to change { project.deploy_keys.count }
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
it 'joins an existing ssh key to a new project' do
@@ -106,7 +106,7 @@ describe API::DeployKeys do
post api("/projects/#{project2.id}/deploy_keys", admin), { key: deploy_key.key, title: deploy_key.title }
end.to change { project2.deploy_keys.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
it 'accepts can_push parameter' do
@@ -114,7 +114,7 @@ describe API::DeployKeys do
post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['can_push']).to eq(true)
end
end
@@ -130,7 +130,7 @@ describe API::DeployKeys do
put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin), { title: 'new title' }
end.not_to change(deploy_key, :title)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'does not update a public deploy key as non admin' do
@@ -138,7 +138,7 @@ describe API::DeployKeys do
put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", user), { title: 'new title' }
end.not_to change(deploy_key, :title)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'does not update a private key with invalid title' do
@@ -148,7 +148,7 @@ describe API::DeployKeys do
put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), { title: '' }
end.not_to change(deploy_key, :title)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'updates a private ssh key with correct attributes' do
@@ -181,14 +181,14 @@ describe API::DeployKeys do
expect do
delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { project.deploy_keys.count }.by(-1)
end
it 'returns 404 Not Found with invalid ID' do
delete api("/projects/#{project.id}/deploy_keys/404", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it_behaves_like '412 response' do
@@ -205,7 +205,7 @@ describe API::DeployKeys do
post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", admin)
end.to change { project2.deploy_keys.count }.from(0).to(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['id']).to eq(deploy_key.id)
end
end
@@ -214,7 +214,7 @@ describe API::DeployKeys do
it 'returns a 404 error' do
post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 90d78d060ca..c7977e624ff 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -15,7 +15,7 @@ describe API::Deployments do
it 'returns projects deployments' do
get api("/projects/#{project.id}/deployments", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
@@ -28,7 +28,7 @@ describe API::Deployments do
it 'returns a 404 status code' do
get api("/projects/#{project.id}/deployments", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -38,7 +38,7 @@ describe API::Deployments do
it 'returns the projects deployment' do
get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['sha']).to match /\A\h{40}\z/
expect(json_response['id']).to eq(deployment.id)
end
@@ -48,7 +48,7 @@ describe API::Deployments do
it 'returns a 404 status code' do
get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index 868fef65c1c..308134eba72 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -8,7 +8,7 @@ describe 'doorkeeper access' do
describe "unauthenticated" do
it "returns authentication success" do
get api("/user"), access_token: token.token
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
include_examples 'user login request with unique ip limit' do
@@ -21,14 +21,14 @@ describe 'doorkeeper access' do
describe "when token invalid" do
it "returns authentication error" do
get api("/user"), access_token: "123a"
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
- describe "authorization by private token" do
+ describe "authorization by OAuth token" do
it "returns authentication success" do
get api("/user", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
include_examples 'user login request with unique ip limit' do
@@ -39,20 +39,20 @@ describe 'doorkeeper access' do
end
describe "when user is blocked" do
- it "returns authentication error" do
+ it "returns authorization error" do
user.block
get api("/user"), access_token: token.token
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(403)
end
end
describe "when user is ldap_blocked" do
- it "returns authentication error" do
+ it "returns authorization error" do
user.ldap_block
get api("/user"), access_token: token.token
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index f8cd529a06c..3665cfd7241 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -25,7 +25,7 @@ describe API::Environments do
get api("/projects/#{project.id}/environments", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
@@ -39,7 +39,7 @@ describe API::Environments do
it 'returns a 404 status code' do
get api("/projects/#{project.id}/environments", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -49,7 +49,7 @@ describe API::Environments do
it 'creates a environment with valid params' do
post api("/projects/#{project.id}/environments", user), name: "mepmep"
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq('mepmep')
expect(json_response['slug']).to eq('mepmep')
expect(json_response['external']).to be nil
@@ -58,19 +58,19 @@ describe API::Environments do
it 'requires name to be passed' do
post api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns a 400 if environment already exists' do
post api("/projects/#{project.id}/environments", user), name: environment.name
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns a 400 if slug is specified' do
post api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo"
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
end
end
@@ -79,7 +79,7 @@ describe API::Environments do
it 'rejects the request' do
post api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 400 when the required params are missing' do
@@ -94,7 +94,7 @@ describe API::Environments do
put api("/projects/#{project.id}/environments/#{environment.id}", user),
name: 'Mepmep', external_url: url
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq('Mepmep')
expect(json_response['external_url']).to eq(url)
end
@@ -104,7 +104,7 @@ describe API::Environments do
api_url = api("/projects/#{project.id}/environments/#{environment.id}", user)
put api_url, slug: slug + "-foo"
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
end
@@ -113,7 +113,7 @@ describe API::Environments do
put api("/projects/#{project.id}/environments/#{environment.id}", user),
name: 'Mepmep'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq('Mepmep')
expect(json_response['external_url']).to eq(url)
end
@@ -121,7 +121,7 @@ describe API::Environments do
it 'returns a 404 if the environment does not exist' do
put api("/projects/#{project.id}/environments/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -130,13 +130,13 @@ describe API::Environments do
it 'returns a 200 for an existing environment' do
delete api("/projects/#{project.id}/environments/#{environment.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end
it 'returns a 404 for non existing id' do
delete api("/projects/#{project.id}/environments/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
@@ -149,7 +149,7 @@ describe API::Environments do
it 'rejects the request' do
delete api("/projects/#{project.id}/environments/#{environment.id}", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -164,7 +164,7 @@ describe API::Environments do
end
it 'returns a 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'actually stops the environment' do
@@ -175,7 +175,7 @@ describe API::Environments do
it 'returns a 404 for non existing id' do
post api("/projects/#{project.id}/environments/12345/stop", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
end
@@ -184,7 +184,7 @@ describe API::Environments do
it 'rejects the request' do
post api("/projects/#{project.id}/environments/#{environment.id}/stop", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index a23d28994ce..962c845f36d 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -14,7 +14,7 @@ describe API::Events do
it 'returns authentication error' do
get api('/events')
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -22,7 +22,7 @@ describe API::Events do
it 'returns users events' do
get api('/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
@@ -35,7 +35,7 @@ describe API::Events do
it 'returns no events' do
get api("/users/#{user.id}/events", other_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_empty
end
end
@@ -44,7 +44,7 @@ describe API::Events do
it 'accepts a username' do
get api("/users/#{user.username}/events", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
@@ -53,7 +53,7 @@ describe API::Events do
it 'returns the events' do
get api("/users/#{user.id}/events", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
@@ -72,7 +72,7 @@ describe API::Events do
end
it 'responds with HTTP 200 OK' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'includes the push payload as a Hash' do
@@ -120,7 +120,7 @@ describe API::Events do
it 'returns a 404 error if not found' do
get api('/users/42/events', user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
end
@@ -130,7 +130,7 @@ describe API::Events do
it 'returns 404 for private project' do
get api("/projects/#{private_project.id}/events")
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 200 status for a public project' do
@@ -138,7 +138,7 @@ describe API::Events do
get api("/projects/#{public_project.id}/events")
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -146,7 +146,7 @@ describe API::Events do
it 'returns 404' do
get api("/projects/#{private_project.id}/events", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -154,7 +154,7 @@ describe API::Events do
it 'returns project events' do
get api("/projects/#{private_project.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
@@ -163,7 +163,7 @@ describe API::Events do
it 'returns 404 if project does not exist' do
get api("/projects/1234/events", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb
index 7e21006b254..267058d98ee 100644
--- a/spec/requests/api/features_spec.rb
+++ b/spec/requests/api/features_spec.rb
@@ -44,19 +44,19 @@ describe API::Features do
it 'returns a 401 for anonymous users' do
get api('/features')
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns a 403 for users' do
get api('/features', user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'returns the feature list for admins' do
get api('/features', admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to match_array(expected_features)
end
end
@@ -68,20 +68,20 @@ describe API::Features do
it 'returns a 401 for anonymous users' do
post api("/features/#{feature_name}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns a 403 for users' do
post api("/features/#{feature_name}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
context 'when passed value=true' do
it 'creates an enabled feature' do
post api("/features/#{feature_name}", admin), value: 'true'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'on',
@@ -91,7 +91,7 @@ describe API::Features do
it 'creates an enabled feature for the given Flipper group when passed feature_group=perf_team' do
post api("/features/#{feature_name}", admin), value: 'true', feature_group: 'perf_team'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'conditional',
@@ -104,7 +104,7 @@ describe API::Features do
it 'creates an enabled feature for the given user when passed user=username' do
post api("/features/#{feature_name}", admin), value: 'true', user: user.username
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'conditional',
@@ -117,7 +117,7 @@ describe API::Features do
it 'creates an enabled feature for the given user and feature group when passed user=username and feature_group=perf_team' do
post api("/features/#{feature_name}", admin), value: 'true', user: user.username, feature_group: 'perf_team'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'conditional',
@@ -132,7 +132,7 @@ describe API::Features do
it 'creates a feature with the given percentage if passed an integer' do
post api("/features/#{feature_name}", admin), value: '50'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'conditional',
@@ -154,7 +154,7 @@ describe API::Features do
it 'enables the feature' do
post api("/features/#{feature_name}", admin), value: 'true'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'on',
@@ -164,7 +164,7 @@ describe API::Features do
it 'enables the feature for the given Flipper group when passed feature_group=perf_team' do
post api("/features/#{feature_name}", admin), value: 'true', feature_group: 'perf_team'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'conditional',
@@ -177,7 +177,7 @@ describe API::Features do
it 'enables the feature for the given user when passed user=username' do
post api("/features/#{feature_name}", admin), value: 'true', user: user.username
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'conditional',
@@ -195,7 +195,7 @@ describe API::Features do
post api("/features/#{feature_name}", admin), value: 'false'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'off',
@@ -208,7 +208,7 @@ describe API::Features do
post api("/features/#{feature_name}", admin), value: 'false', feature_group: 'perf_team'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'off',
@@ -221,7 +221,7 @@ describe API::Features do
post api("/features/#{feature_name}", admin), value: 'false', user: user.username
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'off',
@@ -237,7 +237,7 @@ describe API::Features do
it 'updates the percentage of time if passed an integer' do
post api("/features/#{feature_name}", admin), value: '30'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq(
'name' => 'my_feature',
'state' => 'conditional',
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 114019441a3..5d8338a3fb7 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -26,7 +26,7 @@ describe API::Files do
it 'returns file attributes as json' do
get api(route(file_path), current_user), params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['file_path']).to eq(CGI.unescape(file_path))
expect(json_response['file_name']).to eq('popen.rb')
expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
@@ -38,7 +38,7 @@ describe API::Files do
get api(route(file_path), current_user), params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.content_type).to eq('application/json')
end
@@ -49,7 +49,7 @@ describe API::Files do
get api(route(file_path), current_user), params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['file_name']).to eq('commit.js.coffee')
expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n")
end
@@ -60,7 +60,7 @@ describe API::Files do
get api(url, current_user), params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
context 'when mandatory params are not given' do
@@ -122,7 +122,7 @@ describe API::Files do
get api(url, current_user), params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns raw file info for files with dots' do
@@ -131,7 +131,7 @@ describe API::Files do
get api(url, current_user), params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns file by commit sha' do
@@ -142,7 +142,7 @@ describe API::Files do
get api(route(file_path) + "/raw", current_user), params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
context 'when mandatory params are not given' do
@@ -209,7 +209,7 @@ describe API::Files do
it "creates a new file in project repo" do
post api(route(file_path), user), valid_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response["file_path"]).to eq(CGI.unescape(file_path))
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
@@ -219,7 +219,7 @@ describe API::Files do
it "returns a 400 bad request if no mandatory params given" do
post api(route("any%2Etxt"), user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 400 if editor fails to create file" do
@@ -228,7 +228,7 @@ describe API::Files do
post api(route("any%2Etxt"), user), valid_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context "when specifying an author" do
@@ -237,7 +237,7 @@ describe API::Files do
post api(route("new_file_with_author%2Etxt"), user), valid_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(response.content_type).to eq('application/json')
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(author_email)
@@ -251,7 +251,7 @@ describe API::Files do
it "creates a new file in project repo" do
post api(route("newfile%2Erb"), user), valid_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['file_path']).to eq('newfile.rb')
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
@@ -272,7 +272,7 @@ describe API::Files do
it "updates existing file in project repo" do
put api(route(file_path), user), valid_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['file_path']).to eq(CGI.unescape(file_path))
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
@@ -284,7 +284,7 @@ describe API::Files do
put api(route(file_path), user), params_with_stale_id
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('You are attempting to update a file that has changed since you started editing it.')
end
@@ -295,13 +295,13 @@ describe API::Files do
put api(route(file_path), user), params_with_correct_id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "returns a 400 bad request if no params given" do
put api(route(file_path), user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context "when specifying an author" do
@@ -310,7 +310,7 @@ describe API::Files do
put api(route(file_path), user), valid_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(author_email)
expect(last_commit.author_name).to eq(author_name)
@@ -329,13 +329,13 @@ describe API::Files do
it "deletes existing file in project repo" do
delete api(route(file_path), user), valid_params
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end
it "returns a 400 bad request if no params given" do
delete api(route(file_path), user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 400 if fails to delete file" do
@@ -343,7 +343,7 @@ describe API::Files do
delete api(route(file_path), user), valid_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context "when specifying an author" do
@@ -352,7 +352,7 @@ describe API::Files do
delete api(route(file_path), user), valid_params
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end
end
end
@@ -380,7 +380,7 @@ describe API::Files do
it "remains unchanged" do
get api(route(file_path), user), get_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['file_path']).to eq(CGI.unescape(file_path))
expect(json_response['file_name']).to eq(CGI.unescape(file_path))
expect(json_response['content']).to eq(put_params[:content])
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
index 93b9cf85c1d..a4f198eb5c9 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -15,7 +15,7 @@ describe API::GroupVariables do
it 'returns group variables' do
get api("/groups/#{group.id}/variables", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_a(Array)
end
end
@@ -24,7 +24,7 @@ describe API::GroupVariables do
it 'does not return group variables' do
get api("/groups/#{group.id}/variables", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -32,7 +32,7 @@ describe API::GroupVariables do
it 'does not return group variables' do
get api("/groups/#{group.id}/variables")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -48,7 +48,7 @@ describe API::GroupVariables do
it 'returns group variable details' do
get api("/groups/#{group.id}/variables/#{variable.key}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['value']).to eq(variable.value)
expect(json_response['protected']).to eq(variable.protected?)
end
@@ -56,7 +56,7 @@ describe API::GroupVariables do
it 'responds with 404 Not Found if requesting non-existing variable' do
get api("/groups/#{group.id}/variables/non_existing_variable", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -64,7 +64,7 @@ describe API::GroupVariables do
it 'does not return group variable details' do
get api("/groups/#{group.id}/variables/#{variable.key}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -72,7 +72,7 @@ describe API::GroupVariables do
it 'does not return group variable details' do
get api("/groups/#{group.id}/variables/#{variable.key}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -90,7 +90,7 @@ describe API::GroupVariables do
post api("/groups/#{group.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2', protected: true
end.to change {group.variables.count}.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_truthy
@@ -101,7 +101,7 @@ describe API::GroupVariables do
post api("/groups/#{group.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
end.to change {group.variables.count}.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_falsey
@@ -112,7 +112,7 @@ describe API::GroupVariables do
post api("/groups/#{group.id}/variables", user), key: variable.key, value: 'VALUE_2'
end.to change {group.variables.count}.by(0)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -120,7 +120,7 @@ describe API::GroupVariables do
it 'does not create variable' do
post api("/groups/#{group.id}/variables", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -128,7 +128,7 @@ describe API::GroupVariables do
it 'does not create variable' do
post api("/groups/#{group.id}/variables")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -149,7 +149,7 @@ describe API::GroupVariables do
updated_variable = group.variables.first
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(value_before).to eq(variable.value)
expect(updated_variable.value).to eq('VALUE_1_UP')
expect(updated_variable).to be_protected
@@ -158,7 +158,7 @@ describe API::GroupVariables do
it 'responds with 404 Not Found if requesting non-existing variable' do
put api("/groups/#{group.id}/variables/non_existing_variable", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -166,7 +166,7 @@ describe API::GroupVariables do
it 'does not update variable' do
put api("/groups/#{group.id}/variables/#{variable.key}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -174,7 +174,7 @@ describe API::GroupVariables do
it 'does not update variable' do
put api("/groups/#{group.id}/variables/#{variable.key}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -191,14 +191,14 @@ describe API::GroupVariables do
expect do
delete api("/groups/#{group.id}/variables/#{variable.key}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change {group.variables.count}.by(-1)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
delete api("/groups/#{group.id}/variables/non_existing_variable", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it_behaves_like '412 response' do
@@ -210,7 +210,7 @@ describe API::GroupVariables do
it 'does not delete variable' do
delete api("/groups/#{group.id}/variables/#{variable.key}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -218,7 +218,7 @@ describe API::GroupVariables do
it 'does not delete variable' do
delete api("/groups/#{group.id}/variables/#{variable.key}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 1671a046fdf..04a658cd6c3 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -23,7 +23,7 @@ describe API::Groups do
it "returns public groups" do
get api("/groups")
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -36,7 +36,7 @@ describe API::Groups do
it "normal user: returns an array of groups of user1" do
get api("/groups", user1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -47,7 +47,7 @@ describe API::Groups do
it "does not include statistics" do
get api("/groups", user1), statistics: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first).not_to include 'statistics'
@@ -58,7 +58,7 @@ describe API::Groups do
it "admin: returns an array of all groups" do
get api("/groups", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
@@ -67,7 +67,7 @@ describe API::Groups do
it "does not include statistics by default" do
get api("/groups", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
@@ -87,7 +87,7 @@ describe API::Groups do
get api("/groups", admin), statistics: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response)
@@ -99,7 +99,7 @@ describe API::Groups do
it "returns all groups excluding skipped groups" do
get api("/groups", admin), skip_groups: [group2.id]
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -114,7 +114,7 @@ describe API::Groups do
get api("/groups", user1), all_available: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to contain_exactly(public_group.name, group1.name)
@@ -132,7 +132,7 @@ describe API::Groups do
it "sorts by name ascending by default" do
get api("/groups", user1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to eq([group3.name, group1.name])
@@ -141,7 +141,7 @@ describe API::Groups do
it "sorts in descending order when passed" do
get api("/groups", user1), sort: "desc"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to eq([group1.name, group3.name])
@@ -150,7 +150,7 @@ describe API::Groups do
it "sorts by the order_by param" do
get api("/groups", user1), order_by: "path"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(response_groups).to eq([group1.name, group3.name])
@@ -163,7 +163,7 @@ describe API::Groups do
get api('/groups', user2), owned: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -176,12 +176,12 @@ describe API::Groups do
context 'when unauthenticated' do
it 'returns 404 for a private group' do
get api("/groups/#{group2.id}")
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 200 for a public group' do
get api("/groups/#{group1.id}")
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -192,7 +192,7 @@ describe API::Groups do
get api("/groups/#{group1.id}", user1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(group1.id)
expect(json_response['name']).to eq(group1.name)
expect(json_response['path']).to eq(group1.path)
@@ -214,13 +214,13 @@ describe API::Groups do
it "does not return a non existing group" do
get api("/groups/1328", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "does not return a group not attached to user1" do
get api("/groups/#{group2.id}", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -228,14 +228,14 @@ describe API::Groups do
it "returns any existing group" do
get api("/groups/#{group2.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(group2.name)
end
it "does not return a non existing group" do
get api("/groups/1328", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -243,20 +243,20 @@ describe API::Groups do
it 'returns any existing group' do
get api("/groups/#{group1.path}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(group1.name)
end
it 'does not return a non existing group' do
get api('/groups/unknown', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'does not return a group not attached to user1' do
get api("/groups/#{group2.path}", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -268,7 +268,7 @@ describe API::Groups do
it 'updates the group' do
put api("/groups/#{group1.id}", user1), name: new_group_name, request_access_enabled: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(new_group_name)
expect(json_response['request_access_enabled']).to eq(true)
end
@@ -276,7 +276,7 @@ describe API::Groups do
it 'returns 404 for a non existing group' do
put api('/groups/1328', user1), name: new_group_name
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -284,7 +284,7 @@ describe API::Groups do
it 'updates the group' do
put api("/groups/#{group1.id}", admin), name: new_group_name
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(new_group_name)
end
end
@@ -293,7 +293,7 @@ describe API::Groups do
it 'does not updates the group' do
put api("/groups/#{group1.id}", user2), name: new_group_name
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -301,7 +301,7 @@ describe API::Groups do
it 'returns 404 when trying to update the group' do
put api("/groups/#{group2.id}", user1), name: new_group_name
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -311,7 +311,7 @@ describe API::Groups do
it "returns the group's projects" do
get api("/groups/#{group1.id}/projects", user1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(2)
project_names = json_response.map { |proj| proj['name'] }
@@ -322,7 +322,7 @@ describe API::Groups do
it "returns the group's projects with simple representation" do
get api("/groups/#{group1.id}/projects", user1), simple: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(2)
project_names = json_response.map { |proj| proj['name'] }
@@ -335,7 +335,7 @@ describe API::Groups do
get api("/groups/#{group1.id}/projects", user1), visibility: 'public'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(1)
@@ -345,13 +345,13 @@ describe API::Groups do
it "does not return a non existing group" do
get api("/groups/1328/projects", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "does not return a group not attached to user1" do
get api("/groups/#{group2.id}/projects", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "only returns projects to which user has access" do
@@ -359,7 +359,7 @@ describe API::Groups do
get api("/groups/#{group1.id}/projects", user3)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project3.name)
@@ -370,7 +370,7 @@ describe API::Groups do
get api("/groups/#{project2.group.id}/projects", user3), owned: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project2.name)
end
@@ -380,7 +380,7 @@ describe API::Groups do
get api("/groups/#{group1.id}/projects", user1), starred: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project1.name)
end
@@ -390,7 +390,7 @@ describe API::Groups do
it "returns any existing group" do
get api("/groups/#{group2.id}/projects", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project2.name)
@@ -399,7 +399,7 @@ describe API::Groups do
it "does not return a non existing group" do
get api("/groups/1328/projects", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -407,7 +407,7 @@ describe API::Groups do
it 'returns any existing group' do
get api("/groups/#{group1.path}/projects", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
project_names = json_response.map { |proj| proj['name'] }
expect(project_names).to match_array([project1.name, project3.name])
@@ -416,13 +416,149 @@ describe API::Groups do
it 'does not return a non existing group' do
get api('/groups/unknown/projects', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'does not return a group not attached to user1' do
get api("/groups/#{group2.path}/projects", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /groups/:id/subgroups', :nested_groups do
+ let!(:subgroup1) { create(:group, parent: group1) }
+ let!(:subgroup2) { create(:group, :private, parent: group1) }
+ let!(:subgroup3) { create(:group, :private, parent: group2) }
+
+ context 'when unauthenticated' do
+ it 'returns only public subgroups' do
+ get api("/groups/#{group1.id}/subgroups")
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(subgroup1.id)
+ expect(json_response.first['parent_id']).to eq(group1.id)
+ end
+
+ it 'returns 404 for a private group' do
+ get api("/groups/#{group2.id}/subgroups")
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when authenticated as user' do
+ context 'when user is not member of a public group' do
+ it 'returns no subgroups for the public group' do
+ get api("/groups/#{group1.id}/subgroups", user2)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ context 'when using all_available in request' do
+ it 'returns public subgroups' do
+ get api("/groups/#{group1.id}/subgroups", user2), all_available: true
+
+ 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[0]['id']).to eq(subgroup1.id)
+ expect(json_response[0]['parent_id']).to eq(group1.id)
+ end
+ end
+ end
+
+ context 'when user is not member of a private group' do
+ it 'returns 404 for the private group' do
+ get api("/groups/#{group2.id}/subgroups", user1)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when user is member of public group' do
+ before do
+ group1.add_guest(user2)
+ end
+
+ it 'returns private subgroups' do
+ get api("/groups/#{group1.id}/subgroups", user2)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ private_subgroups = json_response.select { |group| group['visibility'] == 'private' }
+ expect(private_subgroups.length).to eq(1)
+ expect(private_subgroups.first['id']).to eq(subgroup2.id)
+ expect(private_subgroups.first['parent_id']).to eq(group1.id)
+ end
+
+ context 'when using statistics in request' do
+ it 'does not include statistics' do
+ get api("/groups/#{group1.id}/subgroups", user2), statistics: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include 'statistics'
+ end
+ end
+ end
+
+ context 'when user is member of private group' do
+ before do
+ group2.add_guest(user1)
+ end
+
+ it 'returns subgroups' do
+ get api("/groups/#{group2.id}/subgroups", user1)
+
+ 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(subgroup3.id)
+ expect(json_response.first['parent_id']).to eq(group2.id)
+ end
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'returns private subgroups of a public group' do
+ get api("/groups/#{group1.id}/subgroups", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns subgroups of a private group' do
+ get api("/groups/#{group2.id}/subgroups", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+
+ it 'does not include statistics by default' do
+ get api("/groups/#{group1.id}/subgroups", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('statistics')
+ end
+
+ it 'includes statistics if requested' do
+ get api("/groups/#{group1.id}/subgroups", admin), statistics: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).to include('statistics')
end
end
end
@@ -432,7 +568,7 @@ describe API::Groups do
it "does not create group" do
post api("/groups", user1), attributes_for(:group)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
context 'as owner', :nested_groups do
@@ -443,7 +579,7 @@ describe API::Groups do
it 'can create subgroups' do
post api("/groups", user1), parent_id: group2.id, name: 'foo', path: 'foo'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
end
@@ -455,7 +591,7 @@ describe API::Groups do
it 'cannot create subgroups' do
post api("/groups", user1), parent_id: group2.id, name: 'foo', path: 'foo'
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -466,7 +602,7 @@ describe API::Groups do
post api("/groups", user3), group
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response["name"]).to eq(group[:name])
expect(json_response["path"]).to eq(group[:path])
@@ -481,7 +617,7 @@ describe API::Groups do
post api("/groups", user3), group
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response["full_path"]).to eq("#{parent.path}/#{group[:path]}")
expect(json_response["parent_id"]).to eq(parent.id)
@@ -490,20 +626,20 @@ describe API::Groups do
it "does not create group, duplicate" do
post api("/groups", user3), { name: 'Duplicate Test', path: group2.path }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(response.message).to eq("Bad Request")
end
it "returns 400 bad request error if name not given" do
post api("/groups", user3), { path: group2.path }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns 400 bad request error if path not given" do
post api("/groups", user3), { name: 'test' }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
end
@@ -513,7 +649,7 @@ describe API::Groups do
it "removes group" do
delete api("/groups/#{group1.id}", user1)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end
it_behaves_like '412 response' do
@@ -526,19 +662,19 @@ describe API::Groups do
delete api("/groups/#{group1.id}", user3)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "does not remove a non existing group" do
delete api("/groups/1328", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "does not remove a group not attached to user1" do
delete api("/groups/#{group2.id}", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -546,13 +682,13 @@ describe API::Groups do
it "removes any existing group" do
delete api("/groups/#{group2.id}", admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end
it "does not remove a non existing group" do
delete api("/groups/1328", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -570,7 +706,7 @@ describe API::Groups do
it "does not transfer project to group" do
post api("/groups/#{group1.id}/projects/#{project.id}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -578,7 +714,7 @@ describe API::Groups do
it "transfers project to group" do
post api("/groups/#{group1.id}/projects/#{project.id}", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
context 'when using project path in URL' do
@@ -586,7 +722,7 @@ describe API::Groups do
it "transfers project to group" do
post api("/groups/#{group1.id}/projects/#{project_path}", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
end
@@ -594,7 +730,7 @@ describe API::Groups do
it "does not transfer project to group" do
post api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -604,7 +740,7 @@ describe API::Groups do
it "transfers project to group" do
post api("/groups/#{group1.path}/projects/#{project_path}", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
end
@@ -612,10 +748,20 @@ describe API::Groups do
it "does not transfer project to group" do
post api("/groups/noexist/projects/#{project_path}", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
end
end
+
+ it_behaves_like 'custom attributes endpoints', 'groups' do
+ let(:attributable) { group1 }
+ let(:other_attributable) { group2 }
+ let(:user) { user1 }
+
+ before do
+ group2.add_owner(user1)
+ end
+ end
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 862920ad7c3..0462f494e15 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -11,7 +11,6 @@ describe API::Helpers do
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
- let(:params) { {} }
let(:csrf_token) { SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH) }
let(:env) do
{
@@ -19,60 +18,35 @@ describe API::Helpers do
'rack.session' => {
_csrf_token: csrf_token
},
- 'REQUEST_METHOD' => 'GET'
+ 'REQUEST_METHOD' => 'GET',
+ 'CONTENT_TYPE' => 'text/plain;charset=utf-8'
}
end
let(:header) { }
+ let(:request) { Grape::Request.new(env)}
+ let(:params) { request.params }
before do
allow_any_instance_of(self.class).to receive(:options).and_return({})
end
- def set_env(user_or_token, identifier)
- clear_env
- clear_param
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
- env[API::Helpers::SUDO_HEADER] = identifier.to_s
- end
-
- def set_param(user_or_token, identifier)
- clear_env
- clear_param
- params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
- params[API::Helpers::SUDO_PARAM] = identifier.to_s
- end
-
- def clear_env
- env.delete(API::APIGuard::PRIVATE_TOKEN_HEADER)
- env.delete(API::Helpers::SUDO_HEADER)
- end
-
- def clear_param
- params.delete(API::APIGuard::PRIVATE_TOKEN_PARAM)
- params.delete(API::Helpers::SUDO_PARAM)
- end
-
def warden_authenticate_returns(value)
warden = double("warden", authenticate: value)
env['warden'] = warden
end
- def doorkeeper_guard_returns(value)
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { value }
- end
-
def error!(message, status, header)
raise Exception.new("#{status} - #{message}")
end
+ def set_param(key, value)
+ request.update_param(key, value)
+ end
+
describe ".current_user" do
subject { current_user }
describe "Warden authentication", :allow_forgery_protection do
- before do
- doorkeeper_guard_returns false
- end
-
context "with invalid credentials" do
context "GET request" do
before do
@@ -160,300 +134,53 @@ describe API::Helpers do
end
end
- describe "when authenticating using a user's private token" do
- it "returns a 401 response for an invalid token" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
-
- expect { current_user }.to raise_error /401/
- end
-
- it "returns a 401 response for a user without access" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
- allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
-
- expect { current_user }.to raise_error /401/
- end
-
- it 'returns a 401 response for a user who is blocked' do
- user.block!
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
-
- expect { current_user }.to raise_error /401/
- end
-
- it "leaves user as is when sudo not specified" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
-
- expect(current_user).to eq(user)
-
- clear_env
-
- params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token
-
- expect(current_user).to eq(user)
- end
- end
-
describe "when authenticating using a user's personal access tokens" do
let(:personal_access_token) { create(:personal_access_token, user: user) }
- before do
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
- end
-
it "returns a 401 response for an invalid token" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = 'invalid token'
expect { current_user }.to raise_error /401/
end
- it "returns a 401 response for a user without access" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ it "returns a 403 response for a user without access" do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
- expect { current_user }.to raise_error /401/
+ expect { current_user }.to raise_error /403/
end
- it 'returns a 401 response for a user who is blocked' do
+ it 'returns a 403 response for a user who is blocked' do
user.block!
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect { current_user }.to raise_error /401/
+ expect { current_user }.to raise_error /403/
end
- it "returns a 401 response for a token without the appropriate scope" do
- personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
-
- expect { current_user }.to raise_error /401/
+ it "sets current_user" do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ expect(current_user).to eq(user)
end
- it "leaves user as is when sudo not specified" do
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect(current_user).to eq(user)
- clear_env
- params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token
+ it "does not allow tokens without the appropriate scope" do
+ personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect(current_user).to eq(user)
+ expect { current_user }.to raise_error Gitlab::Auth::InsufficientScopeError
end
it 'does not allow revoked tokens' do
personal_access_token.revoke!
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect { current_user }.to raise_error /401/
+ expect { current_user }.to raise_error Gitlab::Auth::RevokedError
end
it 'does not allow expired tokens' do
personal_access_token.update_attributes!(expires_at: 1.day.ago)
- env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
-
- expect { current_user }.to raise_error /401/
- end
- end
-
- context 'sudo usage' do
- context 'with admin' do
- context 'with header' do
- context 'with id' do
- it 'changes current_user to sudo' do
- set_env(admin, user.id)
-
- expect(current_user).to eq(user)
- end
-
- it 'memoize the current_user: sudo permissions are not run against the sudoed user' do
- set_env(admin, user.id)
-
- expect(current_user).to eq(user)
- expect(current_user).to eq(user)
- end
-
- it 'handles sudo to oneself' do
- set_env(admin, admin.id)
-
- expect(current_user).to eq(admin)
- end
-
- it 'throws an error when user cannot be found' do
- id = user.id + admin.id
- expect(user.id).not_to eq(id)
- expect(admin.id).not_to eq(id)
-
- set_env(admin, id)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
-
- context 'with username' do
- it 'changes current_user to sudo' do
- set_env(admin, user.username)
-
- expect(current_user).to eq(user)
- end
-
- it 'handles sudo to oneself' do
- set_env(admin, admin.username)
-
- expect(current_user).to eq(admin)
- end
-
- it "throws an error when the user cannot be found for a given username" do
- username = "#{user.username}#{admin.username}"
- expect(user.username).not_to eq(username)
- expect(admin.username).not_to eq(username)
-
- set_env(admin, username)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
- end
-
- context 'with param' do
- context 'with id' do
- it 'changes current_user to sudo' do
- set_param(admin, user.id)
-
- expect(current_user).to eq(user)
- end
-
- it 'handles sudo to oneself' do
- set_param(admin, admin.id)
-
- expect(current_user).to eq(admin)
- end
-
- it 'handles sudo to oneself using string' do
- set_env(admin, user.id.to_s)
-
- expect(current_user).to eq(user)
- end
-
- it 'throws an error when user cannot be found' do
- id = user.id + admin.id
- expect(user.id).not_to eq(id)
- expect(admin.id).not_to eq(id)
-
- set_param(admin, id)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
-
- context 'with username' do
- it 'changes current_user to sudo' do
- set_param(admin, user.username)
-
- expect(current_user).to eq(user)
- end
-
- it 'handles sudo to oneself' do
- set_param(admin, admin.username)
-
- expect(current_user).to eq(admin)
- end
-
- it "throws an error when the user cannot be found for a given username" do
- username = "#{user.username}#{admin.username}"
- expect(user.username).not_to eq(username)
- expect(admin.username).not_to eq(username)
-
- set_param(admin, username)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
- end
-
- context 'when user is blocked' do
- before do
- user.block!
- end
-
- it 'changes current_user to sudo' do
- set_env(admin, user.id)
-
- expect(current_user).to eq(user)
- end
- end
- end
-
- context 'with regular user' do
- context 'with env' do
- it 'changes current_user to sudo when admin and user id' do
- set_env(user, admin.id)
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- expect { current_user }.to raise_error(Exception)
- end
-
- it 'changes current_user to sudo when admin and user username' do
- set_env(user, admin.username)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
-
- context 'with params' do
- it 'changes current_user to sudo when admin and user id' do
- set_param(user, admin.id)
-
- expect { current_user }.to raise_error(Exception)
- end
-
- it 'changes current_user to sudo when admin and user username' do
- set_param(user, admin.username)
-
- expect { current_user }.to raise_error(Exception)
- end
- end
- end
- end
- end
-
- describe '.sudo?' do
- context 'when no sudo env or param is passed' do
- before do
- doorkeeper_guard_returns(nil)
- end
-
- it 'returns false' do
- expect(sudo?).to be_falsy
- end
- end
-
- context 'when sudo env or param is passed', 'user is not an admin' do
- before do
- set_env(user, '123')
- end
-
- it 'returns an 403 Forbidden' do
- expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}'
- end
- end
-
- context 'when sudo env or param is passed', 'user is admin' do
- context 'personal access token is used' do
- before do
- personal_access_token = create(:personal_access_token, user: admin)
- set_env(personal_access_token.token, user.id)
- end
-
- it 'returns an 403 Forbidden' do
- expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Private token must be specified in order to use sudo"}'
- end
- end
-
- context 'private access token is used' do
- before do
- set_env(admin.private_token, user.id)
- end
-
- it 'returns true' do
- expect(sudo?).to be_truthy
- end
+ expect { current_user }.to raise_error Gitlab::Auth::ExpiredError
end
end
end
@@ -582,4 +309,147 @@ describe API::Helpers do
end
end
end
+
+ context 'sudo' do
+ shared_examples 'successful sudo' do
+ it 'sets current_user' do
+ expect(current_user).to eq(user)
+ end
+
+ it 'sets sudo?' do
+ expect(sudo?).to be_truthy
+ end
+ end
+
+ shared_examples 'sudo' do
+ context 'when admin' do
+ before do
+ token.user = admin
+ token.save!
+ end
+
+ context 'when token has sudo scope' do
+ before do
+ token.scopes = %w[sudo]
+ token.save!
+ end
+
+ context 'when user exists' do
+ context 'when using header' do
+ context 'when providing username' do
+ before do
+ env[API::Helpers::SUDO_HEADER] = user.username
+ end
+
+ it_behaves_like 'successful sudo'
+ end
+
+ context 'when providing user ID' do
+ before do
+ env[API::Helpers::SUDO_HEADER] = user.id.to_s
+ end
+
+ it_behaves_like 'successful sudo'
+ end
+ end
+
+ context 'when using param' do
+ context 'when providing username' do
+ before do
+ set_param(API::Helpers::SUDO_PARAM, user.username)
+ end
+
+ it_behaves_like 'successful sudo'
+ end
+
+ context 'when providing user ID' do
+ before do
+ set_param(API::Helpers::SUDO_PARAM, user.id.to_s)
+ end
+
+ it_behaves_like 'successful sudo'
+ end
+ end
+ end
+
+ context 'when user does not exist' do
+ before do
+ set_param(API::Helpers::SUDO_PARAM, 'nonexistent')
+ end
+
+ it 'raises an error' do
+ expect { current_user }.to raise_error /User with ID or username 'nonexistent' Not Found/
+ end
+ end
+ end
+
+ context 'when token does not have sudo scope' do
+ before do
+ token.scopes = %w[api]
+ token.save!
+
+ set_param(API::Helpers::SUDO_PARAM, user.id.to_s)
+ end
+
+ it 'raises an error' do
+ expect { current_user }.to raise_error Gitlab::Auth::InsufficientScopeError
+ end
+ end
+ end
+
+ context 'when not admin' do
+ before do
+ token.user = user
+ token.save!
+
+ set_param(API::Helpers::SUDO_PARAM, user.id.to_s)
+ end
+
+ it 'raises an error' do
+ expect { current_user }.to raise_error /Must be admin to use sudo/
+ end
+ end
+ end
+
+ context 'using an OAuth token' do
+ let(:token) { create(:oauth_access_token) }
+
+ before do
+ env['HTTP_AUTHORIZATION'] = "Bearer #{token.token}"
+ end
+
+ it_behaves_like 'sudo'
+ end
+
+ context 'using a personal access token' do
+ let(:token) { create(:personal_access_token) }
+
+ context 'passed as param' do
+ before do
+ set_param(Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_PARAM, token.token)
+ end
+
+ it_behaves_like 'sudo'
+ end
+
+ context 'passed as header' do
+ before do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = token.token
+ end
+
+ it_behaves_like 'sudo'
+ end
+ end
+
+ context 'using warden authentication' do
+ before do
+ warden_authenticate_returns admin
+ env[API::Helpers::SUDO_HEADER] = user.username
+ end
+
+ it 'raises an error' do
+ expect { current_user }.to raise_error /Must be authenticated using an OAuth or Personal Access Token to use sudo/
+ end
+ end
+ end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 1274e66bb4c..67e1539cbc3 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -14,7 +14,7 @@ describe API::Internal do
get api("/internal/check"), secret_token: secret_token
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['api_version']).to eq(API::API.version)
expect(json_response['redis']).to be(true)
end
@@ -35,7 +35,7 @@ describe API::Internal do
it 'returns one broadcast message' do
get api('/internal/broadcast_message'), secret_token: secret_token
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['message']).to eq(broadcast_message.message)
end
end
@@ -44,7 +44,7 @@ describe API::Internal do
it 'returns nothing' do
get api('/internal/broadcast_message'), secret_token: secret_token
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_empty
end
end
@@ -55,7 +55,7 @@ describe API::Internal do
get api('/internal/broadcast_message'), secret_token: secret_token
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_empty
end
end
@@ -68,7 +68,7 @@ describe API::Internal do
it 'returns active broadcast message(s)' do
get api('/internal/broadcast_messages'), secret_token: secret_token
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response[0]['message']).to eq(broadcast_message.message)
end
end
@@ -77,7 +77,7 @@ describe API::Internal do
it 'returns nothing' do
get api('/internal/broadcast_messages'), secret_token: secret_token
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_empty
end
end
@@ -154,7 +154,7 @@ describe API::Internal do
it 'returns the correct information about the key' do
lfs_auth(key.id, project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['username']).to eq(user.username)
expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token)
@@ -164,7 +164,7 @@ describe API::Internal do
it 'returns a 404 when the wrong key is provided' do
lfs_auth(nil, project)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -174,7 +174,7 @@ describe API::Internal do
it 'returns the correct information about the key' do
lfs_auth(key.id, project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['username']).to eq("lfs+deploy-key-#{key.id}")
expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token)
expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
@@ -186,7 +186,7 @@ describe API::Internal do
it do
get(api("/internal/discover"), key_id: key.id, secret_token: secret_token)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(user.name)
end
@@ -203,18 +203,44 @@ describe API::Internal do
end
context 'with env passed as a JSON' do
- it 'sets env in RequestStore' do
- expect(Gitlab::Git::Env).to receive(:set).with({
- 'GIT_OBJECT_DIRECTORY' => 'foo',
- 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
- })
-
- push(key, project.wiki, env: {
- GIT_OBJECT_DIRECTORY: 'foo',
- GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar'
- }.to_json)
-
- expect(response).to have_http_status(200)
+ context 'when relative path envs are not set' do
+ it 'sets env in RequestStore' do
+ expect(Gitlab::Git::Env).to receive(:set).with({
+ 'GIT_OBJECT_DIRECTORY' => 'foo',
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
+ })
+
+ push(key, project.wiki, env: {
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar'
+ }.to_json)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'when relative path envs are set' do
+ it 'sets env in RequestStore' do
+ obj_dir_relative = './objects'
+ alt_obj_dirs_relative = ['./alt-objects-1', './alt-objects-2']
+ repo_path = project.wiki.repository.path_to_repo
+
+ expect(Gitlab::Git::Env).to receive(:set).with({
+ 'GIT_OBJECT_DIRECTORY' => File.join(repo_path, obj_dir_relative),
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => alt_obj_dirs_relative.map { |d| File.join(repo_path, d) },
+ 'GIT_OBJECT_DIRECTORY_RELATIVE' => obj_dir_relative,
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => alt_obj_dirs_relative
+ })
+
+ push(key, project.wiki, env: {
+ GIT_OBJECT_DIRECTORY: 'foo',
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar',
+ GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative,
+ GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative
+ }.to_json)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
end
end
@@ -222,7 +248,7 @@ describe API::Internal do
it 'responds with success' do
push(key, project.wiki)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
@@ -234,7 +260,7 @@ describe API::Internal do
it 'responds with success' do
pull(key, project.wiki)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
@@ -243,12 +269,11 @@ describe API::Internal do
end
context "git pull" do
- context "gitaly disabled" do
+ context "gitaly disabled", :disable_gitaly do
it "has the correct payload" do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:ssh_upload_pack).and_return(false)
pull(key, project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
@@ -259,10 +284,9 @@ describe API::Internal do
context "gitaly enabled" do
it "has the correct payload" do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:ssh_upload_pack).and_return(true)
pull(key, project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
@@ -278,12 +302,11 @@ describe API::Internal do
end
context "git push" do
- context "gitaly disabled" do
+ context "gitaly disabled", :disable_gitaly do
it "has the correct payload" do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:ssh_receive_pack).and_return(false)
push(key, project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
@@ -294,10 +317,9 @@ describe API::Internal do
context "gitaly enabled" do
it "has the correct payload" do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:ssh_receive_pack).and_return(true)
push(key, project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
@@ -315,7 +337,7 @@ describe API::Internal do
it do
pull(key, project_with_repo_path('/' + project.full_path))
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
@@ -326,7 +348,7 @@ describe API::Internal do
it do
pull(key, project_with_repo_path(project.full_path))
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
@@ -344,7 +366,7 @@ describe API::Internal do
it do
pull(key, project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_falsey
expect(user).not_to have_an_activity_record
end
@@ -354,7 +376,7 @@ describe API::Internal do
it do
push(key, project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_falsey
expect(user).not_to have_an_activity_record
end
@@ -372,7 +394,7 @@ describe API::Internal do
it do
pull(key, personal_project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_falsey
expect(user).not_to have_an_activity_record
end
@@ -382,7 +404,7 @@ describe API::Internal do
it do
push(key, personal_project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_falsey
expect(user).not_to have_an_activity_record
end
@@ -399,7 +421,7 @@ describe API::Internal do
it do
pull(key, project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
end
end
@@ -408,7 +430,7 @@ describe API::Internal do
it do
push(key, project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_falsey
end
end
@@ -425,7 +447,7 @@ describe API::Internal do
it do
archive(key, project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
end
end
@@ -434,7 +456,7 @@ describe API::Internal do
it do
archive(key, project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_falsey
end
end
@@ -444,7 +466,7 @@ describe API::Internal do
it do
pull(key, project_with_repo_path('gitlab/notexist'))
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_falsey
end
end
@@ -453,7 +475,7 @@ describe API::Internal do
it do
pull(OpenStruct.new(id: 0), project)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_falsey
end
end
@@ -535,7 +557,7 @@ describe API::Internal do
it 'rejects the push' do
push_with_path(key, old_path_to_repo)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to be_falsey
expect(json_response['message']).to eq(project_moved_message)
end
@@ -543,7 +565,7 @@ describe API::Internal do
it 'rejects the SSH pull' do
pull_with_path(key, old_path_to_repo)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to be_falsey
expect(json_response['message']).to eq(project_moved_message)
end
@@ -614,7 +636,7 @@ describe API::Internal do
#
# post api("/internal/notify_post_receive"), valid_params
#
- # expect(response).to have_http_status(200)
+ # expect(response).to have_gitlab_http_status(200)
# end
#
# it "calls the Gitaly client with the wiki's repository if it's a wiki" do
@@ -626,7 +648,7 @@ describe API::Internal do
#
# post api("/internal/notify_post_receive"), valid_wiki_params
#
- # expect(response).to have_http_status(200)
+ # expect(response).to have_gitlab_http_status(200)
# end
#
# it "returns 500 if the gitaly call fails" do
@@ -635,7 +657,7 @@ describe API::Internal do
#
# post api("/internal/notify_post_receive"), valid_params
#
- # expect(response).to have_http_status(500)
+ # expect(response).to have_gitlab_http_status(500)
# end
#
# context 'with a gl_repository parameter' do
@@ -656,7 +678,7 @@ describe API::Internal do
#
# post api("/internal/notify_post_receive"), valid_params
#
- # expect(response).to have_http_status(200)
+ # expect(response).to have_gitlab_http_status(200)
# end
#
# it "calls the Gitaly client with the wiki's repository if it's a wiki" do
@@ -668,7 +690,7 @@ describe API::Internal do
#
# post api("/internal/notify_post_receive"), valid_wiki_params
#
- # expect(response).to have_http_status(200)
+ # expect(response).to have_gitlab_http_status(200)
# end
# end
# end
@@ -734,7 +756,7 @@ describe API::Internal do
it 'returns one broadcast message' do
post api("/internal/post_receive"), valid_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['broadcast_message']).to eq(broadcast_message.message)
end
end
@@ -743,7 +765,7 @@ describe API::Internal do
it 'returns empty string' do
post api("/internal/post_receive"), valid_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['broadcast_message']).to eq(nil)
end
end
@@ -754,7 +776,7 @@ describe API::Internal do
post api("/internal/post_receive"), valid_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['broadcast_message']).to eq(nil)
end
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 972e57bc373..99525cd0a6a 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -67,7 +67,7 @@ describe API::Issues, :mailer do
it "returns authentication error" do
get api("/issues")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
context "when authenticated" do
@@ -297,7 +297,7 @@ describe API::Issues, :mailer do
it 'matches V4 response schema' do
get api('/issues', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/issues')
end
end
@@ -474,7 +474,7 @@ describe API::Issues, :mailer do
it 'returns an array of issues with no milestone' do
get api("#{base_url}?milestone=#{no_milestone_title}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(group_confidential_issue.id)
@@ -535,7 +535,7 @@ describe API::Issues, :mailer do
it 'returns 404 when project does not exist' do
get api('/projects/1000/issues', non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 on private projects for other users" do
@@ -544,7 +544,7 @@ describe API::Issues, :mailer do
get api("/projects/#{private_project.id}/issues", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns no issues when user has access to project but not issues' do
@@ -732,7 +732,7 @@ describe API::Issues, :mailer do
it 'exposes known attributes' do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(issue.id)
expect(json_response['iid']).to eq(issue.iid)
expect(json_response['project_id']).to eq(issue.project.id)
@@ -753,7 +753,7 @@ describe API::Issues, :mailer do
it "exposes the 'closed_at' attribute" do
get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['closed_at']).to be_present
end
@@ -773,39 +773,39 @@ describe API::Issues, :mailer do
it "returns a project issue by internal id" do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(issue.title)
expect(json_response['iid']).to eq(issue.iid)
end
it "returns 404 if issue id not found" do
get api("/projects/#{project.id}/issues/54321", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 if the issue ID is used" do
get api("/projects/#{project.id}/issues/#{issue.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context 'confidential issues' do
it "returns 404 for non project members" do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 for project members with guest role" do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns confidential issue for project members" do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
@@ -813,7 +813,7 @@ describe API::Issues, :mailer do
it "returns confidential issue for author" do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
@@ -821,7 +821,7 @@ describe API::Issues, :mailer do
it "returns confidential issue for assignee" do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
@@ -829,7 +829,7 @@ describe API::Issues, :mailer do
it "returns confidential issue for admin" do
get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
@@ -842,7 +842,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', assignee_id: user2.id
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
@@ -854,7 +854,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', assignee_ids: [user2.id, guest.id]
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['assignees'].count).to eq(1)
end
@@ -865,7 +865,7 @@ describe API::Issues, :mailer do
title: 'new issue', labels: 'label, label2', weight: 3,
assignee_ids: [user2.id]
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(%w(label label2))
@@ -878,7 +878,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: true
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_truthy
end
@@ -887,7 +887,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: 'y'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_truthy
end
@@ -896,7 +896,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: false
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_falsy
end
@@ -905,7 +905,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: 'foo'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('confidential is invalid')
end
@@ -923,7 +923,7 @@ describe API::Issues, :mailer do
it "returns a 400 bad request if title not given" do
post api("/projects/#{project.id}/issues", user), labels: 'label, label2'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'allows special label names' do
@@ -941,7 +941,7 @@ describe API::Issues, :mailer do
it 'returns 400 if title is too long' do
post api("/projects/#{project.id}/issues", user),
title: 'g' * 256
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['title']).to eq([
'is too long (maximum is 255 characters)'
])
@@ -985,7 +985,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', due_date: due_date
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['due_date']).to eq(due_date)
@@ -998,7 +998,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', created_at: creation_time
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
end
@@ -1028,7 +1028,7 @@ describe API::Issues, :mailer do
it "does not create a new project issue" do
expect { post api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
spam_logs = SpamLog.all
@@ -1044,7 +1044,7 @@ describe API::Issues, :mailer do
it "updates a project issue" do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
title: 'updated title'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
@@ -1052,13 +1052,13 @@ describe API::Issues, :mailer do
it "returns 404 error if issue iid not found" do
put api("/projects/#{project.id}/issues/44444", user),
title: 'updated title'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 error if issue id is used instead of the iid" do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'allows special label names' do
@@ -1078,33 +1078,33 @@ describe API::Issues, :mailer do
it "returns 403 for non project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member),
title: 'updated title'
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "returns 403 for project members with guest role" do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest),
title: 'updated title'
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "updates a confidential issue for project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
title: 'updated title'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it "updates a confidential issue for author" do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author),
title: 'updated title'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
it "updates a confidential issue for admin" do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin),
title: 'updated title'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
@@ -1112,7 +1112,7 @@ describe API::Issues, :mailer do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
confidential: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['confidential']).to be_truthy
end
@@ -1120,7 +1120,7 @@ describe API::Issues, :mailer do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
confidential: false
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['confidential']).to be_falsy
end
@@ -1128,7 +1128,7 @@ describe API::Issues, :mailer do
put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
confidential: 'foo'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('confidential is invalid')
end
end
@@ -1149,7 +1149,7 @@ describe API::Issues, :mailer do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
spam_logs = SpamLog.all
@@ -1167,7 +1167,7 @@ describe API::Issues, :mailer do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
assignee_id: 0
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['assignee']).to be_nil
end
@@ -1176,7 +1176,7 @@ describe API::Issues, :mailer do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
assignee_id: user2.id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['assignee']['name']).to eq(user2.name)
end
@@ -1186,7 +1186,7 @@ describe API::Issues, :mailer do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
assignee_ids: [0]
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['assignees']).to be_empty
end
@@ -1195,7 +1195,7 @@ describe API::Issues, :mailer do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
assignee_ids: [user2.id]
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
@@ -1205,7 +1205,7 @@ describe API::Issues, :mailer do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
assignee_ids: [user2.id, guest.id]
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['assignees'].size).to eq(1)
end
@@ -1219,7 +1219,7 @@ describe API::Issues, :mailer do
it 'does not update labels if not present' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
title: 'updated title'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([label.title])
end
@@ -1238,14 +1238,14 @@ describe API::Issues, :mailer do
it 'removes all labels' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: ''
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([])
end
it 'updates labels' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
labels: 'foo,bar'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'foo'
expect(json_response['labels']).to include 'bar'
end
@@ -1267,7 +1267,7 @@ describe API::Issues, :mailer do
it 'returns 400 if title is too long' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
title: 'g' * 256
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['title']).to eq([
'is too long (maximum is 255 characters)'
])
@@ -1278,7 +1278,7 @@ describe API::Issues, :mailer do
it "updates a project issue" do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
labels: 'label2', state_event: "close"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'label2'
expect(json_response['state']).to eq "closed"
@@ -1287,7 +1287,7 @@ describe API::Issues, :mailer do
it 'reopens a project isssue' do
put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), state_event: 'reopen'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['state']).to eq 'opened'
end
@@ -1297,7 +1297,7 @@ describe API::Issues, :mailer do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
labels: 'label3', state_event: 'close', updated_at: update_time
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'label3'
expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
end
@@ -1310,7 +1310,7 @@ describe API::Issues, :mailer do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), due_date: due_date
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['due_date']).to eq(due_date)
end
end
@@ -1318,12 +1318,12 @@ describe API::Issues, :mailer do
describe "DELETE /projects/:id/issues/:issue_iid" do
it "rejects a non member from deleting an issue" do
delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "rejects a developer from deleting an issue" do
delete api("/projects/#{project.id}/issues/#{issue.iid}", author)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
context "when the user is project owner" do
@@ -1333,7 +1333,7 @@ describe API::Issues, :mailer do
it "deletes the issue if an admin requests it" do
delete api("/projects/#{project.id}/issues/#{issue.iid}", owner)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end
it_behaves_like '412 response' do
@@ -1345,14 +1345,14 @@ describe API::Issues, :mailer do
it 'returns 404 when trying to move an issue' do
delete api("/projects/#{project.id}/issues/123", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
it 'returns 404 when using the issue ID instead of IID' do
delete api("/projects/#{project.id}/issues/#{issue.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1364,7 +1364,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: target_project.id
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['project_id']).to eq(target_project.id)
end
@@ -1373,7 +1373,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: project.id
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
end
end
@@ -1383,7 +1383,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: target_project2.id
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
end
end
@@ -1392,7 +1392,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin),
to_project_id: target_project2.id
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['project_id']).to eq(target_project2.id)
end
@@ -1401,7 +1401,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: target_project.id
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Issue Not Found')
end
end
@@ -1411,7 +1411,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues/123/move", user),
to_project_id: target_project.id
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Issue Not Found')
end
end
@@ -1421,7 +1421,7 @@ describe API::Issues, :mailer do
post api("/projects/123/issues/#{issue.iid}/move", user),
to_project_id: target_project.id
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
end
@@ -1431,7 +1431,7 @@ describe API::Issues, :mailer do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: 123
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -1440,32 +1440,32 @@ describe API::Issues, :mailer do
it 'subscribes to an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
it 'returns 404 if the issue is not found' do
post api("/projects/#{project.id}/issues/123/subscribe", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the issue ID is used instead of the iid' do
post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the issue is confidential' do
post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1473,32 +1473,32 @@ describe API::Issues, :mailer do
it 'unsubscribes from an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
it 'returns 404 if the issue is not found' do
post api("/projects/#{project.id}/issues/123/unsubscribe", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if using the issue ID instead of iid' do
post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the issue is confidential' do
post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1539,7 +1539,7 @@ describe API::Issues, :mailer do
it "returns 404 when issue doesn't exists" do
get api("/projects/#{project.id}/issues/9999/closed_by", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1549,7 +1549,7 @@ describe API::Issues, :mailer do
it 'exposes known attributes' do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
expect(json_response['ip_address']).to eq(user_agent_detail.ip_address)
expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
@@ -1558,12 +1558,12 @@ describe API::Issues, :mailer do
it "returns unautorized for non-admin users" do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
def expect_paginated_array_response(size: nil)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(size) if size
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 2d7cc1a1798..2a83213e87a 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -31,7 +31,7 @@ describe API::Jobs do
context 'authorized user' do
it 'returns project jobs' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
@@ -55,7 +55,7 @@ describe API::Jobs do
let(:query) { { 'scope' => 'pending' } }
it do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
end
end
@@ -64,7 +64,7 @@ describe API::Jobs do
let(:query) { { scope: %w(pending running) } }
it do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
end
end
@@ -72,7 +72,7 @@ describe API::Jobs do
context 'respond 400 when scope contains invalid state' do
let(:query) { { scope: %w(unknown running) } }
- it { expect(response).to have_http_status(400) }
+ it { expect(response).to have_gitlab_http_status(400) }
end
end
@@ -80,7 +80,7 @@ describe API::Jobs do
let(:api_user) { nil }
it 'does not return project jobs' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -94,7 +94,7 @@ describe API::Jobs do
context 'authorized user' do
it 'returns pipeline jobs' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
@@ -118,7 +118,7 @@ describe API::Jobs do
let(:query) { { 'scope' => 'pending' } }
it do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
end
end
@@ -127,7 +127,7 @@ describe API::Jobs do
let(:query) { { scope: %w(pending running) } }
it do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
end
end
@@ -135,7 +135,7 @@ describe API::Jobs do
context 'respond 400 when scope contains invalid state' do
let(:query) { { scope: %w(unknown running) } }
- it { expect(response).to have_http_status(400) }
+ it { expect(response).to have_gitlab_http_status(400) }
end
context 'jobs in different pipelines' do
@@ -152,7 +152,7 @@ describe API::Jobs do
let(:api_user) { nil }
it 'does not return jobs' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -164,8 +164,18 @@ describe API::Jobs do
context 'authorized user' do
it 'returns specific job data' do
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq('test')
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq(job.id)
+ expect(json_response['status']).to eq(job.status)
+ expect(json_response['stage']).to eq(job.stage)
+ expect(json_response['name']).to eq(job.name)
+ expect(json_response['ref']).to eq(job.ref)
+ expect(json_response['tag']).to eq(job.tag)
+ expect(json_response['coverage']).to eq(job.coverage)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(job.created_at)
+ expect(Time.parse(json_response['started_at'])).to be_like_time(job.started_at)
+ expect(Time.parse(json_response['finished_at'])).to be_like_time(job.finished_at)
+ expect(json_response['duration']).to eq(job.duration)
end
it 'returns pipeline data' do
@@ -183,7 +193,7 @@ describe API::Jobs do
let(:api_user) { nil }
it 'does not return specific job data' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -207,7 +217,7 @@ describe API::Jobs do
get_artifact_file(artifact)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -219,7 +229,7 @@ describe API::Jobs do
get_artifact_file(artifact)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -231,7 +241,7 @@ describe API::Jobs do
get_artifact_file(artifact)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -244,7 +254,7 @@ describe API::Jobs do
get_artifact_file(artifact)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.headers)
.to include('Content-Type' => 'application/json',
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
@@ -256,7 +266,7 @@ describe API::Jobs do
it 'does not return job artifact file' do
get_artifact_file('some/artifact')
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -281,7 +291,7 @@ describe API::Jobs do
end
it 'returns specific job artifacts' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.headers).to include(download_headers)
expect(response.body).to match_file(job.artifacts_file.file.file)
end
@@ -292,13 +302,13 @@ describe API::Jobs do
it 'hides artifacts and rejects request' do
expect(project).to be_private
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
it 'does not return job artifacts if not uploaded' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -323,7 +333,7 @@ describe API::Jobs do
it 'does not find a resource in a private project' do
expect(project).to be_private
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -335,13 +345,13 @@ describe API::Jobs do
end
it 'gives 403' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
context 'non-existing job' do
shared_examples 'not found' do
- it { expect(response).to have_http_status(:not_found) }
+ it { expect(response).to have_gitlab_http_status(:not_found) }
end
context 'has no such ref' do
@@ -369,7 +379,7 @@ describe API::Jobs do
"attachment; filename=#{job.artifacts_file.filename}" }
end
- it { expect(response).to have_http_status(200) }
+ it { expect(response).to have_gitlab_http_status(200) }
it { expect(response.headers).to include(download_headers) }
end
@@ -410,7 +420,7 @@ describe API::Jobs do
context 'authorized user' do
it 'returns specific job trace' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.body).to eq(job.trace.raw)
end
end
@@ -419,7 +429,7 @@ describe API::Jobs do
let(:api_user) { nil }
it 'does not return specific job trace' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -432,7 +442,7 @@ describe API::Jobs do
context 'authorized user' do
context 'user with :update_build persmission' do
it 'cancels running or pending job' do
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(project.builds.first.status).to eq('canceled')
end
end
@@ -441,7 +451,7 @@ describe API::Jobs do
let(:api_user) { reporter }
it 'does not cancel job' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -450,7 +460,7 @@ describe API::Jobs do
let(:api_user) { nil }
it 'does not cancel job' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -465,7 +475,7 @@ describe API::Jobs do
context 'authorized user' do
context 'user with :update_build permission' do
it 'retries non-running job' do
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(project.builds.first.status).to eq('canceled')
expect(json_response['status']).to eq('pending')
end
@@ -475,7 +485,7 @@ describe API::Jobs do
let(:api_user) { reporter }
it 'does not retry job' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -484,13 +494,17 @@ describe API::Jobs do
let(:api_user) { nil }
it 'does not retry job' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
describe 'POST /projects/:id/jobs/:job_id/erase' do
+ let(:role) { :master }
+
before do
+ project.team << [user, role]
+
post api("/projects/#{project.id}/jobs/#{job.id}/erase", user)
end
@@ -498,7 +512,7 @@ describe API::Jobs do
let(:job) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
it 'erases job content' do
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(job).not_to have_trace
expect(job.artifacts_file.exists?).to be_falsy
expect(job.artifacts_metadata.exists?).to be_falsy
@@ -516,7 +530,24 @@ describe API::Jobs do
let(:job) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
it 'responds with forbidden' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'when a developer erases a build' do
+ let(:role) { :developer }
+ let(:job) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline, user: owner) }
+
+ context 'when the build was created by the developer' do
+ let(:owner) { user }
+
+ it { expect(response).to have_gitlab_http_status(201) }
+ end
+
+ context 'when the build was created by the other' do
+ let(:owner) { create(:user) }
+
+ it { expect(response).to have_gitlab_http_status(403) }
end
end
end
@@ -533,7 +564,7 @@ describe API::Jobs do
end
it 'keeps artifacts' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(job.reload.artifacts_expire_at).to be_nil
end
end
@@ -542,7 +573,7 @@ describe API::Jobs do
let(:job) { create(:ci_build, project: project, pipeline: pipeline) }
it 'responds with not found' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -557,7 +588,7 @@ describe API::Jobs do
context 'when user is authorized to trigger a manual action' do
it 'plays the job' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['user']['id']).to eq(user.id)
expect(json_response['id']).to eq(job.id)
expect(job.reload).to be_pending
@@ -570,7 +601,7 @@ describe API::Jobs do
it 'does not trigger a manual action' do
expect(job.reload).to be_manual
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -579,7 +610,7 @@ describe API::Jobs do
it 'does not trigger a manual action' do
expect(job.reload).to be_manual
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -587,7 +618,7 @@ describe API::Jobs do
context 'on a non-playable job' do
it 'returns a status code 400, Bad Request' do
- expect(response).to have_http_status 400
+ expect(response).to have_gitlab_http_status 400
expect(response.body).to match("Unplayable Job")
end
end
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index f534332ca6c..3c4719964b6 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -10,14 +10,14 @@ describe API::Keys do
context 'when unauthenticated' do
it 'returns authentication error' do
get api("/keys/#{key.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
context 'when authenticated' do
it 'returns 404 for non-existing key' do
get api('/keys/999999', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
@@ -25,7 +25,7 @@ describe API::Keys do
user.keys << key
user.save
get api("/keys/#{key.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(key.title)
expect(json_response['user']['id']).to eq(user.id)
expect(json_response['user']['username']).to eq(user.username)
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index b231fdea2a3..3498e5bc8d9 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -27,7 +27,7 @@ describe API::Labels do
get api("/projects/#{project.id}/labels", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
@@ -75,7 +75,7 @@ describe API::Labels do
description: 'test',
priority: 2
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq('Foo')
expect(json_response['color']).to eq('#FFAABB')
expect(json_response['description']).to eq('test')
@@ -109,19 +109,19 @@ describe API::Labels do
it 'returns a 400 bad request if name not given' do
post api("/projects/#{project.id}/labels", user), color: '#FFAABB'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns a 400 bad request if color not given' do
post api("/projects/#{project.id}/labels", user), name: 'Foobar'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 for invalid color' do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAA'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
@@ -129,7 +129,7 @@ describe API::Labels do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAAFFFF'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
@@ -137,7 +137,7 @@ describe API::Labels do
post api("/projects/#{project.id}/labels", user),
name: ',',
color: '#FFAABB'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['title']).to eq(['is invalid'])
end
@@ -150,7 +150,7 @@ describe API::Labels do
name: group_label.name,
color: '#FFAABB'
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
expect(json_response['message']).to eq('Label already exists')
end
@@ -160,14 +160,14 @@ describe API::Labels do
color: '#FFAAFFFF',
priority: 'foo'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 409 if label already exists in project' do
post api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FFAABB'
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
expect(json_response['message']).to eq('Label already exists')
end
end
@@ -176,18 +176,18 @@ describe API::Labels do
it 'returns 204 for existing label' do
delete api("/projects/#{project.id}/labels", user), name: 'label1'
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end
it 'returns 404 for non existing label' do
delete api("/projects/#{project.id}/labels", user), name: 'label2'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Label Not Found')
end
it 'returns 400 for wrong parameters' do
delete api("/projects/#{project.id}/labels", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it_behaves_like '412 response' do
@@ -203,7 +203,7 @@ describe API::Labels do
new_name: 'New Label',
color: '#FFFFFF',
description: 'test'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq('New Label')
expect(json_response['color']).to eq('#FFFFFF')
expect(json_response['description']).to eq('test')
@@ -213,7 +213,7 @@ describe API::Labels do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
new_name: 'New Label'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq('New Label')
expect(json_response['color']).to eq(label1.color)
end
@@ -222,7 +222,7 @@ describe API::Labels do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FFFFFF'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(label1.name)
expect(json_response['color']).to eq('#FFFFFF')
end
@@ -232,7 +232,7 @@ describe API::Labels do
name: 'bug',
description: 'test'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(priority_label.name)
expect(json_response['description']).to eq('test')
expect(json_response['priority']).to eq(3)
@@ -272,18 +272,18 @@ describe API::Labels do
put api("/projects/#{project.id}/labels", user),
name: 'label2',
new_name: 'label3'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 400 if no label name given' do
put api("/projects/#{project.id}/labels", user), new_name: 'label2'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('name is missing')
end
it 'returns 400 if no new parameters given' do
put api("/projects/#{project.id}/labels", user), name: 'label1'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('new_name, color, description, priority are missing, '\
'at least one parameter must be provided')
end
@@ -293,7 +293,7 @@ describe API::Labels do
name: 'label1',
new_name: ',',
color: '#FFFFFF'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['title']).to eq(['is invalid'])
end
@@ -301,7 +301,7 @@ describe API::Labels do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FF'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
@@ -309,7 +309,7 @@ describe API::Labels do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAAFFFF'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
@@ -318,7 +318,7 @@ describe API::Labels do
name: 'Foo',
priority: 'foo'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -327,7 +327,7 @@ describe API::Labels do
it "subscribes to the label" do
post api("/projects/#{project.id}/labels/#{label1.title}/subscribe", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_truthy
end
@@ -337,7 +337,7 @@ describe API::Labels do
it "subscribes to the label" do
post api("/projects/#{project.id}/labels/#{label1.id}/subscribe", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_truthy
end
@@ -351,7 +351,7 @@ describe API::Labels do
it "returns 304" do
post api("/projects/#{project.id}/labels/#{label1.id}/subscribe", user)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
end
@@ -359,7 +359,7 @@ describe API::Labels do
it "returns 404 error" do
post api("/projects/#{project.id}/labels/1234/subscribe", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -373,7 +373,7 @@ describe API::Labels do
it "unsubscribes from the label" do
post api("/projects/#{project.id}/labels/#{label1.title}/unsubscribe", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_falsey
end
@@ -383,7 +383,7 @@ describe API::Labels do
it "unsubscribes from the label" do
post api("/projects/#{project.id}/labels/#{label1.id}/unsubscribe", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_falsey
end
@@ -397,7 +397,7 @@ describe API::Labels do
it "returns 304" do
post api("/projects/#{project.id}/labels/#{label1.id}/unsubscribe", user)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
end
@@ -405,7 +405,7 @@ describe API::Labels do
it "returns 404 error" do
post api("/projects/#{project.id}/labels/1234/unsubscribe", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index df7c91b5bc1..e3065840e6f 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -10,7 +10,7 @@ describe API::Lint do
it 'passes validation' do
post api('/ci/lint'), { content: yaml_content }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Hash
expect(json_response['status']).to eq('valid')
expect(json_response['errors']).to eq([])
@@ -21,7 +21,7 @@ describe API::Lint do
it 'responds with errors about invalid syntax' do
post api('/ci/lint'), { content: 'invalid content' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to eq('invalid')
expect(json_response['errors']).to eq(['Invalid configuration format'])
end
@@ -29,7 +29,7 @@ describe API::Lint do
it "responds with errors about invalid configuration" do
post api('/ci/lint'), { content: '{ image: "ruby:2.1", services: ["postgres"] }' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to eq('invalid')
expect(json_response['errors']).to eq(['jobs config should contain at least one visible job'])
end
@@ -39,7 +39,7 @@ describe API::Lint do
it 'responds with validation error about missing content' do
post api('/ci/lint')
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('content is missing')
end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index d3bae8d2888..3349e396ab8 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -35,7 +35,7 @@ describe API::Members do
get api("/#{source_type.pluralize}/#{source.id}/members", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
@@ -49,7 +49,7 @@ describe API::Members do
get api("/#{source_type.pluralize}/#{source.id}/members", developer)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
@@ -59,7 +59,7 @@ describe API::Members do
it 'finds members with query string' do
get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
@@ -81,7 +81,7 @@ describe API::Members do
user = public_send(type)
get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
# User attributes
expect(json_response['id']).to eq(developer.id)
expect(json_response['name']).to eq(developer.name)
@@ -116,7 +116,7 @@ describe API::Members do
post api("/#{source_type.pluralize}/#{source.id}/members", user),
user_id: access_requester.id, access_level: Member::MASTER
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -129,7 +129,7 @@ describe API::Members do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: access_requester.id, access_level: Member::MASTER
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end.to change { source.members.count }.by(1)
expect(source.requesters.count).to eq(0)
expect(json_response['id']).to eq(access_requester.id)
@@ -142,7 +142,7 @@ describe API::Members do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end.to change { source.members.count }.by(1)
expect(json_response['id']).to eq(stranger.id)
expect(json_response['access_level']).to eq(Member::DEVELOPER)
@@ -154,28 +154,28 @@ describe API::Members do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: master.id, access_level: Member::MASTER
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
end
it 'returns 400 when user_id is not given' do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
access_level: Member::MASTER
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 when access_level is not given' do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: stranger.id
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 when access_level is not valid' do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: stranger.id, access_level: 1234
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
end
@@ -197,7 +197,7 @@ describe API::Members do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user),
access_level: Member::MASTER
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -208,7 +208,7 @@ describe API::Members do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
access_level: Member::MASTER, expires_at: '2016-08-05'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(developer.id)
expect(json_response['access_level']).to eq(Member::MASTER)
expect(json_response['expires_at']).to eq('2016-08-05')
@@ -219,20 +219,20 @@ describe API::Members do
put api("/#{source_type.pluralize}/#{source.id}/members/123", master),
access_level: Member::MASTER
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 400 when access_level is not given' do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 when access level is not valid' do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
access_level: 1234
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
end
@@ -250,7 +250,7 @@ describe API::Members do
user = public_send(type)
delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -261,7 +261,7 @@ describe API::Members do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { source.members.count }.by(-1)
end
end
@@ -272,7 +272,7 @@ describe API::Members do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end.not_to change { source.requesters.count }
end
end
@@ -281,7 +281,7 @@ describe API::Members do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { source.members.count }.by(-1)
end
@@ -293,7 +293,7 @@ describe API::Members do
it 'returns 404 if member does not exist' do
delete api("/#{source_type.pluralize}/#{source.id}/members/123", master)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -344,7 +344,7 @@ describe API::Members do
post api("/projects/#{project.id}/members", master),
user_id: stranger.id, access_level: Member::OWNER
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end.to change { project.members.count }.by(0)
end
end
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index d9da94d4713..bf4c8443b23 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -26,12 +26,12 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
it 'returns a 404 when merge_request id is used instead of the iid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 when merge_request_iid not found' do
get api("/projects/#{project.id}/merge_requests/999/versions", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -49,17 +49,17 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
it 'returns a 404 when merge_request id is used instead of the iid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 when merge_request version_id is not found' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/999", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 when merge_request_iid is not found' do
get api("/projects/#{project.id}/merge_requests/12345/versions/#{merge_request_diff.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 5e66e1607ba..91616da6d9a 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -33,26 +33,26 @@ describe API::MergeRequests do
it 'returns an array of all merge requests' do
get api('/merge_requests', user), scope: 'all'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
end
it "returns authentication error without any scope" do
get api("/merge_requests")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it "returns authentication error when scope is assigned-to-me" do
get api("/merge_requests"), scope: 'assigned-to-me'
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it "returns authentication error when scope is created-by-me" do
get api("/merge_requests"), scope: 'created-by-me'
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -158,7 +158,7 @@ describe API::MergeRequests do
it 'returns merge requests for public projects' do
get api("/projects/#{project.id}/merge_requests")
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
end
@@ -166,21 +166,21 @@ describe API::MergeRequests do
project = create(:project, :private)
get api("/projects/#{project.id}/merge_requests")
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
context "when authenticated" do
it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new do
+ control = ActiveRecord::QueryRecorder.new do
get api("/projects/#{project.id}/merge_requests", user)
- end.count
+ end
create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time)
expect do
get api("/projects/#{project.id}/merge_requests", user)
- end.not_to exceed_query_limit(control_count)
+ end.not_to exceed_query_limit(control)
end
it "returns an array of all merge_requests" do
@@ -435,17 +435,7 @@ describe API::MergeRequests do
expect(json_response['merge_status']).to eq('can_be_merged')
expect(json_response['should_close_merge_request']).to be_falsy
expect(json_response['force_close_merge_request']).to be_falsy
- end
-
- it "returns merge_request" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(merge_request.title)
- expect(json_response['iid']).to eq(merge_request.iid)
- expect(json_response['work_in_progress']).to eq(false)
- expect(json_response['merge_status']).to eq('can_be_merged')
- expect(json_response['should_close_merge_request']).to be_falsy
- expect(json_response['force_close_merge_request']).to be_falsy
+ expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size)
end
it "returns a 404 error if merge_request_iid not found" do
@@ -462,12 +452,32 @@ describe API::MergeRequests do
context 'Work in Progress' do
let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
- it "returns merge_request" do
+ it "returns merge request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user)
+
expect(response).to have_gitlab_http_status(200)
expect(json_response['work_in_progress']).to eq(true)
end
end
+
+ context 'when a merge request has more than the changes limit' do
+ it "returns a string indicating that more changes were made" do
+ stub_const('Commit::DIFF_HARD_LIMIT_FILES', 5)
+
+ merge_request_overflow = create(:merge_request, :simple,
+ author: user,
+ assignee: user,
+ source_project: project,
+ source_branch: 'expand-collapse-files',
+ target_project: project,
+ target_branch: 'master')
+
+ get api("/projects/#{project.id}/merge_requests/#{merge_request_overflow.iid}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['changes_count']).to eq('5+')
+ end
+ end
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do
@@ -618,13 +628,11 @@ describe API::MergeRequests do
context 'forked projects' do
let!(:user2) { create(:user) }
- let!(:forked_project) { fork_project(project, user2) }
+ let!(:forked_project) { fork_project(project, user2, repository: true) }
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
before do
forked_project.add_reporter(user2)
-
- allow_any_instance_of(MergeRequest).to receive(:write_ref)
end
it "returns merge_request" do
@@ -1061,6 +1069,30 @@ describe API::MergeRequests do
end
end
+ describe 'POST :id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
+ before do
+ ::MergeRequests::MergeWhenPipelineSucceedsService.new(merge_request.target_project, user).execute(merge_request)
+ end
+
+ it 'removes the merge_when_pipeline_succeeds status' do
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/cancel_merge_when_pipeline_succeeds", user)
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+
+ it 'returns 404 if the merge request is not found' do
+ post api("/projects/#{project.id}/merge_requests/123/merge_when_pipeline_succeeds", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 if the merge request id is used instead of iid' do
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge_when_pipeline_succeeds", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
describe 'Time tracking' do
let(:issuable) { merge_request }
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 26cf653ca8e..98102fcd6a7 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -10,7 +10,7 @@ describe API::Namespaces do
context "when unauthenticated" do
it "returns authentication error" do
get api("/namespaces")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -21,7 +21,7 @@ describe API::Namespaces do
group_kind_json_response = json_response.find { |resource| resource['kind'] == 'group' }
user_kind_json_response = json_response.find { |resource| resource['kind'] == 'user' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(group_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path',
'parent_id', 'members_count_with_descendants')
@@ -32,7 +32,7 @@ describe API::Namespaces do
it "admin: returns an array of all namespaces" do
get api("/namespaces", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(Namespace.count)
@@ -41,7 +41,7 @@ describe API::Namespaces do
it "admin: returns an array of matched namespaces" do
get api("/namespaces?search=#{group2.name}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -75,7 +75,7 @@ describe API::Namespaces do
it "user: returns an array of namespaces" do
get api("/namespaces", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -84,11 +84,134 @@ describe API::Namespaces do
it "admin: returns an array of matched namespaces" do
get api("/namespaces?search=#{user.username}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
end
end
+
+ describe 'GET /namespaces/:id' do
+ let(:owned_group) { group1 }
+ let(:user2) { create(:user) }
+
+ shared_examples 'can access namespace' do
+ it 'returns namespace details' do
+ get api("/namespaces/#{namespace_id}", request_actor)
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['id']).to eq(requested_namespace.id)
+ expect(json_response['path']).to eq(requested_namespace.path)
+ expect(json_response['name']).to eq(requested_namespace.name)
+ end
+ end
+
+ shared_examples 'namespace reader' do
+ let(:requested_namespace) { owned_group }
+
+ before do
+ owned_group.add_owner(request_actor)
+ end
+
+ context 'when namespace exists' do
+ context 'when requested by ID' do
+ context 'when requesting group' do
+ let(:namespace_id) { owned_group.id }
+
+ it_behaves_like 'can access namespace'
+ end
+
+ context 'when requesting personal namespace' do
+ let(:namespace_id) { request_actor.namespace.id }
+ let(:requested_namespace) { request_actor.namespace }
+
+ it_behaves_like 'can access namespace'
+ end
+ end
+
+ context 'when requested by path' do
+ context 'when requesting group' do
+ let(:namespace_id) { owned_group.path }
+
+ it_behaves_like 'can access namespace'
+ end
+
+ context 'when requesting personal namespace' do
+ let(:namespace_id) { request_actor.namespace.path }
+ let(:requested_namespace) { request_actor.namespace }
+
+ it_behaves_like 'can access namespace'
+ end
+ end
+ end
+
+ context "when namespace doesn't exist" do
+ it 'returns not-found' do
+ get api('/namespaces/9999', request_actor)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api("/namespaces/#{group1.id}")
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+
+ context 'when authenticated as regular user' do
+ let(:request_actor) { user }
+
+ context 'when requested namespace is not owned by user' do
+ context 'when requesting group' do
+ it 'returns not-found' do
+ get api("/namespaces/#{group2.id}", request_actor)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when requesting personal namespace' do
+ it 'returns not-found' do
+ get api("/namespaces/#{user2.namespace.id}", request_actor)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context 'when requested namespace is owned by user' do
+ it_behaves_like 'namespace reader'
+ end
+ end
+
+ context 'when authenticated as admin' do
+ let(:request_actor) { admin }
+
+ context 'when requested namespace is not owned by user' do
+ context 'when requesting group' do
+ let(:namespace_id) { group2.id }
+ let(:requested_namespace) { group2 }
+
+ it_behaves_like 'can access namespace'
+ end
+
+ context 'when requesting personal namespace' do
+ let(:namespace_id) { user2.namespace.id }
+ let(:requested_namespace) { user2.namespace }
+
+ it_behaves_like 'can access namespace'
+ end
+ end
+
+ context 'when requested namespace is owned by user' do
+ it_behaves_like 'namespace reader'
+ end
+ end
+ end
end
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index fb440fa551c..3bfb4c5506f 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -34,10 +34,52 @@ describe API::Notes do
describe "GET /projects/:id/noteable/:noteable_id/notes" do
context "when noteable is an Issue" do
+ context 'sorting' do
+ before do
+ create_list(:note, 3, noteable: issue, project: project, author: user)
+ end
+
+ it 'sorts by created_at in descending order by default' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes", user)
+
+ response_dates = json_response.map { |noteable| noteable['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by ascending order when requested' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes?sort=asc", user)
+
+ response_dates = json_response.map { |noteable| noteable['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at in descending order when requested' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes?order_by=updated_at", user)
+
+ response_dates = json_response.map { |noteable| noteable['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at in ascending order when requested' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes??order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |noteable| noteable['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
it "returns an array of issue notes" do
get api("/projects/#{project.id}/issues/#{issue.iid}/notes", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(issue_note.note)
@@ -46,14 +88,14 @@ describe API::Notes do
it "returns a 404 error when issue id not found" do
get api("/projects/#{project.id}/issues/12345/notes", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context "and current user cannot view the notes" do
it "returns an empty array" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to be_empty
@@ -67,7 +109,7 @@ describe API::Notes do
it "returns 404" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -75,7 +117,7 @@ describe API::Notes do
it "returns an empty array" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", private_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(cross_reference_note.note)
@@ -85,10 +127,51 @@ describe API::Notes do
end
context "when noteable is a Snippet" do
+ context 'sorting' do
+ before do
+ create_list(:note, 3, noteable: snippet, project: project, author: user)
+ end
+
+ it 'sorts by created_at in descending order by default' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
+
+ response_dates = json_response.map { |noteable| noteable['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by ascending order when requested' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/notes?sort=asc", user)
+
+ response_dates = json_response.map { |noteable| noteable['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at in descending order when requested' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/notes?order_by=updated_at", user)
+
+ response_dates = json_response.map { |noteable| noteable['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at in ascending order when requested' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/notes??order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |noteable| noteable['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
it "returns an array of snippet notes" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(snippet_note.note)
@@ -97,21 +180,62 @@ describe API::Notes do
it "returns a 404 error when snippet id not found" do
get api("/projects/#{project.id}/snippets/42/notes", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 when not authorized" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
context "when noteable is a Merge Request" do
+ context 'sorting' do
+ before do
+ create_list(:note, 3, noteable: merge_request, project: project, author: user)
+ end
+
+ it 'sorts by created_at in descending order by default' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user)
+
+ response_dates = json_response.map { |noteable| noteable['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by ascending order when requested' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes?sort=asc", user)
+
+ response_dates = json_response.map { |noteable| noteable['created_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at in descending order when requested' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes?order_by=updated_at", user)
+
+ response_dates = json_response.map { |noteable| noteable['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at in ascending order when requested' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes??order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |noteable| noteable['updated_at'] }
+
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
it "returns an array of merge_requests notes" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(merge_request_note.note)
@@ -120,13 +244,13 @@ describe API::Notes do
it "returns a 404 error if merge request id not found" do
get api("/projects/#{project.id}/merge_requests/4444/notes", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 when not authorized" do
get api("/projects/#{project.id}/merge_requests/4444/notes", private_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -136,21 +260,21 @@ describe API::Notes do
it "returns an issue note by id" do
get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{issue_note.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['body']).to eq(issue_note.note)
end
it "returns a 404 error if issue note not found" do
get api("/projects/#{project.id}/issues/#{issue.iid}/notes/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context "and current user cannot view the note" do
it "returns a 404 error" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context "when issue is confidential" do
@@ -161,7 +285,7 @@ describe API::Notes do
it "returns 404" do
get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{issue_note.id}", private_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -169,7 +293,7 @@ describe API::Notes do
it "returns an issue note by id" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", private_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['body']).to eq(cross_reference_note.note)
end
end
@@ -180,14 +304,14 @@ describe API::Notes do
it "returns a snippet note by id" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['body']).to eq(snippet_note.note)
end
it "returns a 404 error if snippet note not found" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -197,7 +321,7 @@ describe API::Notes do
it "creates a new issue note" do
post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'hi!'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
end
@@ -205,13 +329,13 @@ describe API::Notes do
it "returns a 400 bad request error if body not given" do
post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 401 unauthorized error if user not authenticated" do
post api("/projects/#{project.id}/issues/#{issue.iid}/notes"), body: 'hi!'
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
context 'when an admin or owner makes the request' do
@@ -220,7 +344,7 @@ describe API::Notes do
post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user),
body: 'hi!', created_at: creation_time
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
@@ -233,7 +357,7 @@ describe API::Notes do
it 'creates a new issue note' do
post api("/projects/#{project.id}/issues/#{issue2.iid}/notes", user), body: ':+1:'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['body']).to eq(':+1:')
end
end
@@ -242,7 +366,7 @@ describe API::Notes do
it 'creates a new issue note' do
post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: ':+1:'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['body']).to eq(':+1:')
end
end
@@ -252,7 +376,7 @@ describe API::Notes do
it "creates a new snippet note" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
end
@@ -260,13 +384,13 @@ describe API::Notes do
it "returns a 400 bad request error if body not given" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 401 unauthorized error if user not authenticated" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!'
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -278,7 +402,7 @@ describe API::Notes do
post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user),
body: 'Foo'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -314,7 +438,7 @@ describe API::Notes do
it 'returns 200 status' do
subject
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
it 'creates a new note' do
@@ -328,7 +452,7 @@ describe API::Notes do
it 'returns 403 status' do
subject
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'does not create a new note' do
@@ -352,7 +476,7 @@ describe API::Notes do
put api("/projects/#{project.id}/issues/#{issue.iid}/"\
"notes/#{issue_note.id}", user), body: 'Hello!'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['body']).to eq('Hello!')
end
@@ -360,14 +484,14 @@ describe API::Notes do
put api("/projects/#{project.id}/issues/#{issue.iid}/notes/12345", user),
body: 'Hello!'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 400 bad request error if body not given' do
put api("/projects/#{project.id}/issues/#{issue.iid}/"\
"notes/#{issue_note.id}", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -376,7 +500,7 @@ describe API::Notes do
put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user), body: 'Hello!'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['body']).to eq('Hello!')
end
@@ -384,7 +508,7 @@ describe API::Notes do
put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/12345", user), body: "Hello!"
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -393,7 +517,7 @@ describe API::Notes do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/"\
"notes/#{merge_request_note.id}", user), body: 'Hello!'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['body']).to eq('Hello!')
end
@@ -401,7 +525,7 @@ describe API::Notes do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/"\
"notes/12345", user), body: "Hello!"
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -412,17 +536,17 @@ describe API::Notes do
delete api("/projects/#{project.id}/issues/#{issue.iid}/"\
"notes/#{issue_note.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
# Check if note is really deleted
delete api("/projects/#{project.id}/issues/#{issue.iid}/"\
"notes/#{issue_note.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 error when note id not found' do
delete api("/projects/#{project.id}/issues/#{issue.iid}/notes/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it_behaves_like '412 response' do
@@ -435,18 +559,18 @@ describe API::Notes do
delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
# Check if note is really deleted
delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 error when note id not found' do
delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it_behaves_like '412 response' do
@@ -459,18 +583,18 @@ describe API::Notes do
delete api("/projects/#{project.id}/merge_requests/"\
"#{merge_request.iid}/notes/#{merge_request_note.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
# Check if note is really deleted
delete api("/projects/#{project.id}/merge_requests/"\
"#{merge_request.iid}/notes/#{merge_request_note.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 error when note id not found' do
delete api("/projects/#{project.id}/merge_requests/"\
"#{merge_request.iid}/notes/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it_behaves_like '412 response' do
diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb
index 7968659a1ec..3273cd26690 100644
--- a/spec/requests/api/notification_settings_spec.rb
+++ b/spec/requests/api/notification_settings_spec.rb
@@ -9,7 +9,7 @@ describe API::NotificationSettings do
it "returns global notification settings for the current user" do
get api("/notification_settings", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_a Hash
expect(json_response['notification_email']).to eq(user.notification_email)
expect(json_response['level']).to eq(user.global_notification_setting.level)
@@ -22,7 +22,7 @@ describe API::NotificationSettings do
it "updates global notification settings for the current user" do
put api("/notification_settings", user), { level: 'watch', notification_email: email.email }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['notification_email']).to eq(email.email)
expect(user.reload.notification_email).to eq(email.email)
expect(json_response['level']).to eq(user.reload.global_notification_setting.level)
@@ -33,7 +33,7 @@ describe API::NotificationSettings do
it "fails on non-user email address" do
put api("/notification_settings", user), { notification_email: 'invalid@example.com' }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -41,7 +41,7 @@ describe API::NotificationSettings do
it "returns group level notification settings for the current user" do
get api("/groups/#{group.id}/notification_settings", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_a Hash
expect(json_response['level']).to eq(user.notification_settings_for(group).level)
end
@@ -51,7 +51,7 @@ describe API::NotificationSettings do
it "updates group level notification settings for the current user" do
put api("/groups/#{group.id}/notification_settings", user), { level: 'watch' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['level']).to eq(user.reload.notification_settings_for(group).level)
end
end
@@ -60,7 +60,7 @@ describe API::NotificationSettings do
it "returns project level notification settings for the current user" do
get api("/projects/#{project.id}/notification_settings", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_a Hash
expect(json_response['level']).to eq(user.notification_settings_for(project).level)
end
@@ -70,7 +70,7 @@ describe API::NotificationSettings do
it "updates project level notification settings for the current user" do
put api("/projects/#{project.id}/notification_settings", user), { level: 'custom', new_note: true }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['level']).to eq(user.reload.notification_settings_for(project).level)
expect(json_response['events']['new_note']).to be_truthy
expect(json_response['events']['new_issue']).to be_falsey
@@ -81,7 +81,7 @@ describe API::NotificationSettings do
it "fails on invalid level" do
put api("/projects/#{project.id}/notification_settings", user), { level: 'invalid' }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
end
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
index 0d56e1f732e..bdda80cc229 100644
--- a/spec/requests/api/oauth_tokens_spec.rb
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -12,7 +12,7 @@ describe 'OAuth tokens' do
request_oauth_token(user)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
expect(json_response['error']).to eq('invalid_grant')
end
end
@@ -23,7 +23,7 @@ describe 'OAuth tokens' do
request_oauth_token(user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['access_token']).not_to be_nil
end
end
@@ -35,7 +35,7 @@ describe 'OAuth tokens' do
request_oauth_token(user)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -46,7 +46,7 @@ describe 'OAuth tokens' do
request_oauth_token(user)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
new file mode 100644
index 00000000000..d412b045e9f
--- /dev/null
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -0,0 +1,487 @@
+require 'rails_helper'
+
+describe API::PagesDomains do
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+ set(:admin) { create(:admin) }
+
+ set(:pages_domain) { create(:pages_domain, domain: 'www.domain.test', project: project) }
+ set(:pages_domain_secure) { create(:pages_domain, :with_certificate, :with_key, domain: 'ssl.domain.test', project: project) }
+ set(:pages_domain_expired) { create(:pages_domain, :with_expired_certificate, :with_key, domain: 'expired.domain.test', project: project) }
+
+ let(:pages_domain_params) { build(:pages_domain, domain: 'www.other-domain.test').slice(:domain) }
+ let(:pages_domain_secure_params) { build(:pages_domain, :with_certificate, :with_key, domain: 'ssl.other-domain.test', project: project).slice(:domain, :certificate, :key) }
+ let(:pages_domain_secure_key_missmatch_params) {build(:pages_domain, :with_trusted_chain, :with_key, project: project).slice(:domain, :certificate, :key) }
+ let(:pages_domain_secure_missing_chain_params) {build(:pages_domain, :with_missing_chain, project: project).slice(:certificate) }
+
+ let(:route) { "/projects/#{project.id}/pages/domains" }
+ let(:route_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain.domain}" }
+ let(:route_secure_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_secure.domain}" }
+ let(:route_expired_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_expired.domain}" }
+ let(:route_vacant_domain) { "/projects/#{project.id}/pages/domains/www.vacant-domain.test" }
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ end
+
+ describe 'GET /pages/domains' do
+ context 'when pages is disabled' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ end
+
+ it_behaves_like '404 response' do
+ let(:request) { get api('/pages/domains', admin) }
+ end
+ end
+
+ context 'when pages is enabled' do
+ context 'when authenticated as an admin' do
+ it 'returns paginated all pages domains' do
+ get api('/pages/domains', admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain_basics')
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(3)
+ expect(json_response.last).to have_key('domain')
+ expect(json_response.last).to have_key('certificate_expiration')
+ expect(json_response.last['certificate_expiration']['expired']).to be true
+ expect(json_response.first).not_to have_key('certificate_expiration')
+ end
+ end
+
+ context 'when authenticated as a non-member' do
+ it_behaves_like '403 response' do
+ let(:request) { get api('/pages/domains', user) }
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:project_id/pages/domains' do
+ shared_examples_for 'get pages domains' do
+ it 'returns paginated pages domains' do
+ get api(route, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domains')
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(3)
+ expect(json_response.map { |pages_domain| pages_domain['domain'] }).to include(pages_domain.domain)
+ expect(json_response.last).to have_key('domain')
+ end
+ end
+
+ context 'when pages is disabled' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ project.add_master(user)
+ end
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, user) }
+ end
+ end
+
+ context 'when user is a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'get pages domains'
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, user) }
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, user) }
+ end
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, user) }
+ end
+ end
+
+ context 'when user is not a member' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, user) }
+ end
+ end
+ end
+
+ describe 'GET /projects/:project_id/pages/domains/:domain' do
+ shared_examples_for 'get pages domain' do
+ it 'returns pages domain' do
+ get api(route_domain, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(json_response['domain']).to eq(pages_domain.domain)
+ expect(json_response['url']).to eq(pages_domain.url)
+ expect(json_response['certificate']).to be_nil
+ end
+
+ it 'returns pages domain with a certificate' do
+ get api(route_secure_domain, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(json_response['domain']).to eq(pages_domain_secure.domain)
+ expect(json_response['url']).to eq(pages_domain_secure.url)
+ expect(json_response['certificate']['subject']).to eq(pages_domain_secure.subject)
+ expect(json_response['certificate']['expired']).to be false
+ end
+
+ it 'returns pages domain with an expired certificate' do
+ get api(route_expired_domain, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(json_response['certificate']['expired']).to be true
+ end
+ end
+
+ context 'when domain is vacant' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route_vacant_domain, user) }
+ end
+ end
+
+ context 'when user is a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'get pages domain'
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, user) }
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, user) }
+ end
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, user) }
+ end
+ end
+
+ context 'when user is not a member' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, user) }
+ end
+ end
+ end
+
+ describe 'POST /projects/:project_id/pages/domains' do
+ let(:params) { pages_domain_params.slice(:domain) }
+ let(:params_secure) { pages_domain_secure_params.slice(:domain, :certificate, :key) }
+
+ shared_examples_for 'post pages domains' do
+ it 'creates a new pages domain' do
+ post api(route, user), params
+ pages_domain = PagesDomain.find_by(domain: json_response['domain'])
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(pages_domain.domain).to eq(params[:domain])
+ expect(pages_domain.certificate).to be_nil
+ expect(pages_domain.key).to be_nil
+ end
+
+ it 'creates a new secure pages domain' do
+ post api(route, user), params_secure
+ pages_domain = PagesDomain.find_by(domain: json_response['domain'])
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(pages_domain.domain).to eq(params_secure[:domain])
+ expect(pages_domain.certificate).to eq(params_secure[:certificate])
+ expect(pages_domain.key).to eq(params_secure[:key])
+ end
+
+ it 'fails to create pages domain without key' do
+ post api(route, user), pages_domain_secure_params.slice(:domain, :certificate)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'fails to create pages domain with key missmatch' do
+ post api(route, user), pages_domain_secure_key_missmatch_params.slice(:domain, :certificate, :key)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+
+ context 'when user is a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'post pages domains'
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { post api(route, user), params }
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { post api(route, user), params }
+ end
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { post api(route, user), params }
+ end
+ end
+
+ context 'when user is not a member' do
+ it_behaves_like '404 response' do
+ let(:request) { post api(route, user), params }
+ end
+ end
+ end
+
+ describe 'PUT /projects/:project_id/pages/domains/:domain' do
+ let(:params_secure) { pages_domain_secure_params.slice(:certificate, :key) }
+ let(:params_secure_nokey) { pages_domain_secure_params.slice(:certificate) }
+
+ shared_examples_for 'put pages domain' do
+ it 'updates pages domain removing certificate' do
+ put api(route_secure_domain, user)
+ pages_domain_secure.reload
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(pages_domain_secure.certificate).to be_nil
+ expect(pages_domain_secure.key).to be_nil
+ end
+
+ it 'updates pages domain adding certificate' do
+ put api(route_domain, user), params_secure
+ pages_domain.reload
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(pages_domain.certificate).to eq(params_secure[:certificate])
+ expect(pages_domain.key).to eq(params_secure[:key])
+ end
+
+ it 'updates pages domain with expired certificate' do
+ put api(route_expired_domain, user), params_secure
+ pages_domain_expired.reload
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(pages_domain_expired.certificate).to eq(params_secure[:certificate])
+ expect(pages_domain_expired.key).to eq(params_secure[:key])
+ end
+
+ it 'updates pages domain with expired certificate not updating key' do
+ put api(route_secure_domain, user), params_secure_nokey
+ pages_domain_secure.reload
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(pages_domain_secure.certificate).to eq(params_secure_nokey[:certificate])
+ end
+
+ it 'fails to update pages domain adding certificate without key' do
+ put api(route_domain, user), params_secure_nokey
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'fails to update pages domain adding certificate with missing chain' do
+ put api(route_domain, user), pages_domain_secure_missing_chain_params.slice(:certificate)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'fails to update pages domain with key missmatch' do
+ put api(route_secure_domain, user), pages_domain_secure_key_missmatch_params.slice(:certificate, :key)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+
+ context 'when domain is vacant' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like '404 response' do
+ let(:request) { put api(route_vacant_domain, user) }
+ end
+ end
+
+ context 'when user is a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'put pages domain'
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { put api(route_domain, user) }
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { put api(route_domain, user) }
+ end
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { put api(route_domain, user) }
+ end
+ end
+
+ context 'when user is not a member' do
+ it_behaves_like '404 response' do
+ let(:request) { put api(route_domain, user) }
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:project_id/pages/domains/:domain' do
+ shared_examples_for 'delete pages domain' do
+ it 'deletes a pages domain' do
+ delete api(route_domain, user)
+
+ expect(response).to have_gitlab_http_status(204)
+ end
+ end
+
+ context 'when domain is vacant' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like '404 response' do
+ let(:request) { delete api(route_vacant_domain, user) }
+ end
+ end
+
+ context 'when user is a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'delete pages domain'
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { delete api(route_domain, user) }
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { delete api(route_domain, user) }
+ end
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ it_behaves_like '403 response' do
+ let(:request) { delete api(route_domain, user) }
+ end
+ end
+
+ context 'when user is not a member' do
+ it_behaves_like '404 response' do
+ let(:request) { delete api(route_domain, user) }
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb
index f650df57383..7ea25059756 100644
--- a/spec/requests/api/pipeline_schedules_spec.rb
+++ b/spec/requests/api/pipeline_schedules_spec.rb
@@ -20,7 +20,7 @@ describe API::PipelineSchedules do
it 'returns list of pipeline_schedules' do
get api("/projects/#{project.id}/pipeline_schedules", developer)
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('pipeline_schedules')
end
@@ -67,7 +67,7 @@ describe API::PipelineSchedules do
it 'does not return pipeline_schedules list' do
get api("/projects/#{project.id}/pipeline_schedules", user)
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -75,7 +75,7 @@ describe API::PipelineSchedules do
it 'does not return pipeline_schedules list' do
get api("/projects/#{project.id}/pipeline_schedules")
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
@@ -91,14 +91,14 @@ describe API::PipelineSchedules do
it 'returns pipeline_schedule details' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('pipeline_schedule')
end
it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do
get api("/projects/#{project.id}/pipeline_schedules/-5", developer)
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -106,7 +106,7 @@ describe API::PipelineSchedules do
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)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -118,7 +118,7 @@ describe API::PipelineSchedules do
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)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -126,7 +126,7 @@ describe API::PipelineSchedules do
it 'does not return pipeline_schedules list' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
@@ -142,7 +142,7 @@ describe API::PipelineSchedules do
params
end.to change { project.pipeline_schedules.count }.by(1)
- expect(response).to have_http_status(:created)
+ expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('pipeline_schedule')
expect(json_response['description']).to eq(params[:description])
expect(json_response['ref']).to eq(params[:ref])
@@ -156,7 +156,7 @@ describe API::PipelineSchedules do
it 'does not create pipeline_schedule' do
post api("/projects/#{project.id}/pipeline_schedules", developer)
- expect(response).to have_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
end
end
@@ -165,7 +165,7 @@ describe API::PipelineSchedules do
post api("/projects/#{project.id}/pipeline_schedules", developer),
params.merge('cron' => 'invalid-cron')
- expect(response).to have_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to have_key('cron')
end
end
@@ -175,7 +175,7 @@ describe API::PipelineSchedules do
it 'does not create pipeline_schedule' do
post api("/projects/#{project.id}/pipeline_schedules", user), params
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -183,7 +183,7 @@ describe API::PipelineSchedules do
it 'does not create pipeline_schedule' do
post api("/projects/#{project.id}/pipeline_schedules"), params
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
@@ -198,7 +198,7 @@ describe API::PipelineSchedules do
put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer),
cron: '1 2 3 4 *'
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('pipeline_schedule')
expect(json_response['cron']).to eq('1 2 3 4 *')
end
@@ -208,7 +208,7 @@ describe API::PipelineSchedules do
put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer),
cron: 'invalid-cron'
- expect(response).to have_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to have_key('cron')
end
end
@@ -218,7 +218,7 @@ describe API::PipelineSchedules do
it 'does not update pipeline_schedule' do
put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -226,7 +226,7 @@ describe API::PipelineSchedules do
it 'does not update pipeline_schedule' do
put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
@@ -240,7 +240,7 @@ describe API::PipelineSchedules do
it 'updates owner' do
post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", developer)
- expect(response).to have_http_status(:created)
+ expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('pipeline_schedule')
end
end
@@ -249,7 +249,7 @@ describe API::PipelineSchedules do
it 'does not update owner' do
post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", user)
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -257,7 +257,7 @@ describe API::PipelineSchedules do
it 'does not update owner' do
post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership")
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
@@ -279,13 +279,13 @@ 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(204)
+ expect(response).to have_gitlab_http_status(204)
end
it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do
delete api("/projects/#{project.id}/pipeline_schedules/-5", master)
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
it_behaves_like '412 response' do
@@ -299,7 +299,7 @@ describe API::PipelineSchedules do
it 'does not delete pipeline_schedule' do
delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
- expect(response).to have_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
@@ -307,7 +307,7 @@ describe API::PipelineSchedules do
it 'does not delete pipeline_schedule' do
delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
@@ -327,7 +327,7 @@ describe API::PipelineSchedules do
params
end.to change { pipeline_schedule.variables.count }.by(1)
- expect(response).to have_http_status(:created)
+ expect(response).to have_gitlab_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])
@@ -338,7 +338,7 @@ describe API::PipelineSchedules 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)
+ expect(response).to have_gitlab_http_status(:bad_request)
end
end
@@ -347,7 +347,7 @@ describe API::PipelineSchedules 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(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to have_key('key')
end
end
@@ -357,7 +357,7 @@ describe API::PipelineSchedules 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)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -365,7 +365,7 @@ describe API::PipelineSchedules 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)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
@@ -384,7 +384,7 @@ describe API::PipelineSchedules 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 have_gitlab_http_status(:ok)
expect(response).to match_response_schema('pipeline_schedule_variable')
expect(json_response['value']).to eq('updated_value')
end
@@ -394,7 +394,7 @@ describe API::PipelineSchedules 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)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -402,7 +402,7 @@ describe API::PipelineSchedules 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)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
@@ -428,14 +428,14 @@ describe API::PipelineSchedules 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 have_gitlab_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)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -445,7 +445,7 @@ describe API::PipelineSchedules do
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)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
@@ -453,7 +453,7 @@ describe API::PipelineSchedules 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)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 258085e503f..e4dcc9252fa 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -19,7 +19,7 @@ describe API::Pipelines do
it 'returns project pipelines' do
get api("/projects/#{project.id}/pipelines", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['sha']).to match /\A\h{40}\z/
@@ -37,7 +37,7 @@ describe API::Pipelines do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), scope: target
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
json_response.each { |r| expect(r['status']).to eq(target) }
@@ -55,7 +55,7 @@ describe API::Pipelines do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), scope: 'finished'
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
json_response.each { |r| expect(r['status']).to be_in(%w[success failed canceled]) }
@@ -70,7 +70,7 @@ describe API::Pipelines do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), scope: 'branches'
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
expect(json_response.last['id']).to eq(pipeline_branch.id)
@@ -81,7 +81,7 @@ describe API::Pipelines do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), scope: 'tags'
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
expect(json_response.last['id']).to eq(pipeline_tag.id)
@@ -93,7 +93,7 @@ describe API::Pipelines do
it 'returns bad_request' do
get api("/projects/#{project.id}/pipelines", user), scope: 'invalid-scope'
- expect(response).to have_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
end
end
@@ -108,7 +108,7 @@ describe API::Pipelines do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), status: target
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
json_response.each { |r| expect(r['status']).to eq(target) }
@@ -120,7 +120,7 @@ describe API::Pipelines do
it 'returns bad_request' do
get api("/projects/#{project.id}/pipelines", user), status: 'invalid-status'
- expect(response).to have_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
end
end
@@ -133,7 +133,7 @@ describe API::Pipelines do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), ref: 'master'
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
json_response.each { |r| expect(r['ref']).to eq('master') }
@@ -144,7 +144,7 @@ describe API::Pipelines do
it 'returns empty' do
get api("/projects/#{project.id}/pipelines", user), ref: 'invalid-ref'
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_empty
end
@@ -158,7 +158,7 @@ describe API::Pipelines do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), name: user.name
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response.first['id']).to eq(pipeline.id)
end
@@ -168,7 +168,7 @@ describe API::Pipelines do
it 'returns empty' do
get api("/projects/#{project.id}/pipelines", user), name: 'invalid-name'
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_empty
end
@@ -182,7 +182,7 @@ describe API::Pipelines do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), username: user.username
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response.first['id']).to eq(pipeline.id)
end
@@ -192,7 +192,7 @@ describe API::Pipelines do
it 'returns empty' do
get api("/projects/#{project.id}/pipelines", user), username: 'invalid-username'
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_empty
end
@@ -207,7 +207,7 @@ describe API::Pipelines do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), yaml_errors: true
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response.first['id']).to eq(pipeline1.id)
end
@@ -217,7 +217,7 @@ describe API::Pipelines do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), yaml_errors: false
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response.first['id']).to eq(pipeline2.id)
end
@@ -227,7 +227,7 @@ describe API::Pipelines do
it 'returns bad_request' do
get api("/projects/#{project.id}/pipelines", user), yaml_errors: 'invalid-yaml_errors'
- expect(response).to have_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
@@ -244,7 +244,7 @@ describe API::Pipelines do
it 'sorts as user_id: :desc' do
get api("/projects/#{project.id}/pipelines", user), order_by: 'user_id', sort: 'desc'
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
@@ -257,7 +257,7 @@ describe API::Pipelines do
it 'returns bad_request' do
get api("/projects/#{project.id}/pipelines", user), order_by: 'user_id', sort: 'invalid_sort'
- expect(response).to have_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
@@ -266,7 +266,7 @@ describe API::Pipelines do
it 'returns bad_request' do
get api("/projects/#{project.id}/pipelines", user), order_by: 'lock_version', sort: 'asc'
- expect(response).to have_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
@@ -277,7 +277,7 @@ describe API::Pipelines do
it 'does not return project pipelines' do
get api("/projects/#{project.id}/pipelines", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response).not_to be_an Array
end
@@ -296,7 +296,7 @@ describe API::Pipelines do
post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
end.to change { Ci::Pipeline.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to be_a Hash
expect(json_response['sha']).to eq project.commit.id
end
@@ -304,7 +304,7 @@ describe API::Pipelines do
it 'fails when using an invalid ref' do
post api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['base'].first).to eq 'Reference not found'
expect(json_response).not_to be_an Array
end
@@ -314,7 +314,7 @@ describe API::Pipelines do
it 'fails to create pipeline' do
post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file'
expect(json_response).not_to be_an Array
end
@@ -325,7 +325,7 @@ describe API::Pipelines do
it 'does not create pipeline' do
post api("/projects/#{project.id}/pipeline", non_member), ref: project.default_branch
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response).not_to be_an Array
end
@@ -337,14 +337,14 @@ describe API::Pipelines do
it 'returns project pipelines' do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['sha']).to match /\A\h{40}\z/
end
it 'returns 404 when it does not exist' do
get api("/projects/#{project.id}/pipelines/123456", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq '404 Not found'
expect(json_response['id']).to be nil
end
@@ -366,7 +366,7 @@ describe API::Pipelines do
it 'should not return a project pipeline' do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response['id']).to be nil
end
@@ -387,7 +387,7 @@ describe API::Pipelines do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
end.to change { pipeline.builds.count }.from(1).to(2)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(build.reload.retried?).to be true
end
end
@@ -396,7 +396,7 @@ describe API::Pipelines do
it 'should not return a project pipeline' do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response['id']).to be nil
end
@@ -415,7 +415,7 @@ describe API::Pipelines do
it 'retries failed builds' do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to eq('canceled')
end
end
@@ -430,7 +430,7 @@ describe API::Pipelines do
it 'rejects the action' do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(pipeline.reload.status).to eq('pending')
end
end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index ac3bab09c4c..f31344a6238 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -22,7 +22,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "returns project hooks" do
get api("/projects/#{project.id}/hooks", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response).to include_pagination_headers
expect(json_response.count).to eq(1)
@@ -43,7 +43,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "does not access project hooks" do
get api("/projects/#{project.id}/hooks", user3)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -53,7 +53,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "returns a project hook" do
get api("/projects/#{project.id}/hooks/#{hook.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['url']).to eq(hook.url)
expect(json_response['issues_events']).to eq(hook.issues_events)
expect(json_response['push_events']).to eq(hook.push_events)
@@ -69,20 +69,20 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "returns a 404 error if hook id is not available" do
get api("/projects/#{project.id}/hooks/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
context "unauthorized user" do
it "does not access an existing hook" do
get api("/projects/#{project.id}/hooks/#{hook.id}", user3)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
it "returns a 404 error if hook id is not available" do
get api("/projects/#{project.id}/hooks/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -94,7 +94,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
job_events: true
end.to change {project.hooks.count}.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['url']).to eq('http://example.com')
expect(json_response['issues_events']).to eq(true)
expect(json_response['push_events']).to eq(true)
@@ -115,7 +115,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
post api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token
end.to change {project.hooks.count}.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response["url"]).to eq("http://example.com")
expect(json_response).not_to include("token")
@@ -127,12 +127,12 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "returns a 400 error if url not given" do
post api("/projects/#{project.id}/hooks", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 422 error if url not valid" do
post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com"
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
end
@@ -141,7 +141,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
put api("/projects/#{project.id}/hooks/#{hook.id}", user),
url: 'http://example.org', push_events: false, job_events: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['url']).to eq('http://example.org')
expect(json_response['issues_events']).to eq(hook.issues_events)
expect(json_response['push_events']).to eq(false)
@@ -159,7 +159,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["url"]).to eq("http://example.org")
expect(json_response).not_to include("token")
@@ -169,17 +169,17 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "returns 404 error if hook id not found" do
put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 400 error if url is not given" do
put api("/projects/#{project.id}/hooks/#{hook.id}", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 422 error if url is not valid" do
put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com'
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
end
@@ -188,19 +188,19 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect do
delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change {project.hooks.count}.by(-1)
end
it "returns a 404 error when deleting non existent hook" do
delete api("/projects/#{project.id}/hooks/42", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns a 404 error if hook id not given" do
delete api("/projects/#{project.id}/hooks", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
@@ -209,7 +209,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
other_project.team << [test_user, :master]
delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(WebHook.exists?(hook.id)).to be_truthy
end
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index db34149eb73..e741ac4b7bd 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -12,7 +12,7 @@ describe API::ProjectSnippets do
it 'exposes known attributes' do
get api("/projects/#{project.id}/snippets/#{snippet.id}/user_agent_detail", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
expect(json_response['ip_address']).to eq(user_agent_detail.ip_address)
expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
@@ -21,7 +21,7 @@ describe API::ProjectSnippets do
it "returns unautorized for non-admin users" do
get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/user_agent_detail", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -36,7 +36,7 @@ describe API::ProjectSnippets do
get api("/projects/#{project.id}/snippets", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
@@ -49,7 +49,7 @@ describe API::ProjectSnippets do
get api("/projects/#{project.id}/snippets/", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(0)
@@ -63,7 +63,7 @@ describe API::ProjectSnippets do
it 'returns snippet json' do
get api("/projects/#{project.id}/snippets/#{snippet.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(snippet.title)
expect(json_response['description']).to eq(snippet.description)
@@ -73,7 +73,7 @@ describe API::ProjectSnippets do
it 'returns 404 for invalid snippet id' do
get api("/projects/#{project.id}/snippets/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
end
@@ -92,7 +92,7 @@ describe API::ProjectSnippets do
it 'creates a new snippet' do
post api("/projects/#{project.id}/snippets/", admin), params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
snippet = ProjectSnippet.find(json_response['id'])
expect(snippet.content).to eq(params[:code])
expect(snippet.description).to eq(params[:description])
@@ -106,7 +106,7 @@ describe API::ProjectSnippets do
post api("/projects/#{project.id}/snippets/", admin), params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context 'when the snippet is spam' do
@@ -132,7 +132,7 @@ describe API::ProjectSnippets do
expect { create_snippet(project, visibility: 'public') }
.not_to change { Snippet.count }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
end
@@ -154,7 +154,7 @@ describe API::ProjectSnippets do
put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content, description: new_description
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
snippet.reload
expect(snippet.content).to eq(new_content)
expect(snippet.description).to eq(new_description)
@@ -163,14 +163,14 @@ describe API::ProjectSnippets do
it 'returns 404 for invalid snippet id' do
put api("/projects/#{snippet.project.id}/snippets/1234", admin), title: 'foo'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
it 'returns 400 for missing parameters' do
put api("/projects/#{project.id}/snippets/1234", admin)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context 'when the snippet is spam' do
@@ -212,7 +212,7 @@ describe API::ProjectSnippets do
expect { update_snippet(title: 'Foo', visibility: 'public') }
.not_to change { snippet.reload.title }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
end
@@ -230,13 +230,13 @@ describe API::ProjectSnippets do
it 'deletes snippet' do
delete api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end
it 'returns 404 for invalid snippet id' do
delete api("/projects/#{snippet.project.id}/snippets/1234", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
@@ -251,7 +251,7 @@ describe API::ProjectSnippets do
it 'returns raw text' do
get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.content_type).to eq 'text/plain'
expect(response.body).to eq(snippet.content)
end
@@ -259,7 +259,7 @@ describe API::ProjectSnippets do
it 'returns 404 for invalid snippet id' do
get api("/projects/#{snippet.project.id}/snippets/1234/raw", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 5964244f8c5..a41345da05b 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -45,11 +45,17 @@ describe API::Projects do
it 'returns an array of projects' do
get api('/projects', current_user), filter
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id))
end
+
+ it 'returns the proper security headers' do
+ get api('/projects', current_user), filter
+
+ expect(response).to include_security_headers
+ end
end
shared_examples_for 'projects response without N + 1 queries' do
@@ -147,7 +153,7 @@ describe API::Projects do
it "does not include statistics by default" do
get api('/projects', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
@@ -156,7 +162,7 @@ describe API::Projects do
it "includes statistics if requested" do
get api('/projects', user), statistics: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first).to include 'statistics'
@@ -201,7 +207,7 @@ describe API::Projects do
get api('/projects?simple=true', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first.keys).to match_array expected_keys
@@ -228,7 +234,7 @@ describe API::Projects do
it 'filters based on private visibility param' do
get api('/projects', user), { visibility: 'private' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).to contain_exactly(project.id, project2.id, project3.id)
@@ -239,7 +245,7 @@ describe API::Projects do
get api('/projects', user), { visibility: 'internal' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).to contain_exactly(project2.id)
@@ -248,7 +254,7 @@ describe API::Projects do
it 'filters based on public visibility param' do
get api('/projects', user), { visibility: 'public' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).to contain_exactly(public_project.id)
@@ -259,7 +265,7 @@ describe API::Projects do
it 'returns the correct order when sorted by id' do
get api('/projects', user), { order_by: 'id', sort: 'desc' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(project3.id)
@@ -270,7 +276,7 @@ describe API::Projects do
it 'returns an array of projects the user owns' do
get api('/projects', user4), owned: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(project4.name)
@@ -289,7 +295,7 @@ describe API::Projects do
it 'returns the starred projects viewable by the user' do
get api('/projects', user3), starred: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id)
@@ -311,7 +317,7 @@ describe API::Projects do
it 'returns only projects that satisfy all query parameters' do
get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
@@ -330,7 +336,7 @@ describe API::Projects do
it 'returns only projects that satisfy all query parameters' do
get api('/projects', user), { visibility: 'public', membership: true, starred: true, search: 'gitlab' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
@@ -363,14 +369,14 @@ describe API::Projects do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0)
expect { post api('/projects', user2), name: 'foo' }
.to change {Project.count}.by(0)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
it 'creates new project without path but with name and returns 201' do
expect { post api('/projects', user), name: 'Foo Project' }
.to change { Project.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
project = Project.first
@@ -381,7 +387,7 @@ describe API::Projects do
it 'creates new project without name but with path and returns 201' do
expect { post api('/projects', user), path: 'foo_project' }
.to change { Project.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
project = Project.first
@@ -392,7 +398,7 @@ describe API::Projects do
it 'creates new project with name and path and returns 201' do
expect { post api('/projects', user), path: 'path-project-Foo', name: 'Foo Project' }
.to change { Project.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
project = Project.first
@@ -403,12 +409,12 @@ describe API::Projects do
it 'creates last project before reaching project limit' do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
post api('/projects', user2), name: 'foo'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
it 'does not create new project without name or path and returns 400' do
expect { post api('/projects', user) }.not_to change { Project.count }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "assigns attributes to project" do
@@ -427,10 +433,11 @@ describe API::Projects do
post api('/projects', user), project
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
project.each_pair do |k, v|
next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
+
expect(json_response[k.to_s]).to eq(v)
end
@@ -543,7 +550,7 @@ describe API::Projects do
post api('/projects', user), project
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
context 'when a visibility level is restricted' do
@@ -556,7 +563,7 @@ describe API::Projects do
it 'does not allow a non-admin to use a restricted visibility level' do
post api('/projects', user), project_param
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['visibility_level'].first).to(
match('restricted by your GitLab administrator')
)
@@ -576,14 +583,14 @@ describe API::Projects do
it 'returns error when user not found' do
get api('/users/9999/projects/')
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns projects filtered by user' do
get api("/users/#{user4.id}/projects/", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id)
@@ -597,7 +604,7 @@ describe API::Projects do
it 'creates new project without path but with name and return 201' do
expect { post api("/projects/user/#{user.id}", admin), name: 'Foo Project' }.to change {Project.count}.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
project = Project.last
@@ -608,7 +615,7 @@ describe API::Projects do
it 'creates new project with name and path and returns 201' do
expect { post api("/projects/user/#{user.id}", admin), path: 'path-project-Foo', name: 'Foo Project' }
.to change { Project.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
project = Project.last
@@ -620,7 +627,7 @@ describe API::Projects do
expect { post api("/projects/user/#{user.id}", admin) }
.not_to change { Project.count }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('name is missing')
end
@@ -634,9 +641,10 @@ describe API::Projects do
post api("/projects/user/#{user.id}", admin), project
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
project.each_pair do |k, v|
next if %i[has_external_issue_tracker path].include?(k)
+
expect(json_response[k.to_s]).to eq(v)
end
end
@@ -646,7 +654,7 @@ describe API::Projects do
post api("/projects/user/#{user.id}", admin), project
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['visibility']).to eq('public')
end
@@ -655,7 +663,7 @@ describe API::Projects do
post api("/projects/user/#{user.id}", admin), project
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['visibility']).to eq('internal')
end
@@ -720,7 +728,7 @@ describe API::Projects do
it "uploads the file and returns its info" do
post api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['alt']).to eq("dk")
expect(json_response['url']).to start_with("/uploads/")
expect(json_response['url']).to end_with("/dk.png")
@@ -734,7 +742,7 @@ describe API::Projects do
get api("/projects/#{public_project.id}")
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(public_project.id)
expect(json_response['description']).to eq(public_project.description)
expect(json_response['default_branch']).to eq(public_project.default_branch)
@@ -754,7 +762,7 @@ describe API::Projects do
get api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(project.id)
expect(json_response['description']).to eq(project.description)
expect(json_response['default_branch']).to eq(project.default_branch)
@@ -798,20 +806,20 @@ describe API::Projects do
it 'returns a project by path name' do
get api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(project.name)
end
it 'returns a 404 error if not found' do
get api('/projects/42', user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'returns a 404 error if user is not a member' do
other_user = create(:user)
get api("/projects/#{project.id}", other_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'handles users with dots' do
@@ -819,14 +827,14 @@ describe API::Projects do
project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
get api("/projects/#{CGI.escape(project.full_path)}", dot_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(project.name)
end
it 'exposes namespace fields' do
get api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['namespace']).to eq({
'id' => user.namespace.id,
'name' => user.namespace.name,
@@ -840,28 +848,28 @@ describe API::Projects do
it "does not include statistics by default" do
get api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).not_to include 'statistics'
end
it "includes statistics if requested" do
get api("/projects/#{project.id}", user), statistics: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to include 'statistics'
end
it "includes import_error if user can admin project" do
get api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to include("import_error")
end
it "does not include import_error if user cannot admin project" do
get api("/projects/#{project.id}", user3)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).not_to include("import_error")
end
@@ -906,7 +914,7 @@ describe API::Projects do
it 'contains permission information' do
get api("/projects", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.first['permissions']['project_access']['access_level'])
.to eq(Gitlab::Access::MASTER)
expect(json_response.first['permissions']['group_access']).to be_nil
@@ -918,7 +926,7 @@ describe API::Projects do
project.team << [user, :master]
get api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['permissions']['project_access']['access_level'])
.to eq(Gitlab::Access::MASTER)
expect(json_response['permissions']['group_access']).to be_nil
@@ -935,7 +943,7 @@ describe API::Projects do
it 'sets the owner and return 200' do
get api("/projects/#{project2.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['permissions']['project_access']).to be_nil
expect(json_response['permissions']['group_access']['access_level'])
.to eq(Gitlab::Access::OWNER)
@@ -952,7 +960,7 @@ describe API::Projects do
user = project.namespace.owner
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
@@ -981,7 +989,7 @@ describe API::Projects do
it 'returns a 404 error if not found' do
get api('/projects/42/users', user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
@@ -990,7 +998,7 @@ describe API::Projects do
get api("/projects/#{project.id}/users", other_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -1003,7 +1011,7 @@ describe API::Projects do
it 'returns an array of project snippets' do
get api("/projects/#{project.id}/snippets", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(snippet.title)
@@ -1013,13 +1021,13 @@ describe API::Projects do
describe 'GET /projects/:id/snippets/:snippet_id' do
it 'returns a project snippet' do
get api("/projects/#{project.id}/snippets/#{snippet.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(snippet.title)
end
it 'returns a 404 error if snippet id not found' do
get api("/projects/#{project.id}/snippets/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1027,7 +1035,7 @@ describe API::Projects do
it 'creates a new project snippet' do
post api("/projects/#{project.id}/snippets", user),
title: 'api test', file_name: 'sample.rb', code: 'test', visibility: 'private'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('api test')
end
@@ -1041,7 +1049,7 @@ describe API::Projects do
it 'updates an existing project snippet' do
put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
code: 'updated code'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('example')
expect(snippet.reload.content).to eq('updated code')
end
@@ -1049,7 +1057,7 @@ describe API::Projects do
it 'updates an existing project snippet with new title' do
put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
title: 'other api test'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('other api test')
end
end
@@ -1063,13 +1071,13 @@ describe API::Projects do
expect do
delete api("/projects/#{project.id}/snippets/#{snippet.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { Snippet.count }.by(-1)
end
it 'returns 404 when deleting unknown snippet id' do
delete api("/projects/#{project.id}/snippets/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it_behaves_like '412 response' do
@@ -1080,12 +1088,12 @@ describe API::Projects do
describe 'GET /projects/:id/snippets/:snippet_id/raw' do
it 'gets a raw project snippet' do
get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns a 404 error if raw project snippet not found' do
get api("/projects/#{project.id}/snippets/5555/raw", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1098,13 +1106,13 @@ describe API::Projects do
it "is not available for non admin users" do
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'allows project to be forked from an existing project' do
expect(project_fork_target.forked?).not_to be_truthy
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
expect(project_fork_target.forked_project_link).not_to be_nil
@@ -1121,7 +1129,7 @@ describe API::Projects do
it 'fails if forked_from project which does not exist' do
post api("/projects/#{project_fork_target.id}/fork/9999", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'fails with 409 if already forked' do
@@ -1129,7 +1137,7 @@ describe API::Projects do
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
post api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin)
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
expect(project_fork_target.forked?).to be_truthy
@@ -1139,7 +1147,7 @@ describe API::Projects do
describe 'DELETE /projects/:id/fork' do
it "is not visible to users outside group" do
delete api("/projects/#{project_fork_target.id}/fork", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context 'when users belong to project group' do
@@ -1161,7 +1169,7 @@ describe API::Projects do
it 'makes forked project unforked' do
delete api("/projects/#{project_fork_target.id}/fork", admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_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
@@ -1174,13 +1182,13 @@ describe API::Projects do
it 'is forbidden to non-owner users' do
delete api("/projects/#{project_fork_target.id}/fork", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'is idempotent if not forked' do
expect(project_fork_target.forked_from_project).to be_nil
delete api("/projects/#{project_fork_target.id}/fork", admin)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
expect(project_fork_target.reload.forked_from_project).to be_nil
end
end
@@ -1210,7 +1218,7 @@ describe API::Projects do
it 'returns the forks' do
get api("/projects/#{project_fork_source.id}/forks", member)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(1)
expect(json_response[0]['name']).to eq(private_fork.name)
@@ -1221,7 +1229,7 @@ describe API::Projects do
it 'returns an empty array' do
get api("/projects/#{project_fork_source.id}/forks", non_member)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(0)
end
@@ -1232,7 +1240,7 @@ describe API::Projects do
it 'returns an empty array' do
get api("/projects/#{project_fork_source.id}/forks")
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(0)
end
@@ -1250,7 +1258,7 @@ describe API::Projects do
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at
end.to change { ProjectGroupLink.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['group_id']).to eq(group.id)
expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER)
expect(json_response['expires_at']).to eq(expires_at.to_s)
@@ -1258,18 +1266,18 @@ describe API::Projects do
it "returns a 400 error when group id is not given" do
post api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 400 error when access level is not given" do
post api("/projects/#{project.id}/share", user), group_id: group.id
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 400 error when sharing is disabled" do
project.namespace.update(share_with_group_lock: true)
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns a 404 error when user cannot read group' do
@@ -1277,19 +1285,19 @@ describe API::Projects do
post api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 error when group does not exist' do
post api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns a 400 error when wrong params passed" do
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq 'group_access does not have a valid value'
end
end
@@ -1305,7 +1313,7 @@ describe API::Projects do
it 'returns 204 when deleting a group share' do
delete api("/projects/#{project.id}/share/#{group.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(project.project_group_links).to be_empty
end
@@ -1317,19 +1325,19 @@ describe API::Projects do
it 'returns a 400 when group id is not an integer' do
delete api("/projects/#{project.id}/share/foo", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns a 404 error when group link does not exist' do
delete api("/projects/#{project.id}/share/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 error when project does not exist' do
delete api("/projects/123/share/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1350,7 +1358,7 @@ describe API::Projects do
put api("/projects/#{project.id}", user), project_param
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to match('at least one parameter must be provided')
end
@@ -1360,7 +1368,7 @@ describe API::Projects do
put api("/projects/#{project.id}"), project_param
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -1370,7 +1378,7 @@ describe API::Projects do
put api("/projects/#{project.id}", user), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
@@ -1382,7 +1390,7 @@ describe API::Projects do
put api("/projects/#{project3.id}", user), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
@@ -1395,7 +1403,7 @@ describe API::Projects do
put api("/projects/#{project3.id}", user), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
@@ -1409,7 +1417,7 @@ describe API::Projects do
put api("/projects/#{project.id}", user), project_param
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['name']).to eq(['has already been taken'])
end
@@ -1418,7 +1426,7 @@ describe API::Projects do
put api("/projects/#{project.id}", user), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['request_access_enabled']).to eq(false)
end
@@ -1427,7 +1435,7 @@ describe API::Projects do
put api("/projects/#{project3.id}", user), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
@@ -1439,7 +1447,7 @@ describe API::Projects do
put api("/projects/#{project3.id}", user), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
@@ -1451,7 +1459,7 @@ describe API::Projects do
it 'updates path' do
project_param = { path: 'bar' }
put api("/projects/#{project3.id}", user4), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -1465,7 +1473,7 @@ describe API::Projects do
description: 'new description' }
put api("/projects/#{project3.id}", user4), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -1474,20 +1482,20 @@ describe API::Projects do
it 'does not update path to existing path' do
project_param = { path: project.path }
put api("/projects/#{project3.id}", user4), project_param
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['path']).to eq(['has already been taken'])
end
it 'does not update name' do
project_param = { name: 'bar' }
put api("/projects/#{project3.id}", user4), project_param
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'does not update visibility_level' do
project_param = { visibility: 'public' }
put api("/projects/#{project3.id}", user4), project_param
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -1501,7 +1509,7 @@ describe API::Projects do
description: 'new description',
request_access_enabled: true }
put api("/projects/#{project.id}", user3), project_param
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -1511,7 +1519,7 @@ describe API::Projects do
it 'archives the project' do
post api("/projects/#{project.id}/archive", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['archived']).to be_truthy
end
end
@@ -1524,7 +1532,7 @@ describe API::Projects do
it 'remains archived' do
post api("/projects/#{project.id}/archive", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['archived']).to be_truthy
end
end
@@ -1537,7 +1545,7 @@ describe API::Projects do
it 'rejects the action' do
post api("/projects/#{project.id}/archive", user3)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -1547,7 +1555,7 @@ describe API::Projects do
it 'remains unarchived' do
post api("/projects/#{project.id}/unarchive", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['archived']).to be_falsey
end
end
@@ -1560,7 +1568,7 @@ describe API::Projects do
it 'unarchives the project' do
post api("/projects/#{project.id}/unarchive", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['archived']).to be_falsey
end
end
@@ -1573,7 +1581,7 @@ describe API::Projects do
it 'rejects the action' do
post api("/projects/#{project.id}/unarchive", user3)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -1583,7 +1591,7 @@ describe API::Projects do
it 'stars the project' do
expect { post api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['star_count']).to eq(1)
end
end
@@ -1597,7 +1605,7 @@ describe API::Projects do
it 'does not modify the star count' do
expect { post api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
end
end
@@ -1612,7 +1620,7 @@ describe API::Projects do
it 'unstars the project' do
expect { post api("/projects/#{project.id}/unstar", user) }.to change { project.reload.star_count }.by(-1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['star_count']).to eq(0)
end
end
@@ -1621,7 +1629,7 @@ describe API::Projects do
it 'does not modify the star count' do
expect { post api("/projects/#{project.id}/unstar", user) }.not_to change { project.reload.star_count }
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
end
end
@@ -1631,7 +1639,7 @@ describe API::Projects do
it 'removes project' do
delete api("/projects/#{project.id}", user)
- expect(response).to have_http_status(202)
+ expect(response).to have_gitlab_http_status(202)
expect(json_response['message']).to eql('202 Accepted')
end
@@ -1644,17 +1652,17 @@ describe API::Projects do
user3 = create(:user)
project.team << [user3, :developer]
delete api("/projects/#{project.id}", user3)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'does not remove a non existing project' do
delete api('/projects/1328', user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'does not remove a project not attached to user' do
delete api("/projects/#{project.id}", user2)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1662,13 +1670,13 @@ describe API::Projects do
it 'removes any existing project' do
delete api("/projects/#{project.id}", admin)
- expect(response).to have_http_status(202)
+ expect(response).to have_gitlab_http_status(202)
expect(json_response['message']).to eql('202 Accepted')
end
it 'does not remove a non existing project' do
delete api('/projects/1328', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it_behaves_like '412 response' do
@@ -1697,7 +1705,7 @@ describe API::Projects do
it 'forks if user has sufficient access to project' do
post api("/projects/#{project.id}/fork", user2)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to eq(project.path)
expect(json_response['owner']['id']).to eq(user2.id)
@@ -1710,7 +1718,7 @@ describe API::Projects do
it 'forks if user is admin' do
post api("/projects/#{project.id}/fork", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to eq(project.path)
expect(json_response['owner']['id']).to eq(admin.id)
@@ -1724,14 +1732,14 @@ describe API::Projects do
new_user = create(:user)
post api("/projects/#{project.id}/fork", new_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'fails if forked project exists in the user namespace' do
post api("/projects/#{project.id}/fork", user)
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
expect(json_response['message']['name']).to eq(['has already been taken'])
expect(json_response['message']['path']).to eq(['has already been taken'])
end
@@ -1739,61 +1747,61 @@ describe API::Projects do
it 'fails if project to fork from does not exist' do
post api('/projects/424242/fork', user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'forks with explicit own user namespace id' do
post api("/projects/#{project.id}/fork", user2), namespace: user2.namespace.id
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['owner']['id']).to eq(user2.id)
end
it 'forks with explicit own user name as namespace' do
post api("/projects/#{project.id}/fork", user2), namespace: user2.username
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['owner']['id']).to eq(user2.id)
end
it 'forks to another user when admin' do
post api("/projects/#{project.id}/fork", admin), namespace: user2.username
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['owner']['id']).to eq(user2.id)
end
it 'fails if trying to fork to another user when not admin' do
post api("/projects/#{project.id}/fork", user2), namespace: admin.namespace.id
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'fails if trying to fork to non-existent namespace' do
post api("/projects/#{project.id}/fork", user2), namespace: 42424242
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Target Namespace Not Found')
end
it 'forks to owned group' do
post api("/projects/#{project.id}/fork", user2), namespace: group2.name
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['namespace']['name']).to eq(group2.name)
end
it 'fails to fork to not owned group' do
post api("/projects/#{project.id}/fork", user2), namespace: group.name
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'forks to not owned group when admin' do
post api("/projects/#{project.id}/fork", admin), namespace: group.name
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['namespace']['name']).to eq(group.name)
end
end
@@ -1802,7 +1810,7 @@ describe API::Projects do
it 'returns authentication error' do
post api("/projects/#{project.id}/fork")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
expect(json_response['message']).to eq('401 Unauthorized')
end
end
@@ -1821,7 +1829,7 @@ describe API::Projects do
post api("/projects/#{project.id}/housekeeping", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
context 'when housekeeping lease is taken' do
@@ -1830,7 +1838,7 @@ describe API::Projects do
post api("/projects/#{project.id}/housekeeping", user)
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
expect(json_response['message']).to match(/Somebody already triggered housekeeping for this project/)
end
end
@@ -1844,7 +1852,7 @@ describe API::Projects do
it 'returns forbidden error' do
post api("/projects/#{project.id}/housekeeping", user3)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -1852,8 +1860,13 @@ describe API::Projects do
it 'returns authentication error' do
post api("/projects/#{project.id}/housekeeping")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
+
+ it_behaves_like 'custom attributes endpoints', 'projects' do
+ let(:attributable) { project }
+ let(:other_attributable) { project2 }
+ end
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 1a0695615e3..9f2ff3b5af6 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -17,7 +17,7 @@ describe API::Repositories do
it 'returns the repository tree' do
get api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
@@ -106,7 +106,7 @@ describe API::Repositories do
it 'returns blob attributes as json' do
get api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['size']).to eq(111)
expect(json_response['encoding']).to eq("base64")
expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n")
@@ -165,7 +165,7 @@ describe API::Repositories do
get api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
context 'when sha does not exist' do
@@ -218,7 +218,7 @@ describe API::Repositories do
it 'returns the repository archive' do
get api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
repo_name = project.repository.name.gsub("\.git", "")
type, params = workhorse_send_data
@@ -230,7 +230,7 @@ describe API::Repositories do
it 'returns the repository archive archive.zip' do
get api("/projects/#{project.id}/repository/archive.zip", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
repo_name = project.repository.name.gsub("\.git", "")
type, params = workhorse_send_data
@@ -242,7 +242,7 @@ describe API::Repositories do
it 'returns the repository archive archive.tar.bz2' do
get api("/projects/#{project.id}/repository/archive.tar.bz2", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
repo_name = project.repository.name.gsub("\.git", "")
type, params = workhorse_send_data
@@ -293,7 +293,7 @@ describe API::Repositories do
it "compares branches" do
get api(route, current_user), from: 'master', to: 'feature'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
@@ -301,7 +301,7 @@ describe API::Repositories do
it "compares tags" do
get api(route, current_user), from: 'v1.0.0', to: 'v1.1.0'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
@@ -309,7 +309,7 @@ describe API::Repositories do
it "compares commits" do
get api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['commits']).to be_empty
expect(json_response['diffs']).to be_empty
expect(json_response['compare_same_ref']).to be_falsey
@@ -318,7 +318,7 @@ describe API::Repositories do
it "compares commits in reverse order" do
get api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
@@ -326,7 +326,7 @@ describe API::Repositories do
it "compares same refs" do
get api(route, current_user), from: 'master', to: 'master'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['commits']).to be_empty
expect(json_response['diffs']).to be_empty
expect(json_response['compare_same_ref']).to be_truthy
@@ -367,7 +367,7 @@ describe API::Repositories do
it 'returns valid data' do
get api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 5068df5b43a..47f4ccd4887 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -16,7 +16,7 @@ describe API::Runner do
it 'returns 400 error' do
post api('/runners')
- expect(response).to have_http_status 400
+ expect(response).to have_gitlab_http_status 400
end
end
@@ -24,7 +24,7 @@ describe API::Runner do
it 'returns 403 error' do
post api('/runners'), token: 'invalid'
- expect(response).to have_http_status 403
+ expect(response).to have_gitlab_http_status 403
end
end
@@ -34,7 +34,7 @@ describe API::Runner do
runner = Ci::Runner.first
- expect(response).to have_http_status 201
+ expect(response).to have_gitlab_http_status 201
expect(json_response['id']).to eq(runner.id)
expect(json_response['token']).to eq(runner.token)
expect(runner.run_untagged).to be true
@@ -47,7 +47,7 @@ describe API::Runner do
it 'creates runner' do
post api('/runners'), token: project.runners_token
- expect(response).to have_http_status 201
+ expect(response).to have_gitlab_http_status 201
expect(project.runners.size).to eq(1)
expect(Ci::Runner.first.token).not_to eq(registration_token)
expect(Ci::Runner.first.token).not_to eq(project.runners_token)
@@ -60,7 +60,7 @@ describe API::Runner do
post api('/runners'), token: registration_token,
description: 'server.hostname'
- expect(response).to have_http_status 201
+ expect(response).to have_gitlab_http_status 201
expect(Ci::Runner.first.description).to eq('server.hostname')
end
end
@@ -70,7 +70,7 @@ describe API::Runner do
post api('/runners'), token: registration_token,
tag_list: 'tag1, tag2'
- expect(response).to have_http_status 201
+ expect(response).to have_gitlab_http_status 201
expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
end
end
@@ -82,7 +82,7 @@ describe API::Runner do
run_untagged: false,
tag_list: ['tag']
- expect(response).to have_http_status 201
+ expect(response).to have_gitlab_http_status 201
expect(Ci::Runner.first.run_untagged).to be false
expect(Ci::Runner.first.tag_list.sort).to eq(['tag'])
end
@@ -93,7 +93,7 @@ describe API::Runner do
post api('/runners'), token: registration_token,
run_untagged: false
- expect(response).to have_http_status 404
+ expect(response).to have_gitlab_http_status 404
end
end
end
@@ -103,7 +103,7 @@ describe API::Runner do
post api('/runners'), token: registration_token,
locked: true
- expect(response).to have_http_status 201
+ expect(response).to have_gitlab_http_status 201
expect(Ci::Runner.first.locked).to be true
end
end
@@ -116,7 +116,7 @@ describe API::Runner do
post api('/runners'), token: registration_token,
info: { param => value }
- expect(response).to have_http_status 201
+ expect(response).to have_gitlab_http_status 201
expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value)
end
end
@@ -128,7 +128,7 @@ describe API::Runner do
it 'returns 400 error' do
delete api('/runners')
- expect(response).to have_http_status 400
+ expect(response).to have_gitlab_http_status 400
end
end
@@ -136,7 +136,7 @@ describe API::Runner do
it 'returns 403 error' do
delete api('/runners'), token: 'invalid'
- expect(response).to have_http_status 403
+ expect(response).to have_gitlab_http_status 403
end
end
@@ -146,7 +146,7 @@ describe API::Runner do
it 'deletes Runner' do
delete api('/runners'), token: runner.token
- expect(response).to have_http_status 204
+ expect(response).to have_gitlab_http_status 204
expect(Ci::Runner.count).to eq(0)
end
@@ -164,7 +164,7 @@ describe API::Runner do
it 'returns 400 error' do
post api('/runners/verify')
- expect(response).to have_http_status :bad_request
+ expect(response).to have_gitlab_http_status :bad_request
end
end
@@ -172,7 +172,7 @@ describe API::Runner do
it 'returns 403 error' do
post api('/runners/verify'), token: 'invalid-token'
- expect(response).to have_http_status 403
+ expect(response).to have_gitlab_http_status 403
end
end
@@ -180,7 +180,7 @@ describe API::Runner do
it 'verifies Runner credentials' do
post api('/runners/verify'), token: runner.token
- expect(response).to have_http_status 200
+ expect(response).to have_gitlab_http_status 200
end
end
end
@@ -216,7 +216,7 @@ describe API::Runner do
context 'when runner sends version in User-Agent' do
context 'for stable version' do
it 'gives 204 and set X-GitLab-Last-Update' do
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(response.header).to have_key('X-GitLab-Last-Update')
end
end
@@ -225,7 +225,7 @@ describe API::Runner do
let(:last_update) { runner.ensure_runner_queue_value }
it 'gives 204 and set the same X-GitLab-Last-Update' do
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(response.header['X-GitLab-Last-Update']).to eq(last_update)
end
end
@@ -235,7 +235,7 @@ describe API::Runner do
let(:new_update) { runner.tick_runner_queue }
it 'gives 204 and set a new X-GitLab-Last-Update' do
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(response.header['X-GitLab-Last-Update']).to eq(new_update)
end
end
@@ -243,19 +243,19 @@ describe API::Runner do
context 'when beta version is sent' do
let(:user_agent) { 'gitlab-runner 9.0.0~beta.167.g2b2bacc (master; go1.7.4; linux/amd64)' }
- it { expect(response).to have_http_status(204) }
+ it { expect(response).to have_gitlab_http_status(204) }
end
context 'when pre-9-0 version is sent' do
let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0 (1-6-stable; go1.6.3; linux/amd64)' }
- it { expect(response).to have_http_status(204) }
+ it { expect(response).to have_gitlab_http_status(204) }
end
context 'when pre-9-0 beta version is sent' do
let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (master; go1.6.3; linux/amd64)' }
- it { expect(response).to have_http_status(204) }
+ it { expect(response).to have_gitlab_http_status(204) }
end
end
end
@@ -264,7 +264,7 @@ describe API::Runner do
it 'returns 400 error' do
post api('/jobs/request')
- expect(response).to have_http_status 400
+ expect(response).to have_gitlab_http_status 400
end
end
@@ -272,7 +272,7 @@ describe API::Runner do
it 'returns 403 error' do
post api('/jobs/request'), token: 'invalid'
- expect(response).to have_http_status 403
+ expect(response).to have_gitlab_http_status 403
end
end
@@ -283,7 +283,7 @@ describe API::Runner do
it 'returns 204 error' do
request_job
- expect(response).to have_http_status 204
+ expect(response).to have_gitlab_http_status 204
end
end
@@ -365,7 +365,7 @@ describe API::Runner do
it 'picks a job' do
request_job info: { platform: :darwin }
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(response.headers).not_to have_key('X-GitLab-Last-Update')
expect(runner.reload.platform).to eq('darwin')
expect(json_response['id']).to eq(job.id)
@@ -385,12 +385,12 @@ describe API::Runner do
end
context 'when job is made for tag' do
- let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
it 'sets branch as ref_type' do
request_job
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['git_info']['ref_type']).to eq('tag')
end
end
@@ -399,7 +399,7 @@ describe API::Runner do
it 'sets tag as ref_type' do
request_job
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['git_info']['ref_type']).to eq('branch')
end
end
@@ -415,7 +415,7 @@ describe API::Runner do
it "updates provided Runner's parameter" do
request_job info: { param => value }
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(runner.reload.read_attribute(param.to_sym)).to eq(value)
end
end
@@ -430,14 +430,14 @@ describe API::Runner do
it 'returns a conflict' do
request_job
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
expect(response.headers).not_to have_key('X-GitLab-Last-Update')
end
end
context 'when project and pipeline have multiple jobs' do
- let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
- let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
before do
@@ -448,7 +448,7 @@ describe API::Runner do
it 'returns dependent jobs' do
request_job
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['id']).to eq(test_job.id)
expect(json_response['dependencies'].count).to eq(2)
expect(json_response['dependencies']).to include(
@@ -458,7 +458,7 @@ describe API::Runner do
end
context 'when pipeline have jobs with artifacts' do
- let!(:job) { create(:ci_build_tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
before do
@@ -468,7 +468,7 @@ describe API::Runner do
it 'returns dependent jobs' do
request_job
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['id']).to eq(test_job.id)
expect(json_response['dependencies'].count).to eq(1)
expect(json_response['dependencies']).to include(
@@ -478,8 +478,8 @@ describe API::Runner do
end
context 'when explicit dependencies are defined' do
- let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
- let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) do
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
stage: 'deploy', stage_idx: 1,
@@ -494,7 +494,7 @@ describe API::Runner do
it 'returns dependent jobs' do
request_job
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['id']).to eq(test_job.id)
expect(json_response['dependencies'].count).to eq(1)
expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token)
@@ -502,8 +502,8 @@ describe API::Runner do
end
context 'when dependencies is an empty array' do
- let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
- let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:empty_dependencies_job) do
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
stage: 'deploy', stage_idx: 1,
@@ -518,7 +518,7 @@ describe API::Runner do
it 'returns an empty array' do
request_job
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['id']).to eq(empty_dependencies_job.id)
expect(json_response['dependencies'].count).to eq(0)
end
@@ -537,7 +537,7 @@ describe API::Runner do
it 'picks job' do
request_job
- expect(response).to have_http_status 201
+ expect(response).to have_gitlab_http_status 201
end
end
@@ -571,7 +571,7 @@ describe API::Runner do
it 'returns variables for triggers' do
request_job
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['variables']).to include(*expected_variables)
end
end
@@ -683,7 +683,7 @@ describe API::Runner do
it 'updates a running build' do
update_job(trace: 'BUILD TRACE UPDATED')
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(job.reload.trace.raw).to eq 'BUILD TRACE UPDATED'
end
end
@@ -702,7 +702,7 @@ describe API::Runner do
it 'responds with forbidden' do
update_job
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -871,7 +871,7 @@ describe API::Runner do
it 'authorizes posting artifacts to running job' do
authorize_artifacts_with_token_in_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response['TempPath']).not_to be_nil
end
@@ -881,7 +881,7 @@ describe API::Runner do
authorize_artifacts_with_token_in_params(filesize: 100)
- expect(response).to have_http_status(413)
+ expect(response).to have_gitlab_http_status(413)
end
end
@@ -889,7 +889,7 @@ describe API::Runner do
it 'authorizes posting artifacts to running job' do
authorize_artifacts_with_token_in_headers
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response['TempPath']).not_to be_nil
end
@@ -899,7 +899,7 @@ describe API::Runner do
authorize_artifacts_with_token_in_headers(filesize: 100)
- expect(response).to have_http_status(413)
+ expect(response).to have_gitlab_http_status(413)
end
end
@@ -907,7 +907,7 @@ describe API::Runner do
it 'fails to authorize artifacts posting' do
authorize_artifacts(token: job.project.runners_token)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -916,14 +916,14 @@ describe API::Runner do
authorize_artifacts
- expect(response).to have_http_status(500)
+ expect(response).to have_gitlab_http_status(500)
end
context 'authorization token is invalid' do
it 'responds with forbidden' do
authorize_artifacts(token: 'invalid', filesize: 100 )
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -958,14 +958,14 @@ describe API::Runner do
it 'responds with forbidden' do
upload_artifacts(file_upload, headers_with_token)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
context 'when job is running' do
shared_examples 'successful artifacts upload' do
it 'updates successfully' do
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
end
@@ -998,7 +998,7 @@ describe API::Runner do
it 'responds with forbidden' do
upload_artifacts(file_upload, headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.project.runners_token))
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -1009,7 +1009,7 @@ describe API::Runner do
upload_artifacts(file_upload, headers_with_token)
- expect(response).to have_http_status(413)
+ expect(response).to have_gitlab_http_status(413)
end
end
@@ -1017,7 +1017,7 @@ describe API::Runner do
it 'fails to post artifacts without file' do
post api("/jobs/#{job.id}/artifacts"), {}, headers_with_token
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -1025,7 +1025,7 @@ describe API::Runner do
it 'fails to post artifacts without GitLab-Workhorse' do
post api("/jobs/#{job.id}/artifacts"), { token: job.token }, {}
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -1047,7 +1047,7 @@ describe API::Runner do
let(:expire_in) { '7 days' }
it 'updates when specified' do
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(7.days.from_now)
end
end
@@ -1056,7 +1056,7 @@ describe API::Runner do
let(:expire_in) { nil }
it 'ignores if not specified' do
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(job.reload.artifacts_expire_at).to be_nil
end
@@ -1065,7 +1065,7 @@ describe API::Runner do
let(:default_artifacts_expire_in) { '5 days' }
it 'sets to application default' do
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(5.days.from_now)
end
end
@@ -1074,7 +1074,7 @@ describe API::Runner do
let(:default_artifacts_expire_in) { '0' }
it 'does not set expire_in' do
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(job.reload.artifacts_expire_at).to be_nil
end
end
@@ -1103,7 +1103,7 @@ describe API::Runner do
end
it 'stores artifacts and artifacts metadata' do
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
expect(stored_artifacts_size).to eq(71759)
@@ -1116,7 +1116,7 @@ describe API::Runner do
end
it 'is expected to respond with bad request' do
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'does not store metadata' do
@@ -1141,7 +1141,7 @@ describe API::Runner do
it' "fails to post artifacts for outside of tmp path"' do
upload_artifacts(file_upload, headers_with_token)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -1171,7 +1171,7 @@ describe API::Runner do
context 'when using job token' do
it 'download artifacts' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.headers).to include download_headers
end
end
@@ -1180,14 +1180,14 @@ describe API::Runner do
let(:token) { job.project.runners_token }
it 'responds with forbidden' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
context 'when job does not has artifacts' do
it 'responds with not found' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 67907579225..ec5cad4f4fd 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -37,7 +37,7 @@ describe API::Runners do
get api('/runners', user)
shared = json_response.any? { |r| r['is_shared'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_falsey
@@ -47,7 +47,7 @@ describe API::Runners do
get api('/runners?scope=active', user)
shared = json_response.any? { |r| r['is_shared'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_falsey
@@ -55,7 +55,7 @@ describe API::Runners do
it 'avoids filtering if scope is invalid' do
get api('/runners?scope=unknown', user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -63,7 +63,7 @@ describe API::Runners do
it 'does not return runners' do
get api('/runners')
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -75,7 +75,7 @@ describe API::Runners do
get api('/runners/all', admin)
shared = json_response.any? { |r| r['is_shared'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_truthy
@@ -86,7 +86,7 @@ describe API::Runners do
it 'does not return runners list' do
get api('/runners/all', user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -94,7 +94,7 @@ describe API::Runners do
get api('/runners/all?scope=specific', admin)
shared = json_response.any? { |r| r['is_shared'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_falsey
@@ -102,7 +102,7 @@ describe API::Runners do
it 'avoids filtering if scope is invalid' do
get api('/runners?scope=unknown', admin)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -110,7 +110,7 @@ describe API::Runners do
it 'does not return runners' do
get api('/runners')
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -121,7 +121,7 @@ describe API::Runners do
it "returns runner's details" do
get api("/runners/#{shared_runner.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['description']).to eq(shared_runner.description)
end
end
@@ -130,7 +130,7 @@ describe API::Runners do
it "returns runner's details" do
get api("/runners/#{specific_runner.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['description']).to eq(specific_runner.description)
end
end
@@ -138,7 +138,7 @@ describe API::Runners do
it 'returns 404 if runner does not exists' do
get api('/runners/9999', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -147,7 +147,7 @@ describe API::Runners do
it "returns runner's details" do
get api("/runners/#{specific_runner.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['description']).to eq(specific_runner.description)
end
end
@@ -156,7 +156,7 @@ describe API::Runners do
it "returns runner's details" do
get api("/runners/#{shared_runner.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['description']).to eq(shared_runner.description)
end
end
@@ -166,7 +166,7 @@ describe API::Runners do
it "does not return runner's details" do
get api("/runners/#{specific_runner.id}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -174,7 +174,7 @@ describe API::Runners do
it "does not return runner's details" do
get api("/runners/#{specific_runner.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -195,7 +195,7 @@ describe API::Runners do
access_level: 'ref_protected')
shared_runner.reload
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(shared_runner.description).to eq("#{description}_updated")
expect(shared_runner.active).to eq(!active)
expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
@@ -215,7 +215,7 @@ describe API::Runners do
update_runner(specific_runner.id, admin, description: 'test')
specific_runner.reload
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(specific_runner.description).to eq('test')
expect(specific_runner.description).not_to eq(description)
expect(specific_runner.ensure_runner_queue_value)
@@ -226,7 +226,7 @@ describe API::Runners do
it 'returns 404 if runner does not exists' do
update_runner(9999, admin, description: 'test')
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
def update_runner(id, user, args)
@@ -239,7 +239,7 @@ describe API::Runners do
it 'does not update runner' do
put api("/runners/#{shared_runner.id}", user), description: 'test'
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -247,7 +247,7 @@ describe API::Runners do
it 'does not update runner without access to it' do
put api("/runners/#{specific_runner.id}", user2), description: 'test'
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'updates runner with access to it' do
@@ -255,7 +255,7 @@ describe API::Runners do
put api("/runners/#{specific_runner.id}", admin), description: 'test'
specific_runner.reload
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(specific_runner.description).to eq('test')
expect(specific_runner.description).not_to eq(description)
end
@@ -266,7 +266,7 @@ describe API::Runners do
it 'does not delete runner' do
put api("/runners/#{specific_runner.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -278,7 +278,7 @@ describe API::Runners do
expect do
delete api("/runners/#{shared_runner.id}", admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { Ci::Runner.shared.count }.by(-1)
end
@@ -292,7 +292,7 @@ describe API::Runners do
expect do
delete api("/runners/#{unused_specific_runner.id}", admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { Ci::Runner.specific.count }.by(-1)
end
@@ -300,7 +300,7 @@ describe API::Runners do
expect do
delete api("/runners/#{specific_runner.id}", admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { Ci::Runner.specific.count }.by(-1)
end
end
@@ -308,7 +308,7 @@ describe API::Runners do
it 'returns 404 if runner does not exists' do
delete api('/runners/9999', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -316,26 +316,26 @@ describe API::Runners do
context 'when runner is shared' do
it 'does not delete runner' do
delete api("/runners/#{shared_runner.id}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
context 'when runner is not shared' do
it 'does not delete runner without access to it' do
delete api("/runners/#{specific_runner.id}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'does not delete runner with more than one associated project' do
delete api("/runners/#{two_projects_runner.id}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'deletes runner for one owned project' do
expect do
delete api("/runners/#{specific_runner.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { Ci::Runner.specific.count }.by(-1)
end
@@ -349,7 +349,141 @@ describe API::Runners do
it 'does not delete runner' do
delete api("/runners/#{specific_runner.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+ end
+
+ describe 'GET /runners/:id/jobs' do
+ set(:job_1) { create(:ci_build) }
+ let!(:job_2) { create(:ci_build, :running, runner: shared_runner, project: project) }
+ let!(:job_3) { create(:ci_build, :failed, runner: shared_runner, project: project) }
+ let!(:job_4) { create(:ci_build, :running, runner: specific_runner, project: project) }
+ let!(:job_5) { create(:ci_build, :failed, runner: specific_runner, project: project) }
+
+ context 'admin user' do
+ context 'when runner exists' do
+ context 'when runner is shared' do
+ it 'return jobs' do
+ get api("/runners/#{shared_runner.id}/jobs", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(2)
+ end
+ end
+
+ context 'when runner is specific' do
+ it 'return jobs' do
+ get api("/runners/#{specific_runner.id}/jobs", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(2)
+ end
+ end
+
+ context 'when valid status is provided' do
+ it 'return filtered jobs' do
+ get api("/runners/#{specific_runner.id}/jobs?status=failed", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first).to include('id' => job_5.id)
+ end
+ end
+
+ context 'when invalid status is provided' do
+ it 'return 400' do
+ get api("/runners/#{specific_runner.id}/jobs?status=non-existing", admin)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
+ context "when runner doesn't exist" do
+ it 'returns 404' do
+ get api('/runners/9999/jobs', admin)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context "runner project's administrative user" do
+ context 'when runner exists' do
+ context 'when runner is shared' do
+ it 'returns 403' do
+ get api("/runners/#{shared_runner.id}/jobs", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'when runner is specific' do
+ it 'return jobs' do
+ get api("/runners/#{specific_runner.id}/jobs", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(2)
+ end
+ end
+
+ context 'when valid status is provided' do
+ it 'return filtered jobs' do
+ get api("/runners/#{specific_runner.id}/jobs?status=failed", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first).to include('id' => job_5.id)
+ end
+ end
+
+ context 'when invalid status is provided' do
+ it 'return 400' do
+ get api("/runners/#{specific_runner.id}/jobs?status=non-existing", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
+ context "when runner doesn't exist" do
+ it 'returns 404' do
+ get api('/runners/9999/jobs', user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context 'other authorized user' do
+ it 'does not return jobs' do
+ get api("/runners/#{specific_runner.id}/jobs", user2)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return jobs' do
+ get api("/runners/#{specific_runner.id}/jobs")
+
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -360,7 +494,7 @@ describe API::Runners do
get api("/projects/#{project.id}/runners", user)
shared = json_response.any? { |r| r['is_shared'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(shared).to be_truthy
@@ -371,7 +505,7 @@ describe API::Runners do
it "does not return project's runners" do
get api("/projects/#{project.id}/runners", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -379,7 +513,7 @@ describe API::Runners do
it "does not return project's runners" do
get api("/projects/#{project.id}/runners")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -396,14 +530,14 @@ describe API::Runners do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
end.to change { project.runners.count }.by(+1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
it 'avoids changes when enabling already enabled runner' do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id
end.to change { project.runners.count }.by(0)
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
end
it 'does not enable locked runner' do
@@ -413,13 +547,13 @@ describe API::Runners do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
end.to change { project.runners.count }.by(0)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'does not enable shared runner' do
post api("/projects/#{project.id}/runners", user), runner_id: shared_runner.id
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
context 'user is admin' do
@@ -427,7 +561,7 @@ describe API::Runners do
expect do
post api("/projects/#{project.id}/runners", admin), runner_id: unused_specific_runner.id
end.to change { project.runners.count }.by(+1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
end
@@ -435,14 +569,14 @@ describe API::Runners do
it 'does not enable runner without access to' do
post api("/projects/#{project.id}/runners", user), runner_id: unused_specific_runner.id
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
it 'raises an error when no runner_id param is provided' do
post api("/projects/#{project.id}/runners", admin)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -450,7 +584,7 @@ describe API::Runners do
it 'does not enable runner' do
post api("/projects/#{project.id}/runners", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -458,7 +592,7 @@ describe API::Runners do
it 'does not enable runner' do
post api("/projects/#{project.id}/runners")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -470,7 +604,7 @@ describe API::Runners do
expect do
delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { project.runners.count }.by(-1)
end
@@ -484,14 +618,14 @@ describe API::Runners do
expect do
delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
end.to change { project.runners.count }.by(0)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
it 'returns 404 is runner is not found' do
delete api("/projects/#{project.id}/runners/9999", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -499,7 +633,7 @@ describe API::Runners do
it "does not disable project's runner" do
delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -507,7 +641,7 @@ describe API::Runners do
it "does not disable project's runner" do
delete api("/projects/#{project.id}/runners/#{specific_runner.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 7e174903918..ba697e2b305 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -16,7 +16,7 @@ describe API::Services do
it "updates #{service} settings" do
put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
current_service = project.services.first
event = current_service.event_names.empty? ? "foo" : current_service.event_names.first
@@ -24,7 +24,7 @@ describe API::Services do
put api("/projects/#{project.id}/services/#{dashed_service}?#{event}=#{!state}", user), service_attrs
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(project.services.first[event]).not_to eq(state) unless event == "foo"
end
@@ -56,7 +56,7 @@ describe API::Services do
it "deletes #{service}" do
delete api("/projects/#{project.id}/services/#{dashed_service}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
project.send(service_method).reload
expect(project.send(service_method).activated?).to be_falsey
end
@@ -74,20 +74,20 @@ describe API::Services do
it 'returns authentication error when unauthenticated' do
get api("/projects/#{project.id}/services/#{dashed_service}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it "returns all properties of service #{service} when authenticated as admin" do
get api("/projects/#{project.id}/services/#{dashed_service}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list.map)
end
it "returns properties of service #{service} other than passwords when authenticated as project owner" do
get api("/projects/#{project.id}/services/#{dashed_service}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list_without_passwords)
end
@@ -95,7 +95,7 @@ describe API::Services do
project.team << [user2, :developer]
get api("/projects/#{project.id}/services/#{dashed_service}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -108,7 +108,7 @@ describe API::Services do
it 'returns a not found message' do
post api("/projects/#{project.id}/services/idonotexist/trigger")
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response["error"]).to eq("404 Not Found")
end
end
@@ -127,7 +127,7 @@ describe API::Services do
it 'when the service is inactive' do
post api("/projects/#{project.id}/services/#{service_name}/trigger"), params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -142,7 +142,7 @@ describe API::Services do
it 'returns status 200' do
post api("/projects/#{project.id}/services/#{service_name}/trigger"), params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -150,7 +150,7 @@ describe API::Services do
it 'returns a generic 404' do
post api("/projects/404/services/#{service_name}/trigger"), params
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response["message"]).to eq("404 Service Not Found")
end
end
@@ -170,9 +170,30 @@ describe API::Services do
it 'returns status 200' do
post api("/projects/#{project.id}/services/#{service_name}/trigger"), token: 'token', text: 'help'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['response_type']).to eq("ephemeral")
end
end
end
+
+ describe 'Mattermost service' do
+ let(:service_name) { 'mattermost' }
+ let(:params) do
+ { webhook: 'https://hook.example.com', username: 'username' }
+ end
+
+ before do
+ project.create_mattermost_service(
+ active: true,
+ properties: params
+ )
+ end
+
+ it 'accepts a username for update' do
+ put api("/projects/#{project.id}/services/mattermost", user), params.merge(username: 'new_username')
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['properties']['username']).to eq('new_username')
+ end
+ end
end
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
deleted file mode 100644
index 5e77519c867..00000000000
--- a/spec/requests/api/session_spec.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-require 'spec_helper'
-
-describe API::Session do
- let(:user) { create(:user) }
-
- describe "POST /session" do
- context "when valid password" do
- it "returns private token" do
- post api("/session"), email: user.email, password: '12345678'
- expect(response).to have_http_status(201)
-
- expect(json_response['email']).to eq(user.email)
- expect(json_response['private_token']).to eq(user.private_token)
- expect(json_response['is_admin']).to eq(user.admin?)
- expect(json_response['can_create_project']).to eq(user.can_create_project?)
- expect(json_response['can_create_group']).to eq(user.can_create_group?)
- end
-
- context 'with 2FA enabled' do
- it 'rejects sign in attempts' do
- user = create(:user, :two_factor)
-
- post api('/session'), email: user.email, password: user.password
-
- expect(response).to have_http_status(401)
- expect(response.body).to include('You have 2FA enabled.')
- end
- end
- end
-
- context 'when email has case-typo and password is valid' do
- it 'returns private token' do
- post api('/session'), email: user.email.upcase, password: '12345678'
- expect(response.status).to eq 201
-
- expect(json_response['email']).to eq user.email
- expect(json_response['private_token']).to eq user.private_token
- expect(json_response['is_admin']).to eq user.admin?
- expect(json_response['can_create_project']).to eq user.can_create_project?
- expect(json_response['can_create_group']).to eq user.can_create_group?
- end
- end
-
- context 'when login has case-typo and password is valid' do
- it 'returns private token' do
- post api('/session'), login: user.username.upcase, password: '12345678'
- expect(response.status).to eq 201
-
- expect(json_response['email']).to eq user.email
- expect(json_response['private_token']).to eq user.private_token
- expect(json_response['is_admin']).to eq user.admin?
- expect(json_response['can_create_project']).to eq user.can_create_project?
- expect(json_response['can_create_group']).to eq user.can_create_group?
- end
- end
-
- context "when invalid password" do
- it "returns authentication error" do
- post api("/session"), email: user.email, password: '123'
- expect(response).to have_http_status(401)
-
- expect(json_response['email']).to be_nil
- expect(json_response['private_token']).to be_nil
- end
- end
-
- context "when empty password" do
- it "returns authentication error with email" do
- post api("/session"), email: user.email
-
- expect(response).to have_http_status(400)
- end
-
- it "returns authentication error with username" do
- post api("/session"), email: user.username
-
- expect(response).to have_http_status(400)
- end
- end
-
- context "when empty name" do
- it "returns authentication error" do
- post api("/session"), password: user.password
-
- expect(response).to have_http_status(400)
- end
- end
-
- context "when user is blocked" do
- it "returns authentication error" do
- user.block
- post api("/session"), email: user.username, password: user.password
-
- expect(response).to have_http_status(401)
- end
- end
-
- context "when user is ldap_blocked" do
- it "returns authentication error" do
- user.ldap_block
- post api("/session"), email: user.username, password: user.password
-
- expect(response).to have_http_status(401)
- end
- end
- end
-end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 0b9a4b5c3db..63175c40a18 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -7,10 +7,10 @@ describe API::Settings, 'Settings' do
describe "GET /application/settings" do
it "returns application settings" do
get api("/application/settings", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Hash
expect(json_response['default_projects_limit']).to eq(42)
- expect(json_response['password_authentication_enabled']).to be_truthy
+ expect(json_response['password_authentication_enabled_for_web']).to be_truthy
expect(json_response['repository_storages']).to eq(['default'])
expect(json_response['koding_enabled']).to be_falsey
expect(json_response['koding_url']).to be_nil
@@ -23,6 +23,7 @@ describe API::Settings, 'Settings' do
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)
+ expect(json_response['circuitbreaker_failure_count_threshold']).not_to be_nil
end
end
@@ -36,7 +37,7 @@ describe API::Settings, 'Settings' do
it "updates application settings" do
put api("/application/settings", admin),
default_projects_limit: 3,
- password_authentication_enabled: false,
+ password_authentication_enabled_for_web: false,
repository_storages: ['custom'],
koding_enabled: true,
koding_url: 'http://koding.example.com',
@@ -52,11 +53,12 @@ describe API::Settings, 'Settings' do
rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE,
dsa_key_restriction: 2048,
ecdsa_key_restriction: 384,
- ed25519_key_restriction: 256
+ ed25519_key_restriction: 256,
+ circuitbreaker_failure_wait_time: 2
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
- expect(json_response['password_authentication_enabled']).to be_falsey
+ expect(json_response['password_authentication_enabled_for_web']).to be_falsey
expect(json_response['repository_storages']).to eq(['custom'])
expect(json_response['koding_enabled']).to be_truthy
expect(json_response['koding_url']).to eq('http://koding.example.com')
@@ -73,6 +75,7 @@ describe API::Settings, 'Settings' do
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)
+ expect(json_response['circuitbreaker_failure_wait_time']).to eq(2)
end
end
@@ -80,7 +83,7 @@ describe API::Settings, 'Settings' do
it "returns a blank parameter error message" do
put api("/application/settings", admin), koding_enabled: true
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('koding_url is missing')
end
end
@@ -89,7 +92,7 @@ describe API::Settings, 'Settings' do
it "returns a blank parameter error message" do
put api("/application/settings", admin), plantuml_enabled: true
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('plantuml_url is missing')
end
end
diff --git a/spec/requests/api/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb
index 83042d0cb12..fff9adb7f57 100644
--- a/spec/requests/api/sidekiq_metrics_spec.rb
+++ b/spec/requests/api/sidekiq_metrics_spec.rb
@@ -7,28 +7,28 @@ describe API::SidekiqMetrics do
it 'defines the `queue_metrics` endpoint' do
get api('/sidekiq/queue_metrics', admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_a Hash
end
it 'defines the `process_metrics` endpoint' do
get api('/sidekiq/process_metrics', admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['processes']).to be_an Array
end
it 'defines the `job_stats` endpoint' do
get api('/sidekiq/job_stats', admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_a Hash
end
it 'defines the `compound_metrics` endpoint' do
get api('/sidekiq/compound_metrics', admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_a Hash
expect(json_response['queues']).to be_a Hash
expect(json_response['processes']).to be_an Array
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index d3905f698bd..74198c8eb4f 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -11,7 +11,7 @@ describe API::Snippets do
get api("/snippets/", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
@@ -27,7 +27,7 @@ describe API::Snippets do
get api("/snippets/", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(0)
@@ -46,7 +46,7 @@ describe API::Snippets do
it 'returns all snippets with public visibility from all users' do
get api("/snippets/public", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
@@ -67,7 +67,7 @@ describe API::Snippets do
it 'returns raw text' do
get api("/snippets/#{snippet.id}/raw", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.content_type).to eq 'text/plain'
expect(response.body).to eq(snippet.content)
end
@@ -75,7 +75,7 @@ describe API::Snippets do
it 'returns 404 for invalid snippet id' do
get api("/snippets/1234/raw", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end
@@ -86,7 +86,7 @@ describe API::Snippets do
it 'returns snippet json' do
get api("/snippets/#{snippet.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(snippet.title)
expect(json_response['description']).to eq(snippet.description)
@@ -96,7 +96,7 @@ describe API::Snippets do
it 'returns 404 for invalid snippet id' do
get api("/snippets/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
end
@@ -117,7 +117,7 @@ describe API::Snippets do
post api("/snippets/", user), params
end.to change { PersonalSnippet.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq(params[:title])
expect(json_response['description']).to eq(params[:description])
expect(json_response['file_name']).to eq(params[:file_name])
@@ -128,7 +128,7 @@ describe API::Snippets do
post api("/snippets/", user), params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context 'when the snippet is spam' do
@@ -152,7 +152,7 @@ describe API::Snippets do
expect { create_snippet(visibility: 'public') }
.not_to change { Snippet.count }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
end
@@ -177,7 +177,7 @@ describe API::Snippets do
put api("/snippets/#{snippet.id}", user), content: new_content, description: new_description
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
snippet.reload
expect(snippet.content).to eq(new_content)
expect(snippet.description).to eq(new_description)
@@ -186,21 +186,21 @@ describe API::Snippets do
it 'returns 404 for invalid snippet id' do
put api("/snippets/1234", user), title: 'foo'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
it "returns 404 for another user's snippet" do
put api("/snippets/#{snippet.id}", other_user), title: 'fubar'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
it 'returns 400 for missing parameters' do
put api("/snippets/1234", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context 'when the snippet is spam' do
@@ -228,7 +228,7 @@ describe API::Snippets do
expect { update_snippet(title: 'Foo') }
.not_to change { snippet.reload.title }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
end
@@ -260,14 +260,14 @@ describe API::Snippets do
expect do
delete api("/snippets/#{public_snippet.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { PersonalSnippet.count }.by(-1)
end
it 'returns 404 for invalid snippet id' do
delete api("/snippets/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
@@ -284,7 +284,7 @@ describe API::Snippets do
it 'exposes known attributes' do
get api("/snippets/#{snippet.id}/user_agent_detail", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
expect(json_response['ip_address']).to eq(user_agent_detail.ip_address)
expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
@@ -293,7 +293,7 @@ describe API::Snippets do
it "returns unautorized for non-admin users" do
get api("/snippets/#{snippet.id}/user_agent_detail", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index 216d278ad21..c7a009e999e 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -14,7 +14,7 @@ describe API::SystemHooks do
it "returns authentication error" do
get api("/hooks")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -22,7 +22,7 @@ describe API::SystemHooks do
it "returns forbidden error" do
get api("/hooks", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -30,7 +30,7 @@ describe API::SystemHooks do
it "returns an array of hooks" do
get api("/hooks", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['url']).to eq(hook.url)
@@ -51,13 +51,13 @@ describe API::SystemHooks do
it "responds with 400 if url not given" do
post api("/hooks", admin)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "responds with 400 if url is invalid" do
post api("/hooks", admin), url: 'hp://mep.mep'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "does not create new hook without url" do
@@ -69,7 +69,7 @@ describe API::SystemHooks do
it 'sets default values for events' do
post api('/hooks', admin), url: 'http://mep.mep', enable_ssl_verification: true
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['enable_ssl_verification']).to be true
expect(json_response['tag_push_events']).to be false
end
@@ -78,13 +78,13 @@ describe API::SystemHooks do
describe "GET /hooks/:id" do
it "returns hook by id" do
get api("/hooks/#{hook.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['event_name']).to eq('project_create')
end
it "returns 404 on failure" do
get api("/hooks/404", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -93,14 +93,14 @@ describe API::SystemHooks do
expect do
delete api("/hooks/#{hook.id}", admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { SystemHook.count }.by(-1)
end
it 'returns 404 if the system hook does not exist' do
delete api('/hooks/12345', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it_behaves_like '412 response' do
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index f8af9295842..de1619f33c1 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -23,7 +23,7 @@ describe API::Templates do
it 'returns a list of available gitignore templates' do
get api('/templates/gitignores')
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to be > 15
@@ -34,7 +34,7 @@ describe API::Templates do
it 'returns a list of available gitlab_ci_ymls' do
get api('/templates/gitlab_ci_ymls')
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['name']).not_to be_nil
@@ -45,7 +45,7 @@ describe API::Templates do
it 'adds a disclaimer on the top' do
get api('/templates/gitlab_ci_ymls/Ruby')
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['content']).to start_with("# This file is a template,")
end
end
@@ -74,7 +74,7 @@ describe API::Templates do
it 'returns a list of available license templates' do
get api('/templates/licenses')
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(12)
@@ -86,7 +86,7 @@ describe API::Templates do
it 'returns a list of available popular license templates' do
get api('/templates/licenses?popular=1')
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
@@ -169,7 +169,7 @@ describe API::Templates do
let(:license_type) { 'muth-over9000' }
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index 25d7f6dffcf..c6063a2e089 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -110,7 +110,7 @@ describe API::Todos do
it 'returns authentication error' do
post api("/todos/#{pending_1.id}/mark_as_done")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -118,7 +118,7 @@ describe API::Todos do
it 'marks a todo as done' do
post api("/todos/#{pending_1.id}/mark_as_done", john_doe)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['id']).to eq(pending_1.id)
expect(json_response['state']).to eq('done')
expect(pending_1.reload).to be_done
@@ -137,7 +137,7 @@ describe API::Todos do
it 'returns authentication error' do
post api('/todos/mark_as_done')
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -145,7 +145,7 @@ describe API::Todos do
it 'marks all todos as done' do
post api('/todos/mark_as_done', john_doe)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(pending_1.reload).to be_done
expect(pending_2.reload).to be_done
expect(pending_3.reload).to be_done
@@ -196,9 +196,9 @@ describe API::Todos do
post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", guest)
if issuable_type == 'merge_requests'
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
else
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 922b99a6cba..b2c56f7af2c 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -28,13 +28,13 @@ describe API::Triggers do
it 'returns bad request if token is missing' do
post api("/projects/#{project.id}/trigger/pipeline"), ref: 'master'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns not found if project is not found' do
post api('/projects/0/trigger/pipeline'), options.merge(ref: 'master')
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -44,7 +44,7 @@ describe API::Triggers do
it 'creates pipeline' do
post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'master')
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to include('id' => pipeline.id)
pipeline.builds.reload
expect(pipeline.builds.pending.size).to eq(2)
@@ -54,7 +54,7 @@ describe API::Triggers do
it 'returns bad request with no pipeline created if there\'s no commit for that ref' do
post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch')
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('base' => ["Reference not found"])
end
@@ -66,21 +66,21 @@ describe API::Triggers do
it 'validates variables to be a hash' do
post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: 'value', ref: 'master')
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('variables is invalid')
end
it 'validates variables needs to be a map of key-valued strings' do
post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
end
it 'creates trigger request with variables' do
post api("/projects/#{project.id}/trigger/pipeline"), options.merge(variables: variables, ref: 'master')
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(pipeline.variables.map { |v| { v.key => v.value } }.last).to eq(variables)
end
end
@@ -93,7 +93,7 @@ describe API::Triggers do
it 'creates pipeline' do
post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'master')
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to include('id' => pipeline.id)
pipeline.builds.reload
expect(pipeline.builds.pending.size).to eq(2)
@@ -106,7 +106,7 @@ describe API::Triggers do
it 'does not leak the presence of project when token is for different project' do
post api("/projects/#{project2.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'creates builds from the ref given in the URL, not in the body' do
@@ -114,7 +114,7 @@ describe API::Triggers do
post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
end.to change(project.builds, :count).by(5)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
context 'when ref contains a dot' do
@@ -125,7 +125,7 @@ describe API::Triggers do
post api("/projects/#{project.id}/ref/v.1-branch/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
end.to change(project.builds, :count).by(4)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
end
end
@@ -136,7 +136,7 @@ describe API::Triggers do
it 'returns list of triggers' do
get api("/projects/#{project.id}/triggers", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_a(Array)
expect(json_response[0]).to have_key('token')
@@ -147,7 +147,7 @@ describe API::Triggers do
it 'does not return triggers list' do
get api("/projects/#{project.id}/triggers", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -155,7 +155,7 @@ describe API::Triggers do
it 'does not return triggers list' do
get api("/projects/#{project.id}/triggers")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -165,14 +165,14 @@ describe API::Triggers do
it 'returns trigger details' do
get api("/projects/#{project.id}/triggers/#{trigger.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_a(Hash)
end
it 'responds with 404 Not Found if requesting non-existing trigger' do
get api("/projects/#{project.id}/triggers/-5", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -180,7 +180,7 @@ describe API::Triggers do
it 'does not return triggers list' do
get api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -188,7 +188,7 @@ describe API::Triggers do
it 'does not return triggers list' do
get api("/projects/#{project.id}/triggers/#{trigger.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -202,7 +202,7 @@ describe API::Triggers do
description: 'trigger'
end.to change {project.triggers.count}.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to include('description' => 'trigger')
end
end
@@ -211,7 +211,7 @@ describe API::Triggers do
it 'does not create trigger' do
post api("/projects/#{project.id}/triggers", user)
- expect(response).to have_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
@@ -221,7 +221,7 @@ describe API::Triggers do
post api("/projects/#{project.id}/triggers", user2),
description: 'trigger'
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -230,7 +230,7 @@ describe API::Triggers do
post api("/projects/#{project.id}/triggers"),
description: 'trigger'
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -243,7 +243,7 @@ describe API::Triggers do
put api("/projects/#{project.id}/triggers/#{trigger.id}", user),
description: new_description
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to include('description' => new_description)
expect(trigger.reload.description).to eq(new_description)
end
@@ -253,7 +253,7 @@ describe API::Triggers do
it 'does not update trigger' do
put api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -261,7 +261,7 @@ describe API::Triggers do
it 'does not update trigger' do
put api("/projects/#{project.id}/triggers/#{trigger.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -271,7 +271,7 @@ describe API::Triggers do
it 'updates owner' do
post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to include('owner')
expect(trigger.reload.owner).to eq(user)
end
@@ -281,7 +281,7 @@ describe API::Triggers do
it 'does not update owner' do
post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -289,7 +289,7 @@ describe API::Triggers do
it 'does not update owner' do
post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -300,14 +300,14 @@ describe API::Triggers do
expect do
delete api("/projects/#{project.id}/triggers/#{trigger.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change {project.triggers.count}.by(-1)
end
it 'responds with 404 Not Found if requesting non-existing trigger' do
delete api("/projects/#{project.id}/triggers/-5", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it_behaves_like '412 response' do
@@ -319,7 +319,7 @@ describe API::Triggers do
it 'does not delete trigger' do
delete api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -327,7 +327,7 @@ describe API::Triggers do
it 'does not delete trigger' do
delete api("/projects/#{project.id}/triggers/#{trigger.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 69c8aa4482a..2428e63e149 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -113,7 +113,7 @@ describe API::Users do
it "returns a 403 when non-admin user searches by external UID" do
get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}&provider=#{omniauth_user.identities.first.provider}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'does not reveal the `is_admin` flag of the user' do
@@ -127,10 +127,10 @@ describe API::Users do
context "when admin" do
context 'when sudo is defined' do
it 'does not return 500' do
- admin_personal_access_token = create(:personal_access_token, user: admin).token
- get api("/users?private_token=#{admin_personal_access_token}&sudo=#{user.id}", admin)
+ admin_personal_access_token = create(:personal_access_token, user: admin, scopes: [:sudo])
+ get api("/users?sudo=#{user.id}", admin, personal_access_token: admin_personal_access_token)
- expect(response).to have_http_status(:success)
+ expect(response).to have_gitlab_http_status(:success)
end
end
@@ -162,13 +162,13 @@ describe API::Users do
it "returns 400 error if provider with no extern_uid" do
get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}", admin)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns 400 error if provider with no extern_uid" do
get api("/users?provider=#{omniauth_user.identities.first.provider}", admin)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a user created before a specific date" do
@@ -240,21 +240,21 @@ describe API::Users do
get api("/users/#{user.id}")
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
it "returns a 404 error if user id not found" do
get api("/users/9999", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 for invalid ID" do
get api("/users/1ASDF", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -271,7 +271,7 @@ describe API::Users do
it "creates user with correct attributes" do
post api('/users', admin), attributes_for(:user, admin: true, can_create_group: true)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
user_id = json_response['id']
new_user = User.find(user_id)
expect(new_user).not_to eq(nil)
@@ -285,12 +285,12 @@ describe API::Users do
post api('/users', admin), attributes
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
it "creates non-admin user" do
post api('/users', admin), attributes_for(:user, admin: false, can_create_group: false)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
user_id = json_response['id']
new_user = User.find(user_id)
expect(new_user).not_to eq(nil)
@@ -300,7 +300,7 @@ describe API::Users do
it "creates non-admin users by default" do
post api('/users', admin), attributes_for(:user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
user_id = json_response['id']
new_user = User.find(user_id)
expect(new_user).not_to eq(nil)
@@ -309,12 +309,12 @@ describe API::Users do
it "returns 201 Created on success" do
post api("/users", admin), attributes_for(:user, projects_limit: 3)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
it 'creates non-external users by default' do
post api("/users", admin), attributes_for(:user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
user_id = json_response['id']
new_user = User.find(user_id)
@@ -324,7 +324,7 @@ describe API::Users do
it 'allows an external user to be created' do
post api("/users", admin), attributes_for(:user, external: true)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
user_id = json_response['id']
new_user = User.find(user_id)
@@ -335,7 +335,7 @@ describe API::Users do
it "creates user with reset password" do
post api('/users', admin), attributes_for(:user, reset_password: true).except(:password)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
user_id = json_response['id']
new_user = User.find(user_id)
@@ -349,27 +349,27 @@ describe API::Users do
email: 'invalid email',
password: 'password',
name: 'test'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 error if name not given' do
post api('/users', admin), attributes_for(:user).except(:name)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 error if password not given' do
post api('/users', admin), attributes_for(:user).except(:password)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 error if email not given' do
post api('/users', admin), attributes_for(:user).except(:email)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 error if username not given' do
post api('/users', admin), attributes_for(:user).except(:username)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 error if user does not validate' do
@@ -380,7 +380,7 @@ describe API::Users do
name: 'test',
bio: 'g' * 256,
projects_limit: -1
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['password'])
.to eq(['is too short (minimum is 8 characters)'])
expect(json_response['message']['bio'])
@@ -393,7 +393,7 @@ describe API::Users do
it "is not available for non admin users" do
post api("/users", user), attributes_for(:user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
context 'with existing user' do
@@ -413,7 +413,7 @@ describe API::Users do
password: 'password',
username: 'foo'
end.to change { User.count }.by(0)
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
expect(json_response['message']).to eq('Email has already been taken')
end
@@ -425,14 +425,14 @@ describe API::Users do
password: 'password',
username: 'test'
end.to change { User.count }.by(0)
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
expect(json_response['message']).to eq('Username has already been taken')
end
it 'creates user with new identity' do
post api("/users", admin), attributes_for(:user, provider: 'github', extern_uid: '67890')
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['identities'].first['extern_uid']).to eq('67890')
expect(json_response['identities'].first['provider']).to eq('github')
end
@@ -450,7 +450,7 @@ describe API::Users do
describe "GET /users/sign_up" do
it "redirects to sign in page" do
get "/users/sign_up"
- expect(response).to have_http_status(302)
+ expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(new_user_session_path)
end
end
@@ -465,7 +465,7 @@ describe API::Users do
it "updates user with new bio" do
put api("/users/#{user.id}", admin), { bio: 'new test bio' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['bio']).to eq('new test bio')
expect(user.reload.bio).to eq('new test bio')
end
@@ -473,14 +473,14 @@ describe API::Users do
it "updates user with new password and forces reset on next login" do
put api("/users/#{user.id}", admin), password: '12345678'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(user.reload.password_expires_at).to be <= Time.now
end
it "updates user with organization" do
put api("/users/#{user.id}", admin), { organization: 'GitLab' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['organization']).to eq('GitLab')
expect(user.reload.organization).to eq('GitLab')
end
@@ -491,14 +491,14 @@ describe API::Users do
user.reload
expect(user.avatar).to be_present
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['avatar_url']).to include(user.avatar_path)
end
it 'updates user with his own email' do
put api("/users/#{user.id}", admin), email: user.email
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['email']).to eq(user.email)
expect(user.reload.email).to eq(user.email)
end
@@ -506,14 +506,22 @@ describe API::Users do
it 'updates user with a new email' do
put api("/users/#{user.id}", admin), email: 'new@email.com'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(user.reload.notification_email).to eq('new@email.com')
end
+ it 'skips reconfirmation when requested' do
+ put api("/users/#{user.id}", admin), { skip_reconfirmation: true }
+
+ user.reload
+
+ expect(user.confirmed_at).to be_present
+ end
+
it 'updates user with his own username' do
put api("/users/#{user.id}", admin), username: user.username
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['username']).to eq(user.username)
expect(user.reload.username).to eq(user.username)
end
@@ -521,14 +529,14 @@ describe API::Users do
it "updates user's existing identity" do
put api("/users/#{omniauth_user.id}", admin), provider: 'ldapmain', extern_uid: '654321'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(omniauth_user.reload.identities.first.extern_uid).to eq('654321')
end
it 'updates user with new identity' do
put api("/users/#{user.id}", admin), provider: 'github', extern_uid: 'john'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(user.reload.identities.first.extern_uid).to eq('john')
expect(user.reload.identities.first.provider).to eq('github')
end
@@ -536,7 +544,7 @@ describe API::Users do
it "updates admin status" do
put api("/users/#{user.id}", admin), { admin: true }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(user.reload.admin).to eq(true)
end
@@ -551,7 +559,7 @@ describe API::Users do
it "does not update admin status" do
put api("/users/#{admin_user.id}", admin), { can_create_group: false }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(admin_user.reload.admin).to eq(true)
expect(admin_user.can_create_group).to eq(false)
end
@@ -559,7 +567,7 @@ describe API::Users do
it "does not allow invalid update" do
put api("/users/#{user.id}", admin), { email: 'invalid email' }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(user.reload.email).not_to eq('invalid email')
end
@@ -569,21 +577,21 @@ describe API::Users do
put api("/users/#{user.id}", user), attributes_for(:user)
end.not_to change { user.reload.attributes }
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
it "returns 404 for non-existing user" do
put api("/users/999999", admin), { bio: 'update should fail' }
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 if invalid ID" do
put api("/users/ASDF", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 400 error if user does not validate' do
@@ -594,7 +602,7 @@ describe API::Users do
name: 'test',
bio: 'g' * 256,
projects_limit: -1
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['password'])
.to eq(['is too short (minimum is 8 characters)'])
expect(json_response['message']['bio'])
@@ -608,13 +616,13 @@ describe API::Users do
it 'returns 400 if provider is missing for identity update' do
put api("/users/#{omniauth_user.id}", admin), extern_uid: '654321'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 if external UID is missing for identity update' do
put api("/users/#{omniauth_user.id}", admin), provider: 'ldap'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context "with existing user" do
@@ -627,7 +635,7 @@ describe API::Users do
it 'returns 409 conflict error if email address exists' do
put api("/users/#{@user.id}", admin), email: 'test@example.com'
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
expect(@user.reload.email).to eq(@user.email)
end
@@ -635,7 +643,7 @@ describe API::Users do
@user_id = User.all.last.id
put api("/users/#{@user.id}", admin), username: 'test'
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
expect(@user.reload.username).to eq(@user.username)
end
end
@@ -649,14 +657,14 @@ describe API::Users do
it "does not create invalid ssh key" do
post api("/users/#{user.id}/keys", admin), { title: "invalid key" }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('key is missing')
end
it 'does not create key without title' do
post api("/users/#{user.id}/keys", admin), key: 'some key'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('title is missing')
end
@@ -669,7 +677,7 @@ describe API::Users do
it "returns 400 for invalid ID" do
post api("/users/999999/keys", admin)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -681,14 +689,14 @@ describe API::Users do
context 'when unauthenticated' do
it 'returns authentication error' do
get api("/users/#{user.id}/keys")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
context 'when authenticated' do
it 'returns 404 for non-existing user' do
get api('/users/999999/keys', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -698,7 +706,7 @@ describe API::Users do
get api("/users/#{user.id}/keys", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(key.title)
@@ -714,7 +722,7 @@ describe API::Users do
context 'when unauthenticated' do
it 'returns authentication error' do
delete api("/users/#{user.id}/keys/42")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -726,7 +734,7 @@ describe API::Users do
expect do
delete api("/users/#{user.id}/keys/#{key.id}", admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { user.keys.count }.by(-1)
end
@@ -738,13 +746,13 @@ describe API::Users do
user.keys << key
user.save
delete api("/users/999999/keys/#{key.id}", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_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}/keys/42", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Key Not Found')
end
end
@@ -758,7 +766,7 @@ describe API::Users do
it 'does not create invalid GPG key' do
post api("/users/#{user.id}/gpg_keys", admin)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('key is missing')
end
@@ -767,14 +775,14 @@ describe API::Users do
expect do
post api("/users/#{user.id}/gpg_keys", admin), key_attrs
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_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)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -787,7 +795,7 @@ describe API::Users do
it 'returns authentication error' do
get api("/users/#{user.id}/gpg_keys")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -795,14 +803,14 @@ describe API::Users do
it 'returns 404 for non-existing user' do
get api('/users/999999/gpg_keys', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_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(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
@@ -812,7 +820,7 @@ describe API::Users do
get api("/users/#{user.id}/gpg_keys", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['key']).to eq(gpg_key.key)
@@ -829,7 +837,7 @@ describe API::Users do
it 'returns authentication error' do
delete api("/users/#{user.id}/keys/42")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -841,7 +849,7 @@ describe API::Users do
expect do
delete api("/users/#{user.id}/gpg_keys/#{gpg_key.id}", admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { user.gpg_keys.count }.by(-1)
end
@@ -851,14 +859,14 @@ describe API::Users do
delete api("/users/999999/gpg_keys/#{gpg_key.id}", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_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(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
end
@@ -873,7 +881,7 @@ describe API::Users do
it 'returns authentication error' do
post api("/users/#{user.id}/gpg_keys/42/revoke")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -885,7 +893,7 @@ describe API::Users do
expect do
post api("/users/#{user.id}/gpg_keys/#{gpg_key.id}/revoke", admin)
- expect(response).to have_http_status(:accepted)
+ expect(response).to have_gitlab_http_status(:accepted)
end.to change { user.gpg_keys.count }.by(-1)
end
@@ -895,14 +903,14 @@ describe API::Users do
post api("/users/999999/gpg_keys/#{gpg_key.id}/revoke", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_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(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
end
@@ -916,7 +924,7 @@ describe API::Users do
it "does not create invalid email" do
post api("/users/#{user.id}/emails", admin), {}
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('email is missing')
end
@@ -930,7 +938,7 @@ describe API::Users do
it "returns a 400 for invalid ID" do
post api("/users/999999/emails", admin)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -942,14 +950,14 @@ describe API::Users do
context 'when unauthenticated' do
it 'returns authentication error' do
get api("/users/#{user.id}/emails")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
context 'when authenticated' do
it 'returns 404 for non-existing user' do
get api('/users/999999/emails', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -959,7 +967,7 @@ describe API::Users do
get api("/users/#{user.id}/emails", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['email']).to eq(email.email)
@@ -968,7 +976,7 @@ describe API::Users do
it "returns a 404 for invalid ID" do
get api("/users/ASDF/emails", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -981,7 +989,7 @@ describe API::Users do
context 'when unauthenticated' do
it 'returns authentication error' do
delete api("/users/#{user.id}/emails/42")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -993,7 +1001,7 @@ describe API::Users do
expect do
delete api("/users/#{user.id}/emails/#{email.id}", admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { user.emails.count }.by(-1)
end
@@ -1005,20 +1013,20 @@ describe API::Users do
user.emails << email
user.save
delete api("/users/999999/emails/#{email.id}", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns 404 error if email not foud' do
delete api("/users/#{user.id}/emails/42", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Email Not Found')
end
it "returns a 404 for invalid ID" do
delete api("/users/ASDF/emails/bar", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -1034,7 +1042,7 @@ describe API::Users do
it "deletes user" do
Sidekiq::Testing.inline! { delete api("/users/#{user.id}", admin) }
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
expect { Namespace.find(namespace.id) }.to raise_error ActiveRecord::RecordNotFound
end
@@ -1045,31 +1053,31 @@ describe API::Users do
it "does not delete for unauthenticated user" do
Sidekiq::Testing.inline! { delete api("/users/#{user.id}") }
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it "is not available for non admin users" do
Sidekiq::Testing.inline! { delete api("/users/#{user.id}", user) }
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "returns 404 for non-existing user" do
Sidekiq::Testing.inline! { delete api("/users/999999", admin) }
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 for invalid ID" do
Sidekiq::Testing.inline! { delete api("/users/ASDF", admin) }
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context "hard delete disabled" do
it "moves contributions to the ghost user" do
Sidekiq::Testing.inline! { delete api("/users/#{user.id}", admin) }
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(issue.reload).to be_persisted
expect(issue.author.ghost?).to be_truthy
end
@@ -1079,7 +1087,7 @@ describe API::Users do
it "removes contributions" do
Sidekiq::Testing.inline! { delete api("/users/#{user.id}?hard_delete=true", admin) }
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(Issue.exists?(issue.id)).to be_falsy
end
end
@@ -1093,22 +1101,14 @@ describe API::Users do
it 'returns 403 without private token when sudo is defined' do
get api("/user?private_token=#{personal_access_token}&sudo=123")
- expect(response).to have_http_status(403)
- end
- end
-
- context 'with private token' do
- it 'returns 403 without private token when sudo defined' do
- get api("/user?private_token=#{user.private_token}&sudo=123")
-
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
it 'returns current user without private token when sudo not defined' do
get api("/user", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/user/public')
expect(json_response['id']).to eq(user.id)
end
@@ -1128,31 +1128,13 @@ describe API::Users do
it 'returns 403 without private token when sudo defined' do
get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}")
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'returns initial current user without private token but with is_admin when sudo not defined' do
get api("/user?private_token=#{admin_personal_access_token}")
- expect(response).to have_http_status(200)
- expect(response).to match_response_schema('public_api/v4/user/admin')
- expect(json_response['id']).to eq(admin.id)
- end
- end
-
- context 'with private token' do
- it 'returns sudoed user with private token when sudo defined' do
- get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}")
-
- expect(response).to have_http_status(200)
- expect(response).to match_response_schema('public_api/v4/user/login')
- expect(json_response['id']).to eq(user.id)
- end
-
- it 'returns initial current user without private token but with is_admin when sudo not defined' do
- get api("/user?private_token=#{admin.private_token}")
-
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/user/admin')
expect(json_response['id']).to eq(admin.id)
end
@@ -1163,7 +1145,7 @@ describe API::Users do
it "returns 401 error if user is unauthenticated" do
get api("/user")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -1172,7 +1154,7 @@ describe API::Users do
context "when unauthenticated" do
it "returns authentication error" do
get api("/user/keys")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -1183,7 +1165,7 @@ describe API::Users do
get api("/user/keys", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first["title"]).to eq(key.title)
@@ -1203,14 +1185,14 @@ describe API::Users do
user.keys << key
user.save
get api("/user/keys/#{key.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["title"]).to eq(key.title)
end
it "returns 404 Not Found within invalid ID" do
get api("/user/keys/42", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Key Not Found')
end
@@ -1219,14 +1201,14 @@ describe API::Users do
user.save
admin
get api("/user/keys/#{key.id}", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Key Not Found')
end
it "returns 404 for invalid ID" do
get api("/users/keys/ASDF", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context "scopes" do
@@ -1243,31 +1225,31 @@ describe API::Users do
expect do
post api("/user/keys", user), key_attrs
end.to change { user.keys.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
it "returns a 401 error if unauthorized" do
post api("/user/keys"), title: 'some title', key: 'some key'
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it "does not create ssh key without key" do
post api("/user/keys", user), title: 'title'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('key is missing')
end
it 'does not create ssh key without title' do
post api('/user/keys', user), key: 'some key'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('title is missing')
end
it "does not create ssh key without title" do
post api("/user/keys", user), key: "somekey"
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -1279,7 +1261,7 @@ describe API::Users do
expect do
delete api("/user/keys/#{key.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { user.keys.count}.by(-1)
end
@@ -1290,7 +1272,7 @@ describe API::Users do
it "returns 404 if key ID not found" do
delete api("/user/keys/42", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Key Not Found')
end
@@ -1298,13 +1280,13 @@ describe API::Users do
user.keys << key
user.save
delete api("/user/keys/#{key.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it "returns a 404 for invalid ID" do
delete api("/users/keys/ASDF", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1313,7 +1295,7 @@ describe API::Users do
it 'returns authentication error' do
get api('/user/gpg_keys')
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -1324,7 +1306,7 @@ describe API::Users do
get api('/user/gpg_keys', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['key']).to eq(gpg_key.key)
@@ -1346,14 +1328,14 @@ describe API::Users do
get api("/user/gpg_keys/#{gpg_key.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_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(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
@@ -1363,14 +1345,14 @@ describe API::Users do
get api("/user/gpg_keys/#{gpg_key.id}", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_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)
+ expect(response).to have_gitlab_http_status(404)
end
context 'scopes' do
@@ -1387,20 +1369,20 @@ describe API::Users do
expect do
post api('/user/gpg_keys', user), key_attrs
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_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)
+ expect(response).to have_gitlab_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(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('key is missing')
end
end
@@ -1413,14 +1395,14 @@ describe API::Users do
expect do
post api("/user/gpg_keys/#{gpg_key.id}/revoke", user)
- expect(response).to have_http_status(:accepted)
+ expect(response).to have_gitlab_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(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
@@ -1430,13 +1412,13 @@ describe API::Users do
post api("/user/gpg_keys/#{gpg_key.id}/revoke")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_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)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1448,14 +1430,14 @@ describe API::Users do
expect do
delete api("/user/gpg_keys/#{gpg_key.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_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(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
@@ -1465,13 +1447,13 @@ describe API::Users do
delete api("/user/gpg_keys/#{gpg_key.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_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)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1479,7 +1461,7 @@ describe API::Users do
context "when unauthenticated" do
it "returns authentication error" do
get api("/user/emails")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -1490,7 +1472,7 @@ describe API::Users do
get api("/user/emails", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first["email"]).to eq(email.email)
@@ -1510,13 +1492,13 @@ describe API::Users do
user.emails << email
user.save
get api("/user/emails/#{email.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["email"]).to eq(email.email)
end
it "returns 404 Not Found within invalid ID" do
get api("/user/emails/42", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Email Not Found')
end
@@ -1525,14 +1507,14 @@ describe API::Users do
user.save
admin
get api("/user/emails/#{email.id}", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Email Not Found')
end
it "returns 404 for invalid ID" do
get api("/users/emails/ASDF", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context "scopes" do
@@ -1549,18 +1531,18 @@ describe API::Users do
expect do
post api("/user/emails", user), email_attrs
end.to change { user.emails.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
it "returns a 401 error if unauthorized" do
post api("/user/emails"), email: 'some email'
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it "does not create email with invalid email" do
post api("/user/emails", user), {}
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('email is missing')
end
end
@@ -1573,7 +1555,7 @@ describe API::Users do
expect do
delete api("/user/emails/#{email.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { user.emails.count}.by(-1)
end
@@ -1584,7 +1566,7 @@ describe API::Users do
it "returns 404 if email ID not found" do
delete api("/user/emails/42", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Email Not Found')
end
@@ -1592,13 +1574,13 @@ describe API::Users do
user.emails << email
user.save
delete api("/user/emails/#{email.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it "returns 400 for invalid ID" do
delete api("/user/emails/ASDF", admin)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -1609,25 +1591,25 @@ describe API::Users do
it 'blocks existing user' do
post api("/users/#{user.id}/block", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(user.reload.state).to eq('blocked')
end
it 'does not re-block ldap blocked users' do
post api("/users/#{ldap_blocked_user.id}/block", admin)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
it 'does not be available for non admin users' do
post api("/users/#{user.id}/block", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(user.reload.state).to eq('active')
end
it 'returns a 404 error if user id not found' do
post api('/users/9999/block', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
end
@@ -1641,38 +1623,38 @@ describe API::Users do
it 'unblocks existing user' do
post api("/users/#{user.id}/unblock", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(user.reload.state).to eq('active')
end
it 'unblocks a blocked user' do
post api("/users/#{blocked_user.id}/unblock", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(blocked_user.reload.state).to eq('active')
end
it 'does not unblock ldap blocked users' do
post api("/users/#{ldap_blocked_user.id}/unblock", admin)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
it 'does not be available for non admin users' do
post api("/users/#{user.id}/unblock", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(user.reload.state).to eq('active')
end
it 'returns a 404 error if user id not found' do
post api('/users/9999/block', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 for invalid ID" do
post api("/users/ASDF/block", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1684,7 +1666,7 @@ describe API::Users do
it 'has no permission' do
get api("/user/activities", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -1729,21 +1711,21 @@ describe API::Users do
it 'returns a 404 error if user not found' do
get api("/users/#{not_existing_user_id}/impersonation_tokens", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns a 403 error when authenticated as normal user' do
get api("/users/#{not_existing_user_id}/impersonation_tokens", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(json_response['message']).to eq('403 Forbidden')
end
it 'returns an array of all impersonated tokens' do
get api("/users/#{user.id}/impersonation_tokens", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
@@ -1752,7 +1734,7 @@ describe API::Users do
it 'returns an array of active impersonation tokens if state active' do
get api("/users/#{user.id}/impersonation_tokens?state=active", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
@@ -1762,7 +1744,7 @@ describe API::Users do
it 'returns an array of inactive personal access tokens if active is set to false' do
get api("/users/#{user.id}/impersonation_tokens?state=inactive", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response).to all(include('active' => false))
@@ -1778,7 +1760,7 @@ describe API::Users do
it 'returns validation error if impersonation token misses some attributes' do
post api("/users/#{user.id}/impersonation_tokens", admin)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('name is missing')
end
@@ -1787,7 +1769,7 @@ describe API::Users do
name: name,
expires_at: expires_at
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -1796,7 +1778,7 @@ describe API::Users do
name: name,
expires_at: expires_at
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(json_response['message']).to eq('403 Forbidden')
end
@@ -1807,7 +1789,7 @@ describe API::Users do
scopes: scopes,
impersonation: impersonation
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(name)
expect(json_response['scopes']).to eq(scopes)
expect(json_response['expires_at']).to eq(expires_at)
@@ -1827,35 +1809,35 @@ describe API::Users do
it 'returns 404 error if user not found' do
get api("/users/#{not_existing_user_id}/impersonation_tokens/1", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns a 404 error if impersonation token not found' do
get api("/users/#{user.id}/impersonation_tokens/#{not_existing_pat_id}", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Impersonation Token Not Found')
end
it 'returns a 404 error if token is not impersonation token' do
get api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Impersonation Token Not Found')
end
it 'returns a 403 error when authenticated as normal user' do
get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(json_response['message']).to eq('403 Forbidden')
end
it 'returns a personal access token' do
get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['token']).to be_present
expect(json_response['impersonation']).to be_truthy
end
@@ -1868,28 +1850,28 @@ describe API::Users do
it 'returns a 404 error if user not found' do
delete api("/users/#{not_existing_user_id}/impersonation_tokens/1", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns a 404 error if impersonation token not found' do
delete api("/users/#{user.id}/impersonation_tokens/#{not_existing_pat_id}", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Impersonation Token Not Found')
end
it 'returns a 404 error if token is not impersonation token' do
delete api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Impersonation Token Not Found')
end
it 'returns a 403 error when authenticated as normal user' do
delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(json_response['message']).to eq('403 Forbidden')
end
@@ -1900,13 +1882,14 @@ describe API::Users do
it 'revokes a impersonation token' do
delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(impersonation_token.revoked).to be_falsey
expect(impersonation_token.reload.revoked).to be_truthy
end
end
- include_examples 'custom attributes endpoints', 'users' do
+ it_behaves_like 'custom attributes endpoints', 'users' do
let(:attributable) { user }
+ let(:other_attributable) { admin }
end
end
diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb
index 36d793f505d..0cd8b70007f 100644
--- a/spec/requests/api/v3/award_emoji_spec.rb
+++ b/spec/requests/api/v3/award_emoji_spec.rb
@@ -16,7 +16,7 @@ describe API::V3::AwardEmoji do
it "returns an array of award_emoji" do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(award_emoji.name)
end
@@ -24,7 +24,7 @@ describe API::V3::AwardEmoji do
it "returns a 404 error when issue id not found" do
get v3_api("/projects/#{project.id}/issues/12345/award_emoji", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -32,7 +32,7 @@ describe API::V3::AwardEmoji do
it "returns an array of award_emoji" do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(downvote.name)
@@ -46,7 +46,7 @@ describe API::V3::AwardEmoji do
it 'returns the awarded emoji' do
get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(award.name)
end
@@ -58,7 +58,7 @@ describe API::V3::AwardEmoji do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -69,7 +69,7 @@ describe API::V3::AwardEmoji do
it 'returns an array of award emoji' do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(rocket.name)
end
@@ -80,7 +80,7 @@ describe API::V3::AwardEmoji do
it "returns the award emoji" do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(award_emoji.name)
expect(json_response['awardable_id']).to eq(issue.id)
expect(json_response['awardable_type']).to eq("Issue")
@@ -89,7 +89,7 @@ describe API::V3::AwardEmoji do
it "returns a 404 error if the award is not found" do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -97,7 +97,7 @@ describe API::V3::AwardEmoji do
it 'returns the award emoji' do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(downvote.name)
expect(json_response['awardable_id']).to eq(merge_request.id)
expect(json_response['awardable_type']).to eq("MergeRequest")
@@ -111,7 +111,7 @@ describe API::V3::AwardEmoji do
it 'returns the awarded emoji' do
get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(award.name)
expect(json_response['awardable_id']).to eq(snippet.id)
expect(json_response['awardable_type']).to eq("Snippet")
@@ -124,7 +124,7 @@ describe API::V3::AwardEmoji do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -135,7 +135,7 @@ describe API::V3::AwardEmoji do
it 'returns an award emoji' do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).not_to be_an Array
expect(json_response['name']).to eq(rocket.name)
end
@@ -148,7 +148,7 @@ describe API::V3::AwardEmoji do
it "creates a new award emoji" do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq('blowfish')
expect(json_response['user']['username']).to eq(user.username)
end
@@ -156,19 +156,19 @@ describe API::V3::AwardEmoji do
it "returns a 400 bad request error if the name is not given" do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 401 unauthorized error if the user is not authenticated" do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup'
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it "returns a 404 error if the user authored issue" do
post v3_api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "normalizes +1 as thumbsup award" do
@@ -182,7 +182,7 @@ describe API::V3::AwardEmoji do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response["message"]).to match("has already been taken")
end
end
@@ -194,7 +194,7 @@ describe API::V3::AwardEmoji do
post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq('blowfish')
expect(json_response['user']['username']).to eq(user.username)
end
@@ -209,14 +209,14 @@ describe API::V3::AwardEmoji do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
end.to change { note.award_emoji.count }.from(0).to(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['user']['username']).to eq(user.username)
end
it "it returns 404 error when user authored note" do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "normalizes +1 as thumbsup award" do
@@ -230,7 +230,7 @@ describe API::V3::AwardEmoji do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response["message"]).to match("has already been taken")
end
end
@@ -242,14 +242,14 @@ describe API::V3::AwardEmoji do
expect do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change { issue.award_emoji.count }.from(1).to(0)
end
it 'returns a 404 error when the award emoji can not be found' do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -258,14 +258,14 @@ describe API::V3::AwardEmoji do
expect do
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change { merge_request.award_emoji.count }.from(1).to(0)
end
it 'returns a 404 error when note id not found' do
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -277,7 +277,7 @@ describe API::V3::AwardEmoji do
expect do
delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change { snippet.award_emoji.count }.from(1).to(0)
end
end
@@ -290,7 +290,7 @@ describe API::V3::AwardEmoji do
expect do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change { note.award_emoji.count }.from(1).to(0)
end
end
diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb
index ea2627142bf..14409d25544 100644
--- a/spec/requests/api/v3/boards_spec.rb
+++ b/spec/requests/api/v3/boards_spec.rb
@@ -38,7 +38,7 @@ describe API::V3::Boards do
it "returns authentication error" do
get v3_api(base_url)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -46,7 +46,7 @@ describe API::V3::Boards do
it "returns the project issue board" do
get v3_api(base_url, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(board.id)
@@ -63,7 +63,7 @@ describe API::V3::Boards do
it 'returns issue board lists' do
get v3_api(base_url, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['label']['name']).to eq(dev_label.title)
@@ -72,7 +72,7 @@ describe API::V3::Boards do
it 'returns 404 if board not found' do
get v3_api("/projects/#{project.id}/boards/22343/lists", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -82,19 +82,19 @@ describe API::V3::Boards do
it "rejects a non member from deleting a list" do
delete v3_api("#{base_url}/#{dev_list.id}", non_member)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "rejects a user with guest role from deleting a list" do
delete v3_api("#{base_url}/#{dev_list.id}", guest)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "returns 404 error if list id not found" do
delete v3_api("#{base_url}/44444", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context "when the user is project owner" do
@@ -107,7 +107,7 @@ describe API::V3::Boards do
it "deletes the list if an admin requests it" do
delete v3_api("#{base_url}/#{dev_list.id}", owner)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb
index 9cd11a67712..1e038595a1f 100644
--- a/spec/requests/api/v3/branches_spec.rb
+++ b/spec/requests/api/v3/branches_spec.rb
@@ -17,7 +17,7 @@ describe API::V3::Branches do
get v3_api("/projects/#{project.id}/repository/branches", user), per_page: 100
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
branch_names = json_response.map { |x| x['name'] }
expect(branch_names).to match_array(project.repository.branch_names)
@@ -32,20 +32,20 @@ describe API::V3::Branches do
it "removes branch" do
delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['branch_name']).to eq(branch_name)
end
it "removes a branch with dots in the branch name" do
delete v3_api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['branch_name']).to eq("with.1.2.3")
end
it 'returns 404 if branch not exists' do
delete v3_api("/projects/#{project.id}/repository/branches/foobar", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -57,13 +57,13 @@ describe API::V3::Branches do
it 'returns 200' do
delete v3_api("/projects/#{project.id}/repository/merged_branches", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns a 403 error if guest' do
delete v3_api("/projects/#{project.id}/repository/merged_branches", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -73,7 +73,7 @@ describe API::V3::Branches do
branch_name: 'feature1',
ref: branch_sha
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq('feature1')
expect(json_response['commit']['id']).to eq(branch_sha)
@@ -83,14 +83,14 @@ describe API::V3::Branches do
post v3_api("/projects/#{project.id}/repository/branches", user2),
branch_name: branch_name,
ref: branch_sha
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'returns 400 if branch name is invalid' do
post v3_api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new design',
ref: branch_sha
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Branch name is invalid')
end
@@ -98,13 +98,13 @@ describe API::V3::Branches do
post v3_api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new_design1',
ref: branch_sha
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
post v3_api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new_design1',
ref: branch_sha
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Branch already exists')
end
@@ -113,7 +113,7 @@ describe API::V3::Branches do
branch_name: 'new_design3',
ref: 'foo'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Invalid reference name')
end
end
diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb
index d04b1c72004..d9641011491 100644
--- a/spec/requests/api/v3/broadcast_messages_spec.rb
+++ b/spec/requests/api/v3/broadcast_messages_spec.rb
@@ -11,21 +11,21 @@ describe API::V3::BroadcastMessages do
delete v3_api("/broadcast_messages/#{message.id}"),
attributes_for(:broadcast_message)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns a 403 for users' do
delete v3_api("/broadcast_messages/#{message.id}", user),
attributes_for(:broadcast_message)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'deletes the broadcast message for admins' do
expect do
delete v3_api("/broadcast_messages/#{message.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change { BroadcastMessage.count }.by(-1)
end
end
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index 0a2ff1058e3..a73bb456b52 100644
--- a/spec/requests/api/v3/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -21,7 +21,7 @@ describe API::V3::Builds do
context 'authorized user' do
it 'returns project builds' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
@@ -44,7 +44,7 @@ describe API::V3::Builds do
let(:query) { 'scope=pending' }
it do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
end
end
@@ -54,7 +54,7 @@ describe API::V3::Builds do
let(:json_build) { json_response.first }
it 'return builds with status skipped' do
- expect(response).to have_http_status 200
+ expect(response).to have_gitlab_http_status 200
expect(json_response).to be_an Array
expect(json_response.length).to eq 1
expect(json_build['status']).to eq 'skipped'
@@ -65,7 +65,7 @@ describe API::V3::Builds do
let(:query) { 'scope[0]=pending&scope[1]=running' }
it do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
end
end
@@ -73,7 +73,7 @@ describe API::V3::Builds do
context 'respond 400 when scope contains invalid state' do
let(:query) { 'scope[0]=pending&scope[1]=unknown_status' }
- it { expect(response).to have_http_status(400) }
+ it { expect(response).to have_gitlab_http_status(400) }
end
end
@@ -81,7 +81,7 @@ describe API::V3::Builds do
let(:api_user) { nil }
it 'does not return project builds' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -93,7 +93,7 @@ describe API::V3::Builds do
end
it 'responds with 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -109,7 +109,7 @@ describe API::V3::Builds do
end
it 'returns project jobs for specific commit' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq 2
@@ -132,7 +132,7 @@ describe API::V3::Builds do
end
it 'returns an empty array' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response).to be_empty
end
@@ -148,7 +148,7 @@ describe API::V3::Builds do
end
it 'does not return project jobs' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
expect(json_response.except('message')).to be_empty
end
end
@@ -162,7 +162,7 @@ describe API::V3::Builds do
context 'authorized user' do
it 'returns specific job data' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq('test')
end
@@ -180,7 +180,7 @@ describe API::V3::Builds do
let(:api_user) { nil }
it 'does not return specific job data' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -200,7 +200,7 @@ describe API::V3::Builds do
end
it 'returns specific job artifacts' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.headers).to include(download_headers)
expect(response.body).to match_file(build.artifacts_file.file.file)
end
@@ -210,13 +210,13 @@ describe API::V3::Builds do
let(:api_user) { nil }
it 'does not return specific job artifacts' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
it 'does not return job artifacts if not uploaded' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -240,7 +240,7 @@ describe API::V3::Builds do
end
it 'gives 401' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -252,13 +252,13 @@ describe API::V3::Builds do
end
it 'gives 403' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
context 'non-existing job' do
shared_examples 'not found' do
- it { expect(response).to have_http_status(:not_found) }
+ it { expect(response).to have_gitlab_http_status(:not_found) }
end
context 'has no such ref' do
@@ -286,7 +286,7 @@ describe API::V3::Builds do
"attachment; filename=#{build.artifacts_file.filename}" }
end
- it { expect(response).to have_http_status(200) }
+ it { expect(response).to have_gitlab_http_status(200) }
it { expect(response.headers).to include(download_headers) }
end
@@ -327,7 +327,7 @@ describe API::V3::Builds do
context 'authorized user' do
it 'returns specific job trace' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.body).to eq(build.trace.raw)
end
end
@@ -336,7 +336,7 @@ describe API::V3::Builds do
let(:api_user) { nil }
it 'does not return specific job trace' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -349,7 +349,7 @@ describe API::V3::Builds do
context 'authorized user' do
context 'user with :update_build persmission' do
it 'cancels running or pending job' do
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(project.builds.first.status).to eq('canceled')
end
end
@@ -358,7 +358,7 @@ describe API::V3::Builds do
let(:api_user) { reporter.user }
it 'does not cancel job' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -367,7 +367,7 @@ describe API::V3::Builds do
let(:api_user) { nil }
it 'does not cancel job' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -382,7 +382,7 @@ describe API::V3::Builds do
context 'authorized user' do
context 'user with :update_build permission' do
it 'retries non-running job' do
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(project.builds.first.status).to eq('canceled')
expect(json_response['status']).to eq('pending')
end
@@ -392,7 +392,7 @@ describe API::V3::Builds do
let(:api_user) { reporter.user }
it 'does not retry job' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -401,13 +401,15 @@ describe API::V3::Builds do
let(:api_user) { nil }
it 'does not retry job' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
describe 'POST /projects/:id/builds/:build_id/erase' do
before do
+ project.add_master(user)
+
post v3_api("/projects/#{project.id}/builds/#{build.id}/erase", user)
end
@@ -471,7 +473,7 @@ describe API::V3::Builds do
let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
it 'plays the job' do
- expect(response).to have_http_status 200
+ expect(response).to have_gitlab_http_status 200
expect(json_response['user']['id']).to eq(user.id)
expect(json_response['id']).to eq(build.id)
end
@@ -479,7 +481,7 @@ describe API::V3::Builds do
context 'on a non-playable job' do
it 'returns a status code 400, Bad Request' do
- expect(response).to have_http_status 400
+ expect(response).to have_gitlab_http_status 400
expect(response.body).to match("Unplayable Job")
end
end
diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb
index 6d0ca33a6fa..d31c94ddd2c 100644
--- a/spec/requests/api/v3/commits_spec.rb
+++ b/spec/requests/api/v3/commits_spec.rb
@@ -19,7 +19,7 @@ describe API::V3::Commits do
commit = project.repository.commit
get v3_api("/projects/#{project.id}/repository/commits", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(commit.id)
expect(json_response.first['committer_name']).to eq(commit.committer_name)
@@ -30,7 +30,7 @@ describe API::V3::Commits do
context "unauthorized user" do
it "does not return project commits" do
get v3_api("/projects/#{project.id}/repository/commits")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -69,7 +69,7 @@ describe API::V3::Commits do
it "returns an invalid parameter error message" do
get v3_api("/projects/#{project.id}/repository/commits?since=invalid-date", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('since is invalid')
end
end
@@ -92,13 +92,13 @@ describe API::V3::Commits do
it 'returns a 403 unauthorized for user without permissions' do
post v3_api(url, user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'returns a 400 bad request if no params are given' do
post v3_api(url, user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
describe 'create' do
@@ -133,7 +133,7 @@ describe API::V3::Commits do
it 'a new file in project repo' do
post v3_api(url, user), valid_c_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq(message)
expect(json_response['committer_name']).to eq(user.name)
expect(json_response['committer_email']).to eq(user.email)
@@ -142,7 +142,7 @@ describe API::V3::Commits do
it 'returns a 400 bad request if file exists' do
post v3_api(url, user), invalid_c_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context 'with project path containing a dot in URL' do
@@ -152,7 +152,7 @@ describe API::V3::Commits do
it 'a new file in project repo' do
post v3_api(url, user), valid_c_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
end
end
@@ -187,14 +187,14 @@ describe API::V3::Commits do
it 'an existing file in project repo' do
post v3_api(url, user), valid_d_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq(message)
end
it 'returns a 400 bad request if file does not exist' do
post v3_api(url, user), invalid_d_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -232,14 +232,14 @@ describe API::V3::Commits do
it 'an existing file in project repo' do
post v3_api(url, user), valid_m_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq(message)
end
it 'returns a 400 bad request if file does not exist' do
post v3_api(url, user), invalid_m_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -275,14 +275,14 @@ describe API::V3::Commits do
it 'an existing file in project repo' do
post v3_api(url, user), valid_u_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq(message)
end
it 'returns a 400 bad request if file does not exist' do
post v3_api(url, user), invalid_u_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -348,14 +348,14 @@ describe API::V3::Commits do
it 'are commited as one in project repo' do
post v3_api(url, user), valid_mo_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq(message)
end
it 'return a 400 bad request if there are any issues' do
post v3_api(url, user), invalid_mo_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
end
@@ -365,7 +365,7 @@ describe API::V3::Commits do
it "returns a commit by sha" do
get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(project.repository.commit.id)
expect(json_response['title']).to eq(project.repository.commit.title)
expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions)
@@ -375,13 +375,13 @@ describe API::V3::Commits do
it "returns a 404 error if not found" do
get v3_api("/projects/#{project.id}/repository/commits/invalid_sha", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns nil for commit without CI" do
get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to be_nil
end
@@ -391,7 +391,7 @@ describe API::V3::Commits do
get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to eq(pipeline.status)
end
@@ -400,7 +400,7 @@ describe API::V3::Commits do
get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to eq("created")
end
end
@@ -408,7 +408,7 @@ describe API::V3::Commits do
context "unauthorized user" do
it "does not return the selected commit" do
get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -419,7 +419,7 @@ describe API::V3::Commits do
it "returns the diff of the selected commit" do
get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to be >= 1
@@ -428,14 +428,14 @@ describe API::V3::Commits do
it "returns a 404 error if invalid commit" do
get v3_api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
context "unauthorized user" do
it "does not return the diff of the selected commit" do
get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -444,7 +444,7 @@ describe API::V3::Commits do
context 'authorized user' do
it 'returns merge_request comments' do
get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['note']).to eq('a comment on a commit')
@@ -453,14 +453,14 @@ describe API::V3::Commits do
it 'returns a 404 error if merge_request_id not found' do
get v3_api("/projects/#{project.id}/repository/commits/1234ab/comments", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
context 'unauthorized user' do
it 'does not return the diff of the selected commit' do
get v3_api("/projects/#{project.id}/repository/commits/1234ab/comments")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -472,7 +472,7 @@ describe API::V3::Commits do
it 'cherry picks a commit' do
post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'master'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq(master_pickable_commit.title)
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)
@@ -482,7 +482,7 @@ describe API::V3::Commits do
it 'returns 400 if commit is already included in the target branch' do
post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to include('Sorry, we cannot cherry-pick this commit automatically.')
end
@@ -492,35 +492,35 @@ describe API::V3::Commits do
post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('You are not allowed to push into this branch')
end
it 'returns 400 for missing parameters' do
post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('branch is missing')
end
it 'returns 404 if commit is not found' do
post v3_api("/projects/#{project.id}/repository/commits/abcd0123/cherry_pick", user), branch: 'master'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Commit Not Found')
end
it 'returns 404 if branch is not found' do
post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'foo'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Branch Not Found')
end
it 'returns 400 for missing parameters' do
post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('branch is missing')
end
end
@@ -529,7 +529,7 @@ describe API::V3::Commits do
it 'does not cherry pick the commit' do
post v3_api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick"), branch: 'master'
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -538,7 +538,7 @@ describe API::V3::Commits do
context 'authorized user' do
it 'returns comment' do
post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['note']).to eq('My comment')
expect(json_response['path']).to be_nil
expect(json_response['line']).to be_nil
@@ -548,7 +548,7 @@ describe API::V3::Commits do
it 'returns the inline comment' do
post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['note']).to eq('My comment')
expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path)
expect(json_response['line']).to eq(1)
@@ -557,19 +557,19 @@ describe API::V3::Commits do
it 'returns 400 if note is missing' do
post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 404 if note is attached to non existent commit' do
post v3_api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
context 'unauthorized user' do
it 'does not return the diff of the selected commit' do
post v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb
index 2affd0cfa51..785bc1eb4ba 100644
--- a/spec/requests/api/v3/deploy_keys_spec.rb
+++ b/spec/requests/api/v3/deploy_keys_spec.rb
@@ -46,7 +46,7 @@ describe API::V3::DeployKeys do
it 'should return array of ssh keys' do
get v3_api("/projects/#{project.id}/#{path}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(deploy_key.title)
end
@@ -56,14 +56,14 @@ describe API::V3::DeployKeys do
it 'should return a single key' do
get v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(deploy_key.title)
end
it 'should return 404 Not Found with invalid ID' do
get v3_api("/projects/#{project.id}/#{path}/404", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -71,14 +71,14 @@ describe API::V3::DeployKeys do
it 'should not create an invalid ssh key' do
post v3_api("/projects/#{project.id}/#{path}", admin), { title: 'invalid key' }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('key is missing')
end
it 'should not create a key without title' do
post v3_api("/projects/#{project.id}/#{path}", admin), key: 'some key'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('title is missing')
end
@@ -95,7 +95,7 @@ describe API::V3::DeployKeys do
post v3_api("/projects/#{project.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
end.not_to change { project.deploy_keys.count }
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
it 'joins an existing ssh key to a new project' do
@@ -103,7 +103,7 @@ describe API::V3::DeployKeys do
post v3_api("/projects/#{project2.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
end.to change { project2.deploy_keys.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
it 'accepts can_push parameter' do
@@ -111,7 +111,7 @@ describe API::V3::DeployKeys do
post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['can_push']).to eq(true)
end
end
@@ -128,7 +128,7 @@ describe API::V3::DeployKeys do
it 'should return 404 Not Found with invalid ID' do
delete v3_api("/projects/#{project.id}/#{path}/404", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -141,7 +141,7 @@ describe API::V3::DeployKeys do
post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", admin)
end.to change { project2.deploy_keys.count }.from(0).to(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['id']).to eq(deploy_key.id)
end
end
@@ -150,7 +150,7 @@ describe API::V3::DeployKeys do
it 'should return a 404 error' do
post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -162,7 +162,7 @@ describe API::V3::DeployKeys do
delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", admin)
end.to change { project.deploy_keys.count }.from(1).to(0)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(deploy_key.id)
end
end
@@ -171,7 +171,7 @@ describe API::V3::DeployKeys do
it 'should return a 404 error' do
delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/v3/deployments_spec.rb b/spec/requests/api/v3/deployments_spec.rb
index 0389a264781..90eabda4dac 100644
--- a/spec/requests/api/v3/deployments_spec.rb
+++ b/spec/requests/api/v3/deployments_spec.rb
@@ -30,7 +30,7 @@ describe API::V3::Deployments do
it 'returns projects deployments' do
get v3_api("/projects/#{project.id}/deployments", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['iid']).to eq(deployment.iid)
@@ -42,7 +42,7 @@ describe API::V3::Deployments do
it 'returns a 404 status code' do
get v3_api("/projects/#{project.id}/deployments", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -52,7 +52,7 @@ describe API::V3::Deployments do
it 'returns the projects deployment' do
get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['sha']).to match /\A\h{40}\z/
expect(json_response['id']).to eq(deployment.id)
end
@@ -62,7 +62,7 @@ describe API::V3::Deployments do
it 'returns a 404 status code' do
get v3_api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb
index 39264e819a3..937250b5219 100644
--- a/spec/requests/api/v3/environments_spec.rb
+++ b/spec/requests/api/v3/environments_spec.rb
@@ -36,7 +36,7 @@ describe API::V3::Environments do
it 'returns project environments' do
get v3_api("/projects/#{project.id}/environments", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['name']).to eq(environment.name)
@@ -50,7 +50,7 @@ describe API::V3::Environments do
it 'returns a 404 status code' do
get v3_api("/projects/#{project.id}/environments", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -60,7 +60,7 @@ describe API::V3::Environments do
it 'creates a environment with valid params' do
post v3_api("/projects/#{project.id}/environments", user), name: "mepmep"
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq('mepmep')
expect(json_response['slug']).to eq('mepmep')
expect(json_response['external']).to be nil
@@ -69,19 +69,19 @@ describe API::V3::Environments do
it 'requires name to be passed' do
post v3_api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns a 400 if environment already exists' do
post v3_api("/projects/#{project.id}/environments", user), name: environment.name
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns a 400 if slug is specified' do
post v3_api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo"
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
end
end
@@ -90,7 +90,7 @@ describe API::V3::Environments do
it 'rejects the request' do
post v3_api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 400 when the required params are missing' do
@@ -105,7 +105,7 @@ describe API::V3::Environments do
put v3_api("/projects/#{project.id}/environments/#{environment.id}", user),
name: 'Mepmep', external_url: url
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq('Mepmep')
expect(json_response['external_url']).to eq(url)
end
@@ -115,7 +115,7 @@ describe API::V3::Environments do
api_url = v3_api("/projects/#{project.id}/environments/#{environment.id}", user)
put api_url, slug: slug + "-foo"
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
end
@@ -124,7 +124,7 @@ describe API::V3::Environments do
put v3_api("/projects/#{project.id}/environments/#{environment.id}", user),
name: 'Mepmep'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq('Mepmep')
expect(json_response['external_url']).to eq(url)
end
@@ -132,7 +132,7 @@ describe API::V3::Environments do
it 'returns a 404 if the environment does not exist' do
put v3_api("/projects/#{project.id}/environments/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -141,13 +141,13 @@ describe API::V3::Environments do
it 'returns a 200 for an existing environment' do
delete v3_api("/projects/#{project.id}/environments/#{environment.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns a 404 for non existing id' do
delete v3_api("/projects/#{project.id}/environments/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
end
@@ -156,7 +156,7 @@ describe API::V3::Environments do
it 'rejects the request' do
delete v3_api("/projects/#{project.id}/environments/#{environment.id}", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb
index dc7f0eefd16..5500c1cf770 100644
--- a/spec/requests/api/v3/files_spec.rb
+++ b/spec/requests/api/v3/files_spec.rb
@@ -36,7 +36,7 @@ describe API::V3::Files do
it "returns file info" do
get v3_api(route, current_user), params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['file_path']).to eq(file_path)
expect(json_response['file_name']).to eq('popen.rb')
expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
@@ -112,7 +112,7 @@ describe API::V3::Files do
it "creates a new file in project repo" do
post v3_api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['file_path']).to eq('newfile.rb')
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
@@ -122,7 +122,7 @@ describe API::V3::Files do
it "returns a 400 bad request if no params given" do
post v3_api("/projects/#{project.id}/repository/files", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 400 if editor fails to create file" do
@@ -131,7 +131,7 @@ describe API::V3::Files do
post v3_api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context "when specifying an author" do
@@ -140,7 +140,7 @@ describe API::V3::Files do
post v3_api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(author_email)
expect(last_commit.author_name).to eq(author_name)
@@ -153,7 +153,7 @@ describe API::V3::Files do
it "creates a new file in project repo" do
post v3_api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['file_path']).to eq('newfile.rb')
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
@@ -175,7 +175,7 @@ describe API::V3::Files do
it "updates existing file in project repo" do
put v3_api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['file_path']).to eq(file_path)
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
@@ -185,7 +185,7 @@ describe API::V3::Files do
it "returns a 400 bad request if no params given" do
put v3_api("/projects/#{project.id}/repository/files", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context "when specifying an author" do
@@ -194,7 +194,7 @@ describe API::V3::Files do
put v3_api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(author_email)
expect(last_commit.author_name).to eq(author_name)
@@ -214,7 +214,7 @@ describe API::V3::Files do
it "deletes existing file in project repo" do
delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['file_path']).to eq(file_path)
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
@@ -224,7 +224,7 @@ describe API::V3::Files do
it "returns a 400 bad request if no params given" do
delete v3_api("/projects/#{project.id}/repository/files", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 400 if fails to delete file" do
@@ -232,7 +232,7 @@ describe API::V3::Files do
delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context "when specifying an author" do
@@ -241,7 +241,7 @@ describe API::V3::Files do
delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(author_email)
expect(last_commit.author_name).to eq(author_name)
@@ -274,7 +274,7 @@ describe API::V3::Files do
it "remains unchanged" do
get v3_api("/projects/#{project.id}/repository/files", user), get_params
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['file_path']).to eq(file_path)
expect(json_response['file_name']).to eq(file_path)
expect(json_response['content']).to eq(put_params[:content])
diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
index 778fcc73c30..498cb42fad1 100644
--- a/spec/requests/api/v3/groups_spec.rb
+++ b/spec/requests/api/v3/groups_spec.rb
@@ -23,7 +23,7 @@ describe API::V3::Groups do
it "returns authentication error" do
get v3_api("/groups")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -31,7 +31,7 @@ describe API::V3::Groups do
it "normal user: returns an array of groups of user1" do
get v3_api("/groups", user1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response)
@@ -41,7 +41,7 @@ describe API::V3::Groups do
it "does not include statistics" do
get v3_api("/groups", user1), statistics: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).not_to include 'statistics'
end
@@ -51,7 +51,7 @@ describe API::V3::Groups do
it "admin: returns an array of all groups" do
get v3_api("/groups", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -59,7 +59,7 @@ describe API::V3::Groups do
it "does not include statistics by default" do
get v3_api("/groups", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
@@ -76,7 +76,7 @@ describe API::V3::Groups do
get v3_api("/groups", admin), statistics: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response)
.to satisfy_one { |group| group['statistics'] == attributes }
@@ -87,7 +87,7 @@ describe API::V3::Groups do
it "returns all groups excluding skipped groups" do
get v3_api("/groups", admin), skip_groups: [group2.id]
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -101,7 +101,7 @@ describe API::V3::Groups do
get v3_api("/groups", user1), all_available: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_groups).to contain_exactly(public_group.name, group1.name)
end
@@ -118,7 +118,7 @@ describe API::V3::Groups do
it "sorts by name ascending by default" do
get v3_api("/groups", user1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_groups).to eq([group3.name, group1.name])
end
@@ -126,7 +126,7 @@ describe API::V3::Groups do
it "sorts in descending order when passed" do
get v3_api("/groups", user1), sort: "desc"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_groups).to eq([group1.name, group3.name])
end
@@ -134,7 +134,7 @@ describe API::V3::Groups do
it "sorts by the order_by param" do
get v3_api("/groups", user1), order_by: "path"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_groups).to eq([group1.name, group3.name])
end
@@ -146,7 +146,7 @@ describe API::V3::Groups do
it 'returns authentication error' do
get v3_api('/groups/owned')
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -154,7 +154,7 @@ describe API::V3::Groups do
it 'returns an array of groups the user owns' do
get v3_api('/groups/owned', user2)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(group2.name)
@@ -170,7 +170,7 @@ describe API::V3::Groups do
get v3_api("/groups/#{group1.id}", user1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(group1.id)
expect(json_response['name']).to eq(group1.name)
expect(json_response['path']).to eq(group1.path)
@@ -192,13 +192,13 @@ describe API::V3::Groups do
it "does not return a non existing group" do
get v3_api("/groups/1328", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "does not return a group not attached to user1" do
get v3_api("/groups/#{group2.id}", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -206,14 +206,14 @@ describe API::V3::Groups do
it "returns any existing group" do
get v3_api("/groups/#{group2.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(group2.name)
end
it "does not return a non existing group" do
get v3_api("/groups/1328", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -221,20 +221,20 @@ describe API::V3::Groups do
it 'returns any existing group' do
get v3_api("/groups/#{group1.path}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(group1.name)
end
it 'does not return a non existing group' do
get v3_api('/groups/unknown', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'does not return a group not attached to user1' do
get v3_api("/groups/#{group2.path}", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -246,7 +246,7 @@ describe API::V3::Groups do
it 'updates the group' do
put v3_api("/groups/#{group1.id}", user1), name: new_group_name, request_access_enabled: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(new_group_name)
expect(json_response['request_access_enabled']).to eq(true)
end
@@ -254,7 +254,7 @@ describe API::V3::Groups do
it 'returns 404 for a non existing group' do
put v3_api('/groups/1328', user1), name: new_group_name
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -262,7 +262,7 @@ describe API::V3::Groups do
it 'updates the group' do
put v3_api("/groups/#{group1.id}", admin), name: new_group_name
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(new_group_name)
end
end
@@ -271,7 +271,7 @@ describe API::V3::Groups do
it 'does not updates the group' do
put v3_api("/groups/#{group1.id}", user2), name: new_group_name
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -279,7 +279,7 @@ describe API::V3::Groups do
it 'returns 404 when trying to update the group' do
put v3_api("/groups/#{group2.id}", user1), name: new_group_name
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -289,7 +289,7 @@ describe API::V3::Groups do
it "returns the group's projects" do
get v3_api("/groups/#{group1.id}/projects", user1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.length).to eq(2)
project_names = json_response.map { |proj| proj['name'] }
expect(project_names).to match_array([project1.name, project3.name])
@@ -299,7 +299,7 @@ describe API::V3::Groups do
it "returns the group's projects with simple representation" do
get v3_api("/groups/#{group1.id}/projects", user1), simple: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.length).to eq(2)
project_names = json_response.map { |proj| proj['name'] }
expect(project_names).to match_array([project1.name, project3.name])
@@ -311,7 +311,7 @@ describe API::V3::Groups do
get v3_api("/groups/#{group1.id}/projects", user1), visibility: 'public'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(public_project.name)
@@ -320,13 +320,13 @@ describe API::V3::Groups do
it "does not return a non existing group" do
get v3_api("/groups/1328/projects", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "does not return a group not attached to user1" do
get v3_api("/groups/#{group2.id}/projects", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "only returns projects to which user has access" do
@@ -334,7 +334,7 @@ describe API::V3::Groups do
get v3_api("/groups/#{group1.id}/projects", user3)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project3.name)
end
@@ -344,7 +344,7 @@ describe API::V3::Groups do
get v3_api("/groups/#{project2.group.id}/projects", user3), owned: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project2.name)
end
@@ -354,7 +354,7 @@ describe API::V3::Groups do
get v3_api("/groups/#{group1.id}/projects", user1), starred: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project1.name)
end
@@ -364,7 +364,7 @@ describe API::V3::Groups do
it "returns any existing group" do
get v3_api("/groups/#{group2.id}/projects", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(project2.name)
end
@@ -372,7 +372,7 @@ describe API::V3::Groups do
it "does not return a non existing group" do
get v3_api("/groups/1328/projects", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -380,7 +380,7 @@ describe API::V3::Groups do
it 'returns any existing group' do
get v3_api("/groups/#{group1.path}/projects", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_names = json_response.map { |proj| proj['name'] }
expect(project_names).to match_array([project1.name, project3.name])
end
@@ -388,13 +388,13 @@ describe API::V3::Groups do
it 'does not return a non existing group' do
get v3_api('/groups/unknown/projects', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'does not return a group not attached to user1' do
get v3_api("/groups/#{group2.path}/projects", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -404,7 +404,7 @@ describe API::V3::Groups do
it "does not create group" do
post v3_api("/groups", user1), attributes_for(:group)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -414,7 +414,7 @@ describe API::V3::Groups do
post v3_api("/groups", user3), group
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response["name"]).to eq(group[:name])
expect(json_response["path"]).to eq(group[:path])
@@ -428,7 +428,7 @@ describe API::V3::Groups do
post v3_api("/groups", user3), group
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response["full_path"]).to eq("#{parent.path}/#{group[:path]}")
expect(json_response["parent_id"]).to eq(parent.id)
@@ -437,20 +437,20 @@ describe API::V3::Groups do
it "does not create group, duplicate" do
post v3_api("/groups", user3), { name: 'Duplicate Test', path: group2.path }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(response.message).to eq("Bad Request")
end
it "returns 400 bad request error if name not given" do
post v3_api("/groups", user3), { path: group2.path }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns 400 bad request error if path not given" do
post v3_api("/groups", user3), { name: 'test' }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
end
@@ -460,7 +460,7 @@ describe API::V3::Groups do
it "removes group" do
delete v3_api("/groups/#{group1.id}", user1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "does not remove a group if not an owner" do
@@ -469,19 +469,19 @@ describe API::V3::Groups do
delete v3_api("/groups/#{group1.id}", user3)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "does not remove a non existing group" do
delete v3_api("/groups/1328", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "does not remove a group not attached to user1" do
delete v3_api("/groups/#{group2.id}", user1)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -489,13 +489,13 @@ describe API::V3::Groups do
it "removes any existing group" do
delete v3_api("/groups/#{group2.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "does not remove a non existing group" do
delete v3_api("/groups/1328", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -513,7 +513,7 @@ describe API::V3::Groups do
it "does not transfer project to group" do
post v3_api("/groups/#{group1.id}/projects/#{project.id}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -521,7 +521,7 @@ describe API::V3::Groups do
it "transfers project to group" do
post v3_api("/groups/#{group1.id}/projects/#{project.id}", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
context 'when using project path in URL' do
@@ -529,7 +529,7 @@ describe API::V3::Groups do
it "transfers project to group" do
post v3_api("/groups/#{group1.id}/projects/#{project_path}", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
end
@@ -537,7 +537,7 @@ describe API::V3::Groups do
it "does not transfer project to group" do
post v3_api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -547,7 +547,7 @@ describe API::V3::Groups do
it "transfers project to group" do
post v3_api("/groups/#{group1.path}/projects/#{project_path}", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
end
@@ -555,7 +555,7 @@ describe API::V3::Groups do
it "does not transfer project to group" do
post v3_api("/groups/noexist/projects/#{project_path}", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 86768d7397a..39a47a62f16 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -59,7 +59,7 @@ describe API::V3::Issues, :mailer do
it "returns authentication error" do
get v3_api("/issues")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -67,7 +67,7 @@ describe API::V3::Issues, :mailer do
it "returns an array of issues" do
get v3_api("/issues", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(issue.title)
expect(json_response.last).to have_key('web_url')
@@ -76,7 +76,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of closed issues' do
get v3_api('/issues?state=closed', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_issue.id)
@@ -85,7 +85,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of opened issues' do
get v3_api('/issues?state=opened', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(issue.id)
@@ -94,7 +94,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of all issues' do
get v3_api('/issues?state=all', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['id']).to eq(issue.id)
@@ -104,7 +104,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of labeled issues' do
get v3_api("/issues?labels=#{label.title}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
@@ -113,7 +113,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of labeled issues when at least one label matches' do
get v3_api("/issues?labels=#{label.title},foo,bar", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
@@ -122,7 +122,7 @@ describe API::V3::Issues, :mailer do
it 'returns an empty array if no issue matches labels' do
get v3_api('/issues?labels=foo,bar', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -130,7 +130,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of labeled issues matching given state' do
get v3_api("/issues?labels=#{label.title}&state=opened", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
@@ -140,7 +140,7 @@ describe API::V3::Issues, :mailer do
it 'returns an empty array if no issue matches labels and state filters' do
get v3_api("/issues?labels=#{label.title}&state=closed", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -148,7 +148,7 @@ describe API::V3::Issues, :mailer do
it 'returns an empty array if no issue matches milestone' do
get v3_api("/issues?milestone=#{empty_milestone.title}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -156,7 +156,7 @@ describe API::V3::Issues, :mailer do
it 'returns an empty array if milestone does not exist' do
get v3_api("/issues?milestone=foo", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -164,7 +164,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of issues in given milestone' do
get v3_api("/issues?milestone=#{milestone.title}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['id']).to eq(issue.id)
@@ -175,7 +175,7 @@ describe API::V3::Issues, :mailer do
get v3_api("/issues?milestone=#{milestone.title}", user),
'&state=closed'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_issue.id)
@@ -184,7 +184,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of issues with no milestone' do
get v3_api("/issues?milestone=#{no_milestone_title}", author)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(confidential_issue.id)
@@ -195,7 +195,7 @@ describe API::V3::Issues, :mailer do
response_dates = json_response.map { |issue| issue['created_at'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -205,7 +205,7 @@ describe API::V3::Issues, :mailer do
response_dates = json_response.map { |issue| issue['created_at'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
@@ -215,7 +215,7 @@ describe API::V3::Issues, :mailer do
response_dates = json_response.map { |issue| issue['updated_at'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -225,7 +225,7 @@ describe API::V3::Issues, :mailer do
response_dates = json_response.map { |issue| issue['updated_at'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
@@ -233,7 +233,7 @@ describe API::V3::Issues, :mailer do
it 'matches V3 response schema' do
get v3_api('/issues', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v3/issues')
end
end
@@ -285,7 +285,7 @@ describe API::V3::Issues, :mailer do
it 'returns all group issues (including opened and closed)' do
get v3_api(base_url, admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
end
@@ -293,7 +293,7 @@ describe API::V3::Issues, :mailer do
it 'returns group issues without confidential issues for non project members' do
get v3_api("#{base_url}?state=opened", non_member)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['title']).to eq(group_issue.title)
@@ -302,7 +302,7 @@ describe API::V3::Issues, :mailer do
it 'returns group confidential issues for author' do
get v3_api("#{base_url}?state=opened", author)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -310,7 +310,7 @@ describe API::V3::Issues, :mailer do
it 'returns group confidential issues for assignee' do
get v3_api("#{base_url}?state=opened", assignee)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -318,7 +318,7 @@ describe API::V3::Issues, :mailer do
it 'returns group issues with confidential issues for project members' do
get v3_api("#{base_url}?state=opened", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -326,7 +326,7 @@ describe API::V3::Issues, :mailer do
it 'returns group confidential issues for admin' do
get v3_api("#{base_url}?state=opened", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -334,7 +334,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of labeled group issues' do
get v3_api("#{base_url}?labels=#{group_label.title}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([group_label.title])
@@ -343,7 +343,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of labeled group issues where all labels match' do
get v3_api("#{base_url}?labels=#{group_label.title},foo,bar", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -351,7 +351,7 @@ describe API::V3::Issues, :mailer do
it 'returns an empty array if no group issue matches labels' do
get v3_api("#{base_url}?labels=foo,bar", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -359,7 +359,7 @@ describe API::V3::Issues, :mailer do
it 'returns an empty array if no issue matches milestone' do
get v3_api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -367,7 +367,7 @@ describe API::V3::Issues, :mailer do
it 'returns an empty array if milestone does not exist' do
get v3_api("#{base_url}?milestone=foo", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -375,7 +375,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of issues in given milestone' do
get v3_api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(group_issue.id)
@@ -385,7 +385,7 @@ describe API::V3::Issues, :mailer do
get v3_api("#{base_url}?milestone=#{group_milestone.title}", user),
'&state=closed'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(group_closed_issue.id)
@@ -394,7 +394,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of issues with no milestone' do
get v3_api("#{base_url}?milestone=#{no_milestone_title}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(group_confidential_issue.id)
@@ -405,7 +405,7 @@ describe API::V3::Issues, :mailer do
response_dates = json_response.map { |issue| issue['created_at'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -415,7 +415,7 @@ describe API::V3::Issues, :mailer do
response_dates = json_response.map { |issue| issue['created_at'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
@@ -425,7 +425,7 @@ describe API::V3::Issues, :mailer do
response_dates = json_response.map { |issue| issue['updated_at'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -435,7 +435,7 @@ describe API::V3::Issues, :mailer do
response_dates = json_response.map { |issue| issue['updated_at'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
@@ -447,7 +447,7 @@ describe API::V3::Issues, :mailer do
it 'returns 404 when project does not exist' do
get v3_api('/projects/1000/issues', non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 on private projects for other users" do
@@ -456,7 +456,7 @@ describe API::V3::Issues, :mailer do
get v3_api("/projects/#{private_project.id}/issues", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns no issues when user has access to project but not issues' do
@@ -471,7 +471,7 @@ describe API::V3::Issues, :mailer do
it 'returns project issues without confidential issues for non project members' do
get v3_api("#{base_url}/issues", non_member)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['title']).to eq(issue.title)
@@ -480,7 +480,7 @@ describe API::V3::Issues, :mailer do
it 'returns project issues without confidential issues for project members with guest role' do
get v3_api("#{base_url}/issues", guest)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['title']).to eq(issue.title)
@@ -489,7 +489,7 @@ describe API::V3::Issues, :mailer do
it 'returns project confidential issues for author' do
get v3_api("#{base_url}/issues", author)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -498,7 +498,7 @@ describe API::V3::Issues, :mailer do
it 'returns project confidential issues for assignee' do
get v3_api("#{base_url}/issues", assignee)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -507,7 +507,7 @@ describe API::V3::Issues, :mailer do
it 'returns project issues with confidential issues for project members' do
get v3_api("#{base_url}/issues", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -516,7 +516,7 @@ describe API::V3::Issues, :mailer do
it 'returns project confidential issues for admin' do
get v3_api("#{base_url}/issues", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
@@ -525,7 +525,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of labeled project issues' do
get v3_api("#{base_url}/issues?labels=#{label.title}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
@@ -534,7 +534,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of labeled project issues where all labels match' do
get v3_api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label.title])
@@ -543,7 +543,7 @@ describe API::V3::Issues, :mailer do
it 'returns an empty array if no project issue matches labels' do
get v3_api("#{base_url}/issues?labels=foo,bar", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -551,7 +551,7 @@ describe API::V3::Issues, :mailer do
it 'returns an empty array if no issue matches milestone' do
get v3_api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -559,7 +559,7 @@ describe API::V3::Issues, :mailer do
it 'returns an empty array if milestone does not exist' do
get v3_api("#{base_url}/issues?milestone=foo", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -567,7 +567,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of issues in given milestone' do
get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['id']).to eq(issue.id)
@@ -578,7 +578,7 @@ describe API::V3::Issues, :mailer do
get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user),
'&state=closed'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_issue.id)
@@ -587,7 +587,7 @@ describe API::V3::Issues, :mailer do
it 'returns an array of issues with no milestone' do
get v3_api("#{base_url}/issues?milestone=#{no_milestone_title}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(confidential_issue.id)
@@ -598,7 +598,7 @@ describe API::V3::Issues, :mailer do
response_dates = json_response.map { |issue| issue['created_at'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -608,7 +608,7 @@ describe API::V3::Issues, :mailer do
response_dates = json_response.map { |issue| issue['created_at'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
@@ -618,7 +618,7 @@ describe API::V3::Issues, :mailer do
response_dates = json_response.map { |issue| issue['updated_at'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -628,7 +628,7 @@ describe API::V3::Issues, :mailer do
response_dates = json_response.map { |issue| issue['updated_at'] }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(response_dates).to eq(response_dates.sort)
end
@@ -638,7 +638,7 @@ describe API::V3::Issues, :mailer do
it 'exposes known attributes' do
get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(issue.id)
expect(json_response['iid']).to eq(issue.iid)
expect(json_response['project_id']).to eq(issue.project.id)
@@ -657,7 +657,7 @@ describe API::V3::Issues, :mailer do
it "returns a project issue by id" do
get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(issue.title)
expect(json_response['iid']).to eq(issue.iid)
end
@@ -682,26 +682,26 @@ describe API::V3::Issues, :mailer do
it "returns 404 if issue id not found" do
get v3_api("/projects/#{project.id}/issues/54321", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context 'confidential issues' do
it "returns 404 for non project members" do
get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 for project members with guest role" do
get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns confidential issue for project members" do
get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
@@ -709,7 +709,7 @@ describe API::V3::Issues, :mailer do
it "returns confidential issue for author" do
get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
@@ -717,7 +717,7 @@ describe API::V3::Issues, :mailer do
it "returns confidential issue for assignee" do
get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
@@ -725,7 +725,7 @@ describe API::V3::Issues, :mailer do
it "returns confidential issue for admin" do
get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
@@ -737,7 +737,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', assignee_id: assignee.id
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(%w(label label2))
@@ -749,7 +749,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: true
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_truthy
end
@@ -758,7 +758,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: 'y'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_truthy
end
@@ -767,7 +767,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: false
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['confidential']).to be_falsy
end
@@ -776,7 +776,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', confidential: 'foo'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('confidential is invalid')
end
@@ -795,7 +795,7 @@ describe API::V3::Issues, :mailer do
it "returns a 400 bad request if title not given" do
post v3_api("/projects/#{project.id}/issues", user), labels: 'label, label2'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'allows special label names' do
@@ -815,7 +815,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues", user),
title: 'g' * 256
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['title']).to eq([
'is too long (maximum is 255 characters)'
])
@@ -834,7 +834,7 @@ describe API::V3::Issues, :mailer do
end
it 'creates a new project issue' do
- expect(response).to have_http_status(:created)
+ expect(response).to have_gitlab_http_status(:created)
end
it 'resolves the discussions in a merge request' do
@@ -855,7 +855,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', due_date: due_date
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['due_date']).to eq(due_date)
@@ -868,7 +868,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', created_at: creation_time
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
end
@@ -899,7 +899,7 @@ describe API::V3::Issues, :mailer do
it "does not create a new project issue" do
expect { post v3_api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
spam_logs = SpamLog.all
@@ -917,7 +917,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
@@ -925,7 +925,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/44444", user),
title: 'updated title'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'allows special label names' do
@@ -946,21 +946,21 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
title: 'updated title'
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "returns 403 for project members with guest role" do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
title: 'updated title'
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "updates a confidential issue for project members" do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
title: 'updated title'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
@@ -968,7 +968,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
title: 'updated title'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
@@ -976,7 +976,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
title: 'updated title'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
@@ -984,7 +984,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
confidential: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['confidential']).to be_truthy
end
@@ -992,7 +992,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
confidential: false
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['confidential']).to be_falsy
end
@@ -1000,7 +1000,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
confidential: 'foo'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('confidential is invalid')
end
end
@@ -1021,7 +1021,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
spam_logs = SpamLog.all
@@ -1041,7 +1041,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([label.title])
end
@@ -1060,7 +1060,7 @@ describe API::V3::Issues, :mailer do
it 'removes all labels' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: ''
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([])
end
@@ -1068,7 +1068,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'foo,bar'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'foo'
expect(json_response['labels']).to include 'bar'
end
@@ -1092,7 +1092,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'g' * 256
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['title']).to eq([
'is too long (maximum is 255 characters)'
])
@@ -1104,7 +1104,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label2', state_event: "close"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'label2'
expect(json_response['state']).to eq "closed"
end
@@ -1112,7 +1112,7 @@ describe API::V3::Issues, :mailer do
it 'reopens a project isssue' do
put v3_api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['state']).to eq 'opened'
end
@@ -1122,7 +1122,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label3', state_event: 'close', updated_at: update_time
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'label3'
expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
end
@@ -1135,7 +1135,7 @@ describe API::V3::Issues, :mailer do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['due_date']).to eq(due_date)
end
end
@@ -1144,14 +1144,14 @@ describe API::V3::Issues, :mailer do
it 'updates an issue with no assignee' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), assignee_id: 0
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['assignee']).to eq(nil)
end
it 'updates an issue with assignee' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), assignee_id: user2.id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['assignee']['name']).to eq(user2.name)
end
end
@@ -1160,13 +1160,13 @@ describe API::V3::Issues, :mailer do
it "rejects a non member from deleting an issue" do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}", non_member)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "rejects a developer from deleting an issue" do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}", author)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
context "when the user is project owner" do
@@ -1176,7 +1176,7 @@ describe API::V3::Issues, :mailer do
it "deletes the issue if an admin requests it" do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}", owner)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['state']).to eq 'opened'
end
end
@@ -1185,7 +1185,7 @@ describe API::V3::Issues, :mailer do
it 'returns 404 when trying to move an issue' do
delete v3_api("/projects/#{project.id}/issues/123", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -1198,7 +1198,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: target_project.id
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['project_id']).to eq(target_project.id)
end
@@ -1207,7 +1207,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: project.id
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
end
end
@@ -1217,7 +1217,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: target_project2.id
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
end
end
@@ -1226,7 +1226,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", admin),
to_project_id: target_project2.id
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['project_id']).to eq(target_project2.id)
end
@@ -1235,7 +1235,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues/123/move", user),
to_project_id: target_project.id
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Issue Not Found')
end
end
@@ -1245,7 +1245,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/123/issues/#{issue.id}/move", user),
to_project_id: target_project.id
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
end
@@ -1255,7 +1255,7 @@ describe API::V3::Issues, :mailer do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
to_project_id: 123
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -1264,26 +1264,26 @@ describe API::V3::Issues, :mailer do
it 'subscribes to an issue' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
it 'returns 404 if the issue is not found' do
post v3_api("/projects/#{project.id}/issues/123/subscription", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the issue is confidential' do
post v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1291,26 +1291,26 @@ describe API::V3::Issues, :mailer do
it 'unsubscribes from an issue' do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
it 'returns 404 if the issue is not found' do
delete v3_api("/projects/#{project.id}/issues/123/subscription", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the issue is confidential' do
delete v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb
index 32f37a08024..1d31213d5ca 100644
--- a/spec/requests/api/v3/labels_spec.rb
+++ b/spec/requests/api/v3/labels_spec.rb
@@ -27,7 +27,7 @@ describe API::V3::Labels do
get v3_api("/projects/#{project.id}/labels", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
expect(json_response.first.keys).to match_array expected_keys
@@ -71,7 +71,7 @@ describe API::V3::Labels do
it "subscribes to the label" do
post v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_truthy
end
@@ -81,7 +81,7 @@ describe API::V3::Labels do
it "subscribes to the label" do
post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_truthy
end
@@ -93,7 +93,7 @@ describe API::V3::Labels do
it "returns 304" do
post v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
end
@@ -101,7 +101,7 @@ describe API::V3::Labels do
it "returns 404 error" do
post v3_api("/projects/#{project.id}/labels/1234/subscription", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -113,7 +113,7 @@ describe API::V3::Labels do
it "unsubscribes from the label" do
delete v3_api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_falsey
end
@@ -123,7 +123,7 @@ describe API::V3::Labels do
it "unsubscribes from the label" do
delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["name"]).to eq(label1.title)
expect(json_response["subscribed"]).to be_falsey
end
@@ -135,7 +135,7 @@ describe API::V3::Labels do
it "returns 304" do
delete v3_api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
end
@@ -143,7 +143,7 @@ describe API::V3::Labels do
it "returns 404 error" do
delete v3_api("/projects/#{project.id}/labels/1234/subscription", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -152,18 +152,18 @@ describe API::V3::Labels do
it 'returns 200 for existing label' do
delete v3_api("/projects/#{project.id}/labels", user), name: 'label1'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns 404 for non existing label' do
delete v3_api("/projects/#{project.id}/labels", user), name: 'label2'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Label Not Found')
end
it 'returns 400 for wrong parameters' do
delete v3_api("/projects/#{project.id}/labels", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
end
diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb
index bc918a8eb02..68be3d24c26 100644
--- a/spec/requests/api/v3/members_spec.rb
+++ b/spec/requests/api/v3/members_spec.rb
@@ -34,7 +34,7 @@ describe API::V3::Members do
user = public_send(type)
get v3_api("/#{source_type.pluralize}/#{source.id}/members", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(2)
expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
end
@@ -46,7 +46,7 @@ describe API::V3::Members do
get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(2)
expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
end
@@ -54,7 +54,7 @@ describe API::V3::Members do
it 'finds members with query string' do
get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.count).to eq(1)
expect(json_response.first['username']).to eq(master.username)
end
@@ -74,7 +74,7 @@ describe API::V3::Members do
user = public_send(type)
get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
# User attributes
expect(json_response['id']).to eq(developer.id)
expect(json_response['name']).to eq(developer.name)
@@ -109,7 +109,7 @@ describe API::V3::Members do
post v3_api("/#{source_type.pluralize}/#{source.id}/members", user),
user_id: access_requester.id, access_level: Member::MASTER
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -122,7 +122,7 @@ describe API::V3::Members do
post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: access_requester.id, access_level: Member::MASTER
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end.to change { source.members.count }.by(1)
expect(source.requesters.count).to eq(0)
expect(json_response['id']).to eq(access_requester.id)
@@ -135,7 +135,7 @@ describe API::V3::Members do
post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end.to change { source.members.count }.by(1)
expect(json_response['id']).to eq(stranger.id)
expect(json_response['access_level']).to eq(Member::DEVELOPER)
@@ -147,28 +147,28 @@ describe API::V3::Members do
post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: master.id, access_level: Member::MASTER
- expect(response).to have_http_status(source_type == 'project' ? 201 : 409)
+ expect(response).to have_gitlab_http_status(source_type == 'project' ? 201 : 409)
end
it 'returns 400 when user_id is not given' do
post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
access_level: Member::MASTER
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 400 when access_level is not given' do
post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: stranger.id
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 422 when access_level is not valid' do
post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: stranger.id, access_level: 1234
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
end
end
@@ -190,7 +190,7 @@ describe API::V3::Members do
put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user),
access_level: Member::MASTER
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -201,7 +201,7 @@ describe API::V3::Members do
put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
access_level: Member::MASTER, expires_at: '2016-08-05'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(developer.id)
expect(json_response['access_level']).to eq(Member::MASTER)
expect(json_response['expires_at']).to eq('2016-08-05')
@@ -212,20 +212,20 @@ describe API::V3::Members do
put v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master),
access_level: Member::MASTER
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 400 when access_level is not given' do
put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns 422 when access level is not valid' do
put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
access_level: 1234
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
end
end
@@ -243,7 +243,7 @@ describe API::V3::Members do
user = public_send(type)
delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -254,7 +254,7 @@ describe API::V3::Members do
expect do
delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change { source.members.count }.by(-1)
end
end
@@ -265,7 +265,7 @@ describe API::V3::Members do
expect do
delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
- expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ expect(response).to have_gitlab_http_status(source_type == 'project' ? 200 : 404)
end.not_to change { source.requesters.count }
end
end
@@ -274,7 +274,7 @@ describe API::V3::Members do
expect do
delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change { source.members.count }.by(-1)
end
end
@@ -282,7 +282,7 @@ describe API::V3::Members do
it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do
delete v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master)
- expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ expect(response).to have_gitlab_http_status(source_type == 'project' ? 200 : 404)
end
end
end
@@ -333,7 +333,7 @@ describe API::V3::Members do
post v3_api("/projects/#{project.id}/members", master),
user_id: stranger.id, access_level: Member::OWNER
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end.to change { project.members.count }.by(0)
end
end
diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb
index 3f21ff40726..e613036a88d 100644
--- a/spec/requests/api/v3/merge_request_diffs_spec.rb
+++ b/spec/requests/api/v3/merge_request_diffs_spec.rb
@@ -24,7 +24,7 @@ describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs' do
it 'returns a 404 when merge_request_id not found' do
get v3_api("/projects/#{project.id}/merge_requests/999/versions", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -42,7 +42,7 @@ describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs' do
it 'returns a 404 when merge_request_id not found' do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
index df73c731c96..2e2b9449429 100644
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -21,14 +21,14 @@ describe API::MergeRequests do
context "when unauthenticated" do
it "returns authentication error" do
get v3_api("/projects/#{project.id}/merge_requests")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
context "when authenticated" do
it "returns an array of all merge_requests" do
get v3_api("/projects/#{project.id}/merge_requests", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.last['title']).to eq(merge_request.title)
@@ -44,7 +44,7 @@ describe API::MergeRequests do
it "returns an array of all merge_requests" do
get v3_api("/projects/#{project.id}/merge_requests?state", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.last['title']).to eq(merge_request.title)
@@ -52,7 +52,7 @@ describe API::MergeRequests do
it "returns an array of open merge_requests" do
get v3_api("/projects/#{project.id}/merge_requests?state=opened", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.last['title']).to eq(merge_request.title)
@@ -60,7 +60,7 @@ describe API::MergeRequests do
it "returns an array of closed merge_requests" do
get v3_api("/projects/#{project.id}/merge_requests?state=closed", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['title']).to eq(merge_request_closed.title)
@@ -68,7 +68,7 @@ describe API::MergeRequests do
it "returns an array of merged merge_requests" do
get v3_api("/projects/#{project.id}/merge_requests?state=merged", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['title']).to eq(merge_request_merged.title)
@@ -77,7 +77,7 @@ describe API::MergeRequests do
it 'matches V3 response schema' do
get v3_api("/projects/#{project.id}/merge_requests", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v3/merge_requests')
end
@@ -89,7 +89,7 @@ describe API::MergeRequests do
it "returns an array of merge_requests in ascending order" do
get v3_api("/projects/#{project.id}/merge_requests?sort=asc", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
response_dates = json_response.map { |merge_request| merge_request['created_at'] }
@@ -98,7 +98,7 @@ describe API::MergeRequests do
it "returns an array of merge_requests in descending order" do
get v3_api("/projects/#{project.id}/merge_requests?sort=desc", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
response_dates = json_response.map { |merge_request| merge_request['created_at'] }
@@ -107,7 +107,7 @@ describe API::MergeRequests do
it "returns an array of merge_requests ordered by updated_at" do
get v3_api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
response_dates = json_response.map { |merge_request| merge_request['updated_at'] }
@@ -116,7 +116,7 @@ describe API::MergeRequests do
it "returns an array of merge_requests ordered by created_at" do
get v3_api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
response_dates = json_response.map { |merge_request| merge_request['created_at'] }
@@ -130,7 +130,7 @@ describe API::MergeRequests do
it 'exposes known attributes' do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(merge_request.id)
expect(json_response['iid']).to eq(merge_request.iid)
expect(json_response['project_id']).to eq(merge_request.project.id)
@@ -158,7 +158,7 @@ describe API::MergeRequests do
it "returns merge_request" do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(merge_request.title)
expect(json_response['iid']).to eq(merge_request.iid)
expect(json_response['work_in_progress']).to eq(false)
@@ -178,7 +178,7 @@ describe API::MergeRequests do
it 'returns merge_request by iid array' do
get v3_api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid]
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['title']).to eq merge_request_closed.title
@@ -187,7 +187,7 @@ describe API::MergeRequests do
it "returns a 404 error if merge_request_id not found" do
get v3_api("/projects/#{project.id}/merge_requests/999", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context 'Work in Progress' do
@@ -195,7 +195,7 @@ describe API::MergeRequests do
it "returns merge_request" do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['work_in_progress']).to eq(true)
end
end
@@ -214,7 +214,7 @@ describe API::MergeRequests do
it 'returns a 404 when merge_request_id not found' do
get v3_api("/projects/#{project.id}/merge_requests/999/commits", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -227,7 +227,7 @@ describe API::MergeRequests do
it 'returns a 404 when merge_request_id not found' do
get v3_api("/projects/#{project.id}/merge_requests/999/changes", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -243,7 +243,7 @@ describe API::MergeRequests do
milestone_id: milestone.id,
remove_source_branch: true
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['milestone']['id']).to eq(milestone.id)
@@ -253,25 +253,25 @@ describe API::MergeRequests do
it "returns 422 when source_branch equals target_branch" do
post v3_api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", source_branch: "master", target_branch: "master", author: user
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
it "returns 400 when source_branch is missing" do
post v3_api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", target_branch: "master", author: user
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns 400 when target_branch is missing" do
post v3_api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", source_branch: "markdown", author: user
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns 400 when title is missing" do
post v3_api("/projects/#{project.id}/merge_requests", user),
target_branch: 'master', source_branch: 'markdown'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'allows special label names' do
@@ -307,20 +307,18 @@ describe API::MergeRequests do
target_branch: 'master',
author: user
end.to change { MergeRequest.count }.by(0)
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
end
end
end
context 'forked projects' do
let!(:user2) { create(:user) }
- let!(:forked_project) { fork_project(project, user2) }
+ let!(:forked_project) { fork_project(project, user2, repository: true) }
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
before do
forked_project.add_reporter(user2)
-
- allow_any_instance_of(MergeRequest).to receive(:write_ref)
end
it "returns merge_request" do
@@ -403,7 +401,7 @@ describe API::MergeRequests do
it "denies the deletion of the merge request" do
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -411,7 +409,7 @@ describe API::MergeRequests do
it "destroys the merge request owners can destroy" do
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
@@ -422,7 +420,7 @@ describe API::MergeRequests do
it "returns merge_request in case of success" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "returns 406 if branch can't be merged" do
@@ -431,21 +429,21 @@ describe API::MergeRequests do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
- expect(response).to have_http_status(406)
+ expect(response).to have_gitlab_http_status(406)
expect(json_response['message']).to eq('Branch cannot be merged')
end
it "returns 405 if merge_request is not open" do
merge_request.close
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
- expect(response).to have_http_status(405)
+ expect(response).to have_gitlab_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
it "returns 405 if merge_request is a work in progress" do
merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
- expect(response).to have_http_status(405)
+ expect(response).to have_gitlab_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
@@ -454,7 +452,7 @@ describe API::MergeRequests do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
- expect(response).to have_http_status(405)
+ expect(response).to have_gitlab_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
@@ -462,21 +460,21 @@ describe API::MergeRequests do
user2 = create(:user)
project.team << [user2, :reporter]
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
expect(json_response['message']).to eq('401 Unauthorized')
end
it "returns 409 if the SHA parameter doesn't match" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
expect(json_response['message']).to start_with('SHA does not match HEAD of source branch')
end
it "succeeds if the SHA parameter matches" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "enables merge when pipeline succeeds if the pipeline is active" do
@@ -485,7 +483,7 @@ describe API::MergeRequests do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('Test')
expect(json_response['merge_when_build_succeeds']).to eq(true)
end
@@ -496,39 +494,39 @@ describe API::MergeRequests do
it "returns merge_request" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['state']).to eq('closed')
end
end
it "updates title and returns merge_request" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('New title')
end
it "updates description and returns merge_request" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['description']).to eq('New description')
end
it "updates milestone_id and returns merge_request" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['milestone']['id']).to eq(milestone.id)
end
it "returns merge_request with renamed target_branch" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['target_branch']).to eq('wiki')
end
it "returns merge_request that removes the source branch" do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['force_remove_source_branch']).to be_truthy
end
@@ -549,7 +547,7 @@ describe API::MergeRequests do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil
merge_request.reload
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(merge_request.state).to eq('opened')
end
@@ -557,7 +555,7 @@ describe API::MergeRequests do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil
merge_request.reload
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(merge_request.state).to eq('opened')
end
end
@@ -568,7 +566,7 @@ describe API::MergeRequests do
post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['note']).to eq('My comment')
expect(json_response['author']['name']).to eq(user.name)
expect(json_response['author']['username']).to eq(user.username)
@@ -577,13 +575,13 @@ describe API::MergeRequests do
it "returns 400 if note is missing" do
post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns 404 if note is attached to non existent merge request" do
post v3_api("/projects/#{project.id}/merge_requests/404/comments", user),
note: 'My comment'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -593,7 +591,7 @@ describe API::MergeRequests do
it "returns merge_request comments ordered by created_at" do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['note']).to eq("a comment on a MR")
@@ -603,7 +601,7 @@ describe API::MergeRequests do
it "returns a 404 error if merge_request_id not found" do
get v3_api("/projects/#{project.id}/merge_requests/999/comments", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -615,7 +613,7 @@ describe API::MergeRequests do
end
get v3_api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(issue.id)
@@ -623,7 +621,7 @@ describe API::MergeRequests do
it 'returns an empty array when there are no issues to be closed' do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -636,7 +634,7 @@ describe API::MergeRequests do
get v3_api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['title']).to eq(issue.title)
@@ -651,7 +649,7 @@ describe API::MergeRequests do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -659,20 +657,20 @@ describe API::MergeRequests do
it 'subscribes to a merge request' do
post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
it 'returns 404 if the merge request is not found' do
post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 403 if user has no access to read code' do
@@ -681,7 +679,7 @@ describe API::MergeRequests do
post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -689,20 +687,20 @@ describe API::MergeRequests do
it 'unsubscribes from a merge request' do
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
it 'returns 404 if the merge request is not found' do
post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 403 if user has no access to read code' do
@@ -711,7 +709,7 @@ describe API::MergeRequests do
delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb
index feaa87faec7..e82f35598a6 100644
--- a/spec/requests/api/v3/milestones_spec.rb
+++ b/spec/requests/api/v3/milestones_spec.rb
@@ -12,7 +12,7 @@ describe API::V3::Milestones do
it 'returns project milestones' do
get v3_api("/projects/#{project.id}/milestones", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(milestone.title)
end
@@ -20,13 +20,13 @@ describe API::V3::Milestones do
it 'returns a 401 error if user not authenticated' do
get v3_api("/projects/#{project.id}/milestones")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns an array of active milestones' do
get v3_api("/projects/#{project.id}/milestones?state=active", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(milestone.id)
@@ -35,7 +35,7 @@ describe API::V3::Milestones do
it 'returns an array of closed milestones' do
get v3_api("/projects/#{project.id}/milestones?state=closed", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(closed_milestone.id)
@@ -46,7 +46,7 @@ describe API::V3::Milestones do
it 'returns a project milestone by id' do
get v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(milestone.title)
expect(json_response['iid']).to eq(milestone.iid)
end
@@ -63,7 +63,7 @@ describe API::V3::Milestones do
it 'returns a project milestone by iid array' do
get v3_api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid]
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(2)
expect(json_response.first['title']).to eq milestone.title
expect(json_response.first['id']).to eq milestone.id
@@ -72,13 +72,13 @@ describe API::V3::Milestones do
it 'returns 401 error if user not authenticated' do
get v3_api("/projects/#{project.id}/milestones/#{milestone.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns a 404 error if milestone id not found' do
get v3_api("/projects/#{project.id}/milestones/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -86,7 +86,7 @@ describe API::V3::Milestones do
it 'creates a new project milestone' do
post v3_api("/projects/#{project.id}/milestones", user), title: 'new milestone'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new milestone')
expect(json_response['description']).to be_nil
end
@@ -95,7 +95,7 @@ describe API::V3::Milestones do
post v3_api("/projects/#{project.id}/milestones", user),
title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['description']).to eq('release')
expect(json_response['due_date']).to eq('2013-03-02')
expect(json_response['start_date']).to eq('2013-02-02')
@@ -104,20 +104,20 @@ describe API::V3::Milestones do
it 'returns a 400 error if title is missing' do
post v3_api("/projects/#{project.id}/milestones", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns a 400 error if params are invalid (duplicate title)' do
post v3_api("/projects/#{project.id}/milestones", user),
title: milestone.title, description: 'release', due_date: '2013-03-02'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'creates a new project with reserved html characters' do
post v3_api("/projects/#{project.id}/milestones", user), title: 'foo & bar 1.1 -> 2.2'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2')
expect(json_response['description']).to be_nil
end
@@ -128,7 +128,7 @@ describe API::V3::Milestones do
put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user),
title: 'updated title'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
@@ -137,7 +137,7 @@ describe API::V3::Milestones do
put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['due_date']).to be_nil
end
@@ -145,7 +145,7 @@ describe API::V3::Milestones do
put v3_api("/projects/#{project.id}/milestones/1234", user),
title: 'updated title'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -153,7 +153,7 @@ describe API::V3::Milestones do
it 'updates a project milestone' do
put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user),
state_event: 'close'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['state']).to eq('closed')
end
@@ -175,7 +175,7 @@ describe API::V3::Milestones do
it 'returns project issues for a particular milestone' do
get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['milestone']['title']).to eq(milestone.title)
end
@@ -183,14 +183,14 @@ describe API::V3::Milestones do
it 'matches V3 response schema for a list of issues' do
get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v3/issues')
end
it 'returns a 401 error if user not authenticated' do
get v3_api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
describe 'confidential issues' do
@@ -207,7 +207,7 @@ describe API::V3::Milestones do
it 'returns confidential issues to team members' do
get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id)
@@ -219,7 +219,7 @@ describe API::V3::Milestones do
get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
@@ -228,7 +228,7 @@ describe API::V3::Milestones do
it 'does not return confidential issues to regular users' do
get v3_api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user))
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb
index 56729692eed..d3455a4bba4 100644
--- a/spec/requests/api/v3/notes_spec.rb
+++ b/spec/requests/api/v3/notes_spec.rb
@@ -35,7 +35,7 @@ describe API::V3::Notes do
it "returns an array of issue notes" do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(issue_note.note)
@@ -46,14 +46,14 @@ describe API::V3::Notes do
it "returns a 404 error when issue id not found" do
get v3_api("/projects/#{project.id}/issues/12345/notes", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context "and current user cannot view the notes" do
it "returns an empty array" do
get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to be_empty
@@ -65,7 +65,7 @@ describe API::V3::Notes do
it "returns 404" do
get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -73,7 +73,7 @@ describe API::V3::Notes do
it "returns an empty array" do
get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(cross_reference_note.note)
@@ -86,7 +86,7 @@ describe API::V3::Notes do
it "returns an array of snippet notes" do
get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(snippet_note.note)
@@ -95,13 +95,13 @@ describe API::V3::Notes do
it "returns a 404 error when snippet id not found" do
get v3_api("/projects/#{project.id}/snippets/42/notes", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 when not authorized" do
get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -109,7 +109,7 @@ describe API::V3::Notes do
it "returns an array of merge_requests notes" do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(merge_request_note.note)
@@ -118,13 +118,13 @@ describe API::V3::Notes do
it "returns a 404 error if merge request id not found" do
get v3_api("/projects/#{project.id}/merge_requests/4444/notes", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 when not authorized" do
get v3_api("/projects/#{project.id}/merge_requests/4444/notes", private_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -134,21 +134,21 @@ describe API::V3::Notes do
it "returns an issue note by id" do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['body']).to eq(issue_note.note)
end
it "returns a 404 error if issue note not found" do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context "and current user cannot view the note" do
it "returns a 404 error" do
get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context "when issue is confidential" do
@@ -157,7 +157,7 @@ describe API::V3::Notes do
it "returns 404" do
get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -165,7 +165,7 @@ describe API::V3::Notes do
it "returns an issue note by id" do
get v3_api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['body']).to eq(cross_reference_note.note)
end
end
@@ -176,14 +176,14 @@ describe API::V3::Notes do
it "returns a snippet note by id" do
get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['body']).to eq(snippet_note.note)
end
it "returns a 404 error if snippet note not found" do
get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -193,7 +193,7 @@ describe API::V3::Notes do
it "creates a new issue note" do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
end
@@ -201,13 +201,13 @@ describe API::V3::Notes do
it "returns a 400 bad request error if body not given" do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 401 unauthorized error if user not authenticated" do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
context 'when an admin or owner makes the request' do
@@ -216,7 +216,7 @@ describe API::V3::Notes do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
body: 'hi!', created_at: creation_time
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
@@ -229,7 +229,7 @@ describe API::V3::Notes do
it 'creates a new issue note' do
post v3_api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['body']).to eq(':+1:')
end
end
@@ -238,7 +238,7 @@ describe API::V3::Notes do
it 'creates a new issue note' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['body']).to eq(':+1:')
end
end
@@ -248,7 +248,7 @@ describe API::V3::Notes do
it "creates a new snippet note" do
post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
end
@@ -256,13 +256,13 @@ describe API::V3::Notes do
it "returns a 400 bad request error if body not given" do
post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 401 unauthorized error if user not authenticated" do
post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!'
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -274,7 +274,7 @@ describe API::V3::Notes do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
body: 'Foo'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -314,7 +314,7 @@ describe API::V3::Notes do
put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user), body: 'Hello!'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['body']).to eq('Hello!')
end
@@ -322,14 +322,14 @@ describe API::V3::Notes do
put v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user),
body: 'Hello!'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 400 bad request error if body not given' do
put v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -338,7 +338,7 @@ describe API::V3::Notes do
put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user), body: 'Hello!'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['body']).to eq('Hello!')
end
@@ -346,7 +346,7 @@ describe API::V3::Notes do
put v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/12345", user), body: "Hello!"
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -355,7 +355,7 @@ describe API::V3::Notes do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
"notes/#{merge_request_note.id}", user), body: 'Hello!'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['body']).to eq('Hello!')
end
@@ -363,7 +363,7 @@ describe API::V3::Notes do
put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
"notes/12345", user), body: "Hello!"
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -374,17 +374,17 @@ describe API::V3::Notes do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
# Check if note is really deleted
delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 error when note id not found' do
delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -393,18 +393,18 @@ describe API::V3::Notes do
delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
# Check if note is really deleted
delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 error when note id not found' do
delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -413,18 +413,18 @@ describe API::V3::Notes do
delete v3_api("/projects/#{project.id}/merge_requests/"\
"#{merge_request.id}/notes/#{merge_request_note.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
# Check if note is really deleted
delete v3_api("/projects/#{project.id}/merge_requests/"\
"#{merge_request.id}/notes/#{merge_request_note.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 error when note id not found' do
delete v3_api("/projects/#{project.id}/merge_requests/"\
"#{merge_request.id}/notes/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/v3/pipelines_spec.rb b/spec/requests/api/v3/pipelines_spec.rb
index e1d036ff365..1c7d9fe32bb 100644
--- a/spec/requests/api/v3/pipelines_spec.rb
+++ b/spec/requests/api/v3/pipelines_spec.rb
@@ -32,7 +32,7 @@ describe API::V3::Pipelines do
it 'returns project pipelines' do
get v3_api("/projects/#{project.id}/pipelines", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['sha']).to match(/\A\h{40}\z/)
expect(json_response.first['id']).to eq pipeline.id
@@ -44,7 +44,7 @@ describe API::V3::Pipelines do
it 'does not return project pipelines' do
get v3_api("/projects/#{project.id}/pipelines", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response).not_to be_an Array
end
@@ -61,7 +61,7 @@ describe API::V3::Pipelines do
post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
end.to change { Ci::Pipeline.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to be_a Hash
expect(json_response['sha']).to eq project.commit.id
end
@@ -69,7 +69,7 @@ describe API::V3::Pipelines do
it 'fails when using an invalid ref' do
post v3_api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['base'].first).to eq 'Reference not found'
expect(json_response).not_to be_an Array
end
@@ -79,7 +79,7 @@ describe API::V3::Pipelines do
it 'fails to create pipeline' do
post v3_api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file'
expect(json_response).not_to be_an Array
end
@@ -90,7 +90,7 @@ describe API::V3::Pipelines do
it 'does not create pipeline' do
post v3_api("/projects/#{project.id}/pipeline", non_member), ref: project.default_branch
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response).not_to be_an Array
end
@@ -102,14 +102,14 @@ describe API::V3::Pipelines do
it 'returns project pipelines' do
get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['sha']).to match /\A\h{40}\z/
end
it 'returns 404 when it does not exist' do
get v3_api("/projects/#{project.id}/pipelines/123456", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq '404 Not found'
expect(json_response['id']).to be nil
end
@@ -131,7 +131,7 @@ describe API::V3::Pipelines do
it 'should not return a project pipeline' do
get v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response['id']).to be nil
end
@@ -152,7 +152,7 @@ describe API::V3::Pipelines do
post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
end.to change { pipeline.builds.count }.from(1).to(2)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(build.reload.retried?).to be true
end
end
@@ -161,7 +161,7 @@ describe API::V3::Pipelines do
it 'should not return a project pipeline' do
post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response['id']).to be nil
end
@@ -180,7 +180,7 @@ describe API::V3::Pipelines do
it 'retries failed builds' do
post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to eq('canceled')
end
end
@@ -193,7 +193,7 @@ describe API::V3::Pipelines do
it 'rejects the action' do
post v3_api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(pipeline.reload.status).to eq('pending')
end
end
diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb
index b0eddbb5dd2..00f59744a31 100644
--- a/spec/requests/api/v3/project_hooks_spec.rb
+++ b/spec/requests/api/v3/project_hooks_spec.rb
@@ -22,7 +22,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "returns project hooks" do
get v3_api("/projects/#{project.id}/hooks", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
expect(json_response.first['url']).to eq("http://example.com")
@@ -42,7 +42,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "does not access project hooks" do
get v3_api("/projects/#{project.id}/hooks", user3)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -51,7 +51,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
context "authorized user" do
it "returns a project hook" do
get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['url']).to eq(hook.url)
expect(json_response['issues_events']).to eq(hook.issues_events)
expect(json_response['push_events']).to eq(hook.push_events)
@@ -66,20 +66,20 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "returns a 404 error if hook id is not available" do
get v3_api("/projects/#{project.id}/hooks/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
context "unauthorized user" do
it "does not access an existing hook" do
get v3_api("/projects/#{project.id}/hooks/#{hook.id}", user3)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
it "returns a 404 error if hook id is not available" do
get v3_api("/projects/#{project.id}/hooks/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -90,7 +90,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
url: "http://example.com", issues_events: true, wiki_page_events: true, build_events: true
end.to change {project.hooks.count}.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['url']).to eq('http://example.com')
expect(json_response['issues_events']).to eq(true)
expect(json_response['push_events']).to eq(true)
@@ -111,7 +111,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
post v3_api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token
end.to change {project.hooks.count}.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response["url"]).to eq("http://example.com")
expect(json_response).not_to include("token")
@@ -123,12 +123,12 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "returns a 400 error if url not given" do
post v3_api("/projects/#{project.id}/hooks", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 422 error if url not valid" do
post v3_api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com"
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
end
@@ -136,7 +136,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "updates an existing project hook" do
put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user),
url: 'http://example.org', push_events: false, build_events: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['url']).to eq('http://example.org')
expect(json_response['issues_events']).to eq(hook.issues_events)
expect(json_response['push_events']).to eq(false)
@@ -154,7 +154,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response["url"]).to eq("http://example.org")
expect(json_response).not_to include("token")
@@ -164,17 +164,17 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "returns 404 error if hook id not found" do
put v3_api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 400 error if url is not given" do
put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 422 error if url is not valid" do
put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com'
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
end
@@ -183,23 +183,23 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect do
delete v3_api("/projects/#{project.id}/hooks/#{hook.id}", user)
end.to change {project.hooks.count}.by(-1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "returns success when deleting hook" do
delete v3_api("/projects/#{project.id}/hooks/#{hook.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "returns a 404 error when deleting non existent hook" do
delete v3_api("/projects/#{project.id}/hooks/42", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns a 404 error if hook id not given" do
delete v3_api("/projects/#{project.id}/hooks", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
@@ -208,7 +208,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
other_project.team << [test_user, :master]
delete v3_api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(WebHook.exists?(hook.id)).to be_truthy
end
end
diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb
index 7e88489082a..2ed31b99516 100644
--- a/spec/requests/api/v3/project_snippets_spec.rb
+++ b/spec/requests/api/v3/project_snippets_spec.rb
@@ -28,7 +28,7 @@ describe API::ProjectSnippets do
get v3_api("/projects/#{project.id}/snippets/", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(3)
expect(json_response.map { |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
expect(json_response.last).to have_key('web_url')
@@ -38,7 +38,7 @@ describe API::ProjectSnippets do
create(:project_snippet, :private, project: project)
get v3_api("/projects/#{project.id}/snippets/", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(0)
end
end
@@ -56,7 +56,7 @@ describe API::ProjectSnippets do
it 'creates a new snippet' do
post v3_api("/projects/#{project.id}/snippets/", admin), params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
snippet = ProjectSnippet.find(json_response['id'])
expect(snippet.content).to eq(params[:code])
expect(snippet.title).to eq(params[:title])
@@ -69,7 +69,7 @@ describe API::ProjectSnippets do
post v3_api("/projects/#{project.id}/snippets/", admin), params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context 'when the snippet is spam' do
@@ -95,7 +95,7 @@ describe API::ProjectSnippets do
expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }
.not_to change { Snippet.count }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
end
@@ -116,7 +116,7 @@ describe API::ProjectSnippets do
put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
snippet.reload
expect(snippet.content).to eq(new_content)
end
@@ -124,14 +124,14 @@ describe API::ProjectSnippets do
it 'returns 404 for invalid snippet id' do
put v3_api("/projects/#{snippet.project.id}/snippets/1234", admin), title: 'foo'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
it 'returns 400 for missing parameters' do
put v3_api("/projects/#{project.id}/snippets/1234", admin)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context 'when the snippet is spam' do
@@ -173,7 +173,7 @@ describe API::ProjectSnippets do
expect { update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC) }
.not_to change { snippet.reload.title }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
end
@@ -194,13 +194,13 @@ describe API::ProjectSnippets do
delete v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns 404 for invalid snippet id' do
delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end
@@ -211,7 +211,7 @@ describe API::ProjectSnippets do
it 'returns raw text' do
get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.content_type).to eq 'text/plain'
expect(response.body).to eq(snippet.content)
end
@@ -219,7 +219,7 @@ describe API::ProjectSnippets do
it 'returns 404 for invalid snippet id' do
delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index e5282c3311f..27288b98d1c 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -44,14 +44,14 @@ describe API::V3::Projects do
context 'when unauthenticated' do
it 'returns authentication error' do
get v3_api('/projects')
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
context 'when authenticated as regular user' do
it 'returns an array of projects' do
get v3_api('/projects', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(project.name)
expect(json_response.first['owner']['username']).to eq(user.username)
@@ -94,7 +94,7 @@ describe API::V3::Projects do
get v3_api('/projects?simple=true', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first.keys).to match_array expected_keys
end
@@ -103,7 +103,7 @@ describe API::V3::Projects do
context 'and using search' do
it 'returns searched project' do
get v3_api('/projects', user), { search: project.name }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -112,21 +112,21 @@ describe API::V3::Projects do
context 'and using the visibility filter' do
it 'filters based on private visibility param' do
get v3_api('/projects', user), { visibility: 'private' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count)
end
it 'filters based on internal visibility param' do
get v3_api('/projects', user), { visibility: 'internal' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count)
end
it 'filters based on public visibility param' do
get v3_api('/projects', user), { visibility: 'public' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count)
end
@@ -138,7 +138,7 @@ describe API::V3::Projects do
it 'returns archived project' do
get v3_api('/projects?archived=true', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(archived_project.id)
@@ -147,7 +147,7 @@ describe API::V3::Projects do
it 'returns non-archived project' do
get v3_api('/projects?archived=false', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(project.id)
@@ -156,7 +156,7 @@ describe API::V3::Projects do
it 'returns all project' do
get v3_api('/projects', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
@@ -170,7 +170,7 @@ describe API::V3::Projects do
it 'returns the correct order when sorted by id' do
get v3_api('/projects', user), { order_by: 'id', sort: 'desc' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(project3.id)
end
@@ -184,21 +184,21 @@ describe API::V3::Projects do
context 'when unauthenticated' do
it 'returns authentication error' do
get v3_api('/projects/all')
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
context 'when authenticated as regular user' do
it 'returns authentication error' do
get v3_api('/projects/all', user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
context 'when authenticated as admin' do
it 'returns an array of all projects' do
get v3_api('/projects/all', admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response).to satisfy do |response|
@@ -213,7 +213,7 @@ describe API::V3::Projects do
it "does not include statistics by default" do
get v3_api('/projects/all', admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
@@ -221,7 +221,7 @@ describe API::V3::Projects do
it "includes statistics if requested" do
get v3_api('/projects/all', admin), statistics: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).to include 'statistics'
end
@@ -237,14 +237,14 @@ describe API::V3::Projects do
context 'when unauthenticated' do
it 'returns authentication error' do
get v3_api('/projects/owned')
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
context 'when authenticated as project owner' do
it 'returns an array of projects the user owns' do
get v3_api('/projects/owned', user4)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(project4.name)
expect(json_response.first['owner']['username']).to eq(user4.username)
@@ -253,7 +253,7 @@ describe API::V3::Projects do
it "does not include statistics by default" do
get v3_api('/projects/owned', user4)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
@@ -271,7 +271,7 @@ describe API::V3::Projects do
get v3_api('/projects/owned', user4), statistics: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['statistics']).to eq attributes.stringify_keys
end
@@ -283,7 +283,7 @@ describe API::V3::Projects do
it 'returns the visible projects' do
get v3_api('/projects/visible', current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id))
end
@@ -329,7 +329,7 @@ describe API::V3::Projects do
it 'returns the starred projects viewable by the user' do
get v3_api('/projects/starred', user3)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id)
end
@@ -341,14 +341,14 @@ describe API::V3::Projects do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0)
expect { post v3_api('/projects', user2), name: 'foo' }
.to change {Project.count}.by(0)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
it 'creates new project without path but with name and returns 201' do
expect { post v3_api('/projects', user), name: 'Foo Project' }
.to change { Project.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
project = Project.first
@@ -359,7 +359,7 @@ describe API::V3::Projects do
it 'creates new project without name but with path and returns 201' do
expect { post v3_api('/projects', user), path: 'foo_project' }
.to change { Project.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
project = Project.first
@@ -370,7 +370,7 @@ describe API::V3::Projects do
it 'creates new project name and path and returns 201' do
expect { post v3_api('/projects', user), path: 'foo-Project', name: 'Foo Project' }
.to change { Project.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
project = Project.first
@@ -381,12 +381,12 @@ describe API::V3::Projects do
it 'creates last project before reaching project limit' do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
post v3_api('/projects', user2), name: 'foo'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
it 'does not create new project without name or path and return 400' do
expect { post v3_api('/projects', user) }.not_to change { Project.count }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "assigns attributes to project" do
@@ -404,6 +404,7 @@ describe API::V3::Projects do
project.each_pair do |k, v|
next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
+
expect(json_response[k.to_s]).to eq(v)
end
@@ -501,7 +502,7 @@ describe API::V3::Projects do
it 'does not allow a non-admin to use a restricted visibility level' do
post v3_api('/projects', user), @project
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['visibility_level'].first).to(
match('restricted by your GitLab administrator')
)
@@ -523,14 +524,14 @@ describe API::V3::Projects do
it 'should create new project without path and return 201' do
expect { post v3_api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
it 'responds with 400 on failure and not project' do
expect { post v3_api("/projects/user/#{user.id}", admin) }
.not_to change { Project.count }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('name is missing')
end
@@ -544,9 +545,10 @@ describe API::V3::Projects do
post v3_api("/projects/user/#{user.id}", admin), project
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
project.each_pair do |k, v|
next if %i[has_external_issue_tracker path].include?(k)
+
expect(json_response[k.to_s]).to eq(v)
end
end
@@ -555,7 +557,7 @@ describe API::V3::Projects do
project = attributes_for(:project, :public)
post v3_api("/projects/user/#{user.id}", admin), project
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
@@ -564,7 +566,7 @@ describe API::V3::Projects do
project = attributes_for(:project, { public: true })
post v3_api("/projects/user/#{user.id}", admin), project
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
@@ -573,7 +575,7 @@ describe API::V3::Projects do
project = attributes_for(:project, :internal)
post v3_api("/projects/user/#{user.id}", admin), project
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
@@ -581,7 +583,7 @@ describe API::V3::Projects do
it 'sets a project as internal overriding :public' do
project = attributes_for(:project, :internal, { public: true })
post v3_api("/projects/user/#{user.id}", admin), project
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
@@ -635,7 +637,7 @@ describe API::V3::Projects do
it "uploads the file and returns its info" do
post v3_api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['alt']).to eq("dk")
expect(json_response['url']).to start_with("/uploads/")
expect(json_response['url']).to end_with("/dk.png")
@@ -649,7 +651,7 @@ describe API::V3::Projects do
get v3_api("/projects/#{public_project.id}")
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(public_project.id)
expect(json_response['description']).to eq(public_project.description)
expect(json_response['default_branch']).to eq(public_project.default_branch)
@@ -668,7 +670,7 @@ describe API::V3::Projects do
get v3_api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(project.id)
expect(json_response['description']).to eq(project.description)
expect(json_response['default_branch']).to eq(project.default_branch)
@@ -710,20 +712,20 @@ describe API::V3::Projects do
it 'returns a project by path name' do
get v3_api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(project.name)
end
it 'returns a 404 error if not found' do
get v3_api('/projects/42', user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'returns a 404 error if user is not a member' do
other_user = create(:user)
get v3_api("/projects/#{project.id}", other_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'handles users with dots' do
@@ -731,14 +733,14 @@ describe API::V3::Projects do
project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
get v3_api("/projects/#{CGI.escape(project.full_path)}", dot_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(project.name)
end
it 'exposes namespace fields' do
get v3_api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['namespace']).to eq({
'id' => user.namespace.id,
'name' => user.namespace.name,
@@ -756,7 +758,7 @@ describe API::V3::Projects do
it 'contains permission information' do
get v3_api("/projects", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.first['permissions']['project_access']['access_level'])
.to eq(Gitlab::Access::MASTER)
expect(json_response.first['permissions']['group_access']).to be_nil
@@ -768,7 +770,7 @@ describe API::V3::Projects do
project.team << [user, :master]
get v3_api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['permissions']['project_access']['access_level'])
.to eq(Gitlab::Access::MASTER)
expect(json_response['permissions']['group_access']).to be_nil
@@ -783,7 +785,7 @@ describe API::V3::Projects do
it 'sets the owner and return 200' do
get v3_api("/projects/#{project2.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['permissions']['project_access']).to be_nil
expect(json_response['permissions']['group_access']['access_level'])
.to eq(Gitlab::Access::OWNER)
@@ -803,7 +805,7 @@ describe API::V3::Projects do
get v3_api("/projects/#{project.id}/events", current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
first_event = json_response.first
@@ -836,7 +838,7 @@ describe API::V3::Projects do
it 'returns a 404 error if not found' do
get v3_api('/projects/42/events', user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
@@ -845,7 +847,7 @@ describe API::V3::Projects do
get v3_api("/projects/#{project.id}/events", other_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -857,7 +859,7 @@ describe API::V3::Projects do
get v3_api("/projects/#{project.id}/users", current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
@@ -886,7 +888,7 @@ describe API::V3::Projects do
it 'returns a 404 error if not found' do
get v3_api('/projects/42/users', user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
@@ -895,7 +897,7 @@ describe API::V3::Projects do
get v3_api("/projects/#{project.id}/users", other_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -905,7 +907,7 @@ describe API::V3::Projects do
it 'returns an array of project snippets' do
get v3_api("/projects/#{project.id}/snippets", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(snippet.title)
end
@@ -914,13 +916,13 @@ describe API::V3::Projects do
describe 'GET /projects/:id/snippets/:snippet_id' do
it 'returns a project snippet' do
get v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(snippet.title)
end
it 'returns a 404 error if snippet id not found' do
get v3_api("/projects/#{project.id}/snippets/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -929,7 +931,7 @@ describe API::V3::Projects do
post v3_api("/projects/#{project.id}/snippets", user),
title: 'v3_api test', file_name: 'sample.rb', code: 'test',
visibility_level: '0'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('v3_api test')
end
@@ -943,7 +945,7 @@ describe API::V3::Projects do
it 'updates an existing project snippet' do
put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user),
code: 'updated code'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('example')
expect(snippet.reload.content).to eq('updated code')
end
@@ -951,7 +953,7 @@ describe API::V3::Projects do
it 'updates an existing project snippet with new title' do
put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user),
title: 'other v3_api test'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('other v3_api test')
end
end
@@ -963,24 +965,24 @@ describe API::V3::Projects do
expect do
delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user)
end.to change { Snippet.count }.by(-1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns 404 when deleting unknown snippet id' do
delete v3_api("/projects/#{project.id}/snippets/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
describe 'GET /projects/:id/snippets/:snippet_id/raw' do
it 'gets a raw project snippet' do
get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns a 404 error if raw project snippet not found' do
get v3_api("/projects/#{project.id}/snippets/5555/raw", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -993,13 +995,13 @@ describe API::V3::Projects do
it "is not available for non admin users" do
post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'allows project to be forked from an existing project' do
expect(project_fork_target.forked?).not_to be_truthy
post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
expect(project_fork_target.forked_project_link).not_to be_nil
@@ -1016,7 +1018,7 @@ describe API::V3::Projects do
it 'fails if forked_from project which does not exist' do
post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'fails with 409 if already forked' do
@@ -1024,7 +1026,7 @@ describe API::V3::Projects do
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
post v3_api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin)
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
expect(project_fork_target.forked?).to be_truthy
@@ -1034,7 +1036,7 @@ describe API::V3::Projects do
describe 'DELETE /projects/:id/fork' do
it "is not visible to users outside group" do
delete v3_api("/projects/#{project_fork_target.id}/fork", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context 'when users belong to project group' do
@@ -1047,7 +1049,7 @@ describe API::V3::Projects do
it 'is forbidden to non-owner users' do
delete v3_api("/projects/#{project_fork_target.id}/fork", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'makes forked project unforked' do
@@ -1056,7 +1058,7 @@ describe API::V3::Projects do
expect(project_fork_target.forked_from_project).not_to be_nil
expect(project_fork_target.forked?).to be_truthy
delete v3_api("/projects/#{project_fork_target.id}/fork", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_fork_target.reload
expect(project_fork_target.forked_from_project).to be_nil
expect(project_fork_target.forked?).not_to be_truthy
@@ -1065,7 +1067,7 @@ describe API::V3::Projects do
it 'is idempotent if not forked' do
expect(project_fork_target.forked_from_project).to be_nil
delete v3_api("/projects/#{project_fork_target.id}/fork", admin)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
expect(project_fork_target.reload.forked_from_project).to be_nil
end
end
@@ -1082,7 +1084,7 @@ describe API::V3::Projects do
post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at
end.to change { ProjectGroupLink.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['group_id']).to eq(group.id)
expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER)
expect(json_response['expires_at']).to eq(expires_at.to_s)
@@ -1090,18 +1092,18 @@ describe API::V3::Projects do
it "returns a 400 error when group id is not given" do
post v3_api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 400 error when access level is not given" do
post v3_api("/projects/#{project.id}/share", user), group_id: group.id
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns a 400 error when sharing is disabled" do
project.namespace.update(share_with_group_lock: true)
post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns a 404 error when user cannot read group' do
@@ -1109,19 +1111,19 @@ describe API::V3::Projects do
post v3_api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 error when group does not exist' do
post v3_api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns a 400 error when wrong params passed" do
post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq 'group_access does not have a valid value'
end
end
@@ -1133,26 +1135,26 @@ describe API::V3::Projects do
delete v3_api("/projects/#{project.id}/share/#{group.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(project.project_group_links).to be_empty
end
it 'returns a 400 when group id is not an integer' do
delete v3_api("/projects/#{project.id}/share/foo", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns a 404 error when group link does not exist' do
delete v3_api("/projects/#{project.id}/share/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 error when project does not exist' do
delete v3_api("/projects/123/share/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -1173,7 +1175,7 @@ describe API::V3::Projects do
it 'returns project search responses' do
get v3_api("/projects/search/#{args[:query]}", current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(args[:results])
json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) }
@@ -1216,7 +1218,7 @@ describe API::V3::Projects do
it 'returns authentication error' do
project_param = { name: 'bar' }
put v3_api("/projects/#{project.id}"), project_param
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -1224,7 +1226,7 @@ describe API::V3::Projects do
it 'updates name' do
project_param = { name: 'bar' }
put v3_api("/projects/#{project.id}", user), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -1233,7 +1235,7 @@ describe API::V3::Projects do
it 'updates visibility_level' do
project_param = { visibility_level: 20 }
put v3_api("/projects/#{project3.id}", user), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -1243,7 +1245,7 @@ describe API::V3::Projects do
project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
project_param = { public: false }
put v3_api("/projects/#{project3.id}", user), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -1253,7 +1255,7 @@ describe API::V3::Projects do
it 'does not update name to existing name' do
project_param = { name: project3.name }
put v3_api("/projects/#{project.id}", user), project_param
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['name']).to eq(['has already been taken'])
end
@@ -1262,14 +1264,14 @@ describe API::V3::Projects do
put v3_api("/projects/#{project.id}", user), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['request_access_enabled']).to eq(false)
end
it 'updates path & name to existing path & name in different namespace' do
project_param = { path: project4.path, name: project4.name }
put v3_api("/projects/#{project3.id}", user), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -1280,7 +1282,7 @@ describe API::V3::Projects do
it 'updates path' do
project_param = { path: 'bar' }
put v3_api("/projects/#{project3.id}", user4), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -1294,7 +1296,7 @@ describe API::V3::Projects do
description: 'new description' }
put v3_api("/projects/#{project3.id}", user4), project_param
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project_param.each_pair do |k, v|
expect(json_response[k.to_s]).to eq(v)
end
@@ -1303,20 +1305,20 @@ describe API::V3::Projects do
it 'does not update path to existing path' do
project_param = { path: project.path }
put v3_api("/projects/#{project3.id}", user4), project_param
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['path']).to eq(['has already been taken'])
end
it 'does not update name' do
project_param = { name: 'bar' }
put v3_api("/projects/#{project3.id}", user4), project_param
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'does not update visibility_level' do
project_param = { visibility_level: 20 }
put v3_api("/projects/#{project3.id}", user4), project_param
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -1330,7 +1332,7 @@ describe API::V3::Projects do
description: 'new description',
request_access_enabled: true }
put v3_api("/projects/#{project.id}", user3), project_param
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -1340,7 +1342,7 @@ describe API::V3::Projects do
it 'archives the project' do
post v3_api("/projects/#{project.id}/archive", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['archived']).to be_truthy
end
end
@@ -1353,7 +1355,7 @@ describe API::V3::Projects do
it 'remains archived' do
post v3_api("/projects/#{project.id}/archive", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['archived']).to be_truthy
end
end
@@ -1366,7 +1368,7 @@ describe API::V3::Projects do
it 'rejects the action' do
post v3_api("/projects/#{project.id}/archive", user3)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -1376,7 +1378,7 @@ describe API::V3::Projects do
it 'remains unarchived' do
post v3_api("/projects/#{project.id}/unarchive", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['archived']).to be_falsey
end
end
@@ -1389,7 +1391,7 @@ describe API::V3::Projects do
it 'unarchives the project' do
post v3_api("/projects/#{project.id}/unarchive", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['archived']).to be_falsey
end
end
@@ -1402,7 +1404,7 @@ describe API::V3::Projects do
it 'rejects the action' do
post v3_api("/projects/#{project.id}/unarchive", user3)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -1412,7 +1414,7 @@ describe API::V3::Projects do
it 'stars the project' do
expect { post v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['star_count']).to eq(1)
end
end
@@ -1426,7 +1428,7 @@ describe API::V3::Projects do
it 'does not modify the star count' do
expect { post v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
end
end
@@ -1441,7 +1443,7 @@ describe API::V3::Projects do
it 'unstars the project' do
expect { delete v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['star_count']).to eq(0)
end
end
@@ -1450,7 +1452,7 @@ describe API::V3::Projects do
it 'does not modify the star count' do
expect { delete v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
end
end
@@ -1459,36 +1461,36 @@ describe API::V3::Projects do
context 'when authenticated as user' do
it 'removes project' do
delete v3_api("/projects/#{project.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'does not remove a project if not an owner' do
user3 = create(:user)
project.team << [user3, :developer]
delete v3_api("/projects/#{project.id}", user3)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'does not remove a non existing project' do
delete v3_api('/projects/1328', user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'does not remove a project not attached to user' do
delete v3_api("/projects/#{project.id}", user2)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
context 'when authenticated as admin' do
it 'removes any existing project' do
delete v3_api("/projects/#{project.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'does not remove a non existing project' do
delete v3_api('/projects/1328', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb
index 1a55e2a71cd..0167eb2c4f6 100644
--- a/spec/requests/api/v3/repositories_spec.rb
+++ b/spec/requests/api/v3/repositories_spec.rb
@@ -17,7 +17,7 @@ describe API::V3::Repositories do
it 'returns the repository tree' do
get v3_api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
first_commit = json_response.first
@@ -97,20 +97,21 @@ describe API::V3::Repositories do
end
end
- {
- 'blobs/:sha' => 'blobs/master',
- 'commits/:sha/blob' => 'commits/master/blob'
- }.each do |desc_path, example_path|
+ [
+ ['blobs/:sha', 'blobs/master'],
+ ['blobs/:sha', 'blobs/v1.1.0'],
+ ['commits/:sha/blob', 'commits/master/blob']
+ ].each do |desc_path, example_path|
describe "GET /projects/:id/repository/#{desc_path}" do
let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" }
shared_examples_for 'repository blob' do
it 'returns the repository blob' do
get v3_api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
context 'when sha does not exist' do
it_behaves_like '404 response' do
- let(:request) { get v3_api(route.sub('master', 'invalid_branch_name'), current_user) }
+ let(:request) { get v3_api("/projects/#{project.id}/repository/#{desc_path.sub(':sha', 'invalid_branch_name')}?filepath=README.md", current_user) }
let(:message) { '404 Commit Not Found' }
end
end
@@ -161,7 +162,7 @@ describe API::V3::Repositories do
shared_examples_for 'repository raw blob' do
it 'returns the repository raw blob' do
get v3_api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
context 'when sha does not exist' do
it_behaves_like '404 response' do
@@ -204,7 +205,7 @@ describe API::V3::Repositories do
shared_examples_for 'repository archive' do
it 'returns the repository archive' do
get v3_api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
repo_name = project.repository.name.gsub("\.git", "")
type, params = workhorse_send_data
expect(type).to eq('git-archive')
@@ -212,7 +213,7 @@ describe API::V3::Repositories do
end
it 'returns the repository archive archive.zip' do
get v3_api("/projects/#{project.id}/repository/archive.zip", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
repo_name = project.repository.name.gsub("\.git", "")
type, params = workhorse_send_data
expect(type).to eq('git-archive')
@@ -220,7 +221,7 @@ describe API::V3::Repositories do
end
it 'returns the repository archive archive.tar.bz2' do
get v3_api("/projects/#{project.id}/repository/archive.tar.bz2", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
repo_name = project.repository.name.gsub("\.git", "")
type, params = workhorse_send_data
expect(type).to eq('git-archive')
@@ -262,32 +263,32 @@ describe API::V3::Repositories do
shared_examples_for 'repository compare' do
it "compares branches" do
get v3_api(route, current_user), from: 'master', to: 'feature'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
it "compares tags" do
get v3_api(route, current_user), from: 'v1.0.0', to: 'v1.1.0'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
it "compares commits" do
get v3_api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['commits']).to be_empty
expect(json_response['diffs']).to be_empty
expect(json_response['compare_same_ref']).to be_falsey
end
it "compares commits in reverse order" do
get v3_api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
it "compares same refs" do
get v3_api(route, current_user), from: 'master', to: 'master'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['commits']).to be_empty
expect(json_response['diffs']).to be_empty
expect(json_response['compare_same_ref']).to be_truthy
@@ -324,7 +325,7 @@ describe API::V3::Repositories do
it 'returns valid data' do
get v3_api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
first_contributor = json_response.first
diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb
index a31eb3f1d43..c91b097a3c7 100644
--- a/spec/requests/api/v3/runners_spec.rb
+++ b/spec/requests/api/v3/runners_spec.rb
@@ -37,7 +37,7 @@ describe API::V3::Runners do
expect do
delete v3_api("/runners/#{shared_runner.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change { Ci::Runner.shared.count }.by(-1)
end
end
@@ -47,7 +47,7 @@ describe API::V3::Runners do
expect do
delete v3_api("/runners/#{unused_specific_runner.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change { Ci::Runner.specific.count }.by(-1)
end
@@ -55,7 +55,7 @@ describe API::V3::Runners do
expect do
delete v3_api("/runners/#{specific_runner.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change { Ci::Runner.specific.count }.by(-1)
end
end
@@ -63,7 +63,7 @@ describe API::V3::Runners do
it 'returns 404 if runner does not exists' do
delete v3_api('/runners/9999', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -71,26 +71,26 @@ describe API::V3::Runners do
context 'when runner is shared' do
it 'does not delete runner' do
delete v3_api("/runners/#{shared_runner.id}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
context 'when runner is not shared' do
it 'does not delete runner without access to it' do
delete v3_api("/runners/#{specific_runner.id}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'does not delete runner with more than one associated project' do
delete v3_api("/runners/#{two_projects_runner.id}", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'deletes runner for one owned project' do
expect do
delete v3_api("/runners/#{specific_runner.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change { Ci::Runner.specific.count }.by(-1)
end
end
@@ -100,7 +100,7 @@ describe API::V3::Runners do
it 'does not delete runner' do
delete v3_api("/runners/#{specific_runner.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -112,7 +112,7 @@ describe API::V3::Runners do
expect do
delete v3_api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change { project.runners.count }.by(-1)
end
end
@@ -122,14 +122,14 @@ describe API::V3::Runners do
expect do
delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
end.to change { project.runners.count }.by(0)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
it 'returns 404 is runner is not found' do
delete v3_api("/projects/#{project.id}/runners/9999", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -137,7 +137,7 @@ describe API::V3::Runners do
it "does not disable project's runner" do
delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -145,7 +145,7 @@ describe API::V3::Runners do
it "does not disable project's runner" do
delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb
index f0fa48e22df..8f212ab6be6 100644
--- a/spec/requests/api/v3/services_spec.rb
+++ b/spec/requests/api/v3/services_spec.rb
@@ -13,7 +13,7 @@ describe API::V3::Services do
it "deletes #{service}" do
delete v3_api("/projects/#{project.id}/services/#{dashed_service}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
project.send(service_method).reload
expect(project.send(service_method).activated?).to be_falsey
end
diff --git a/spec/requests/api/v3/settings_spec.rb b/spec/requests/api/v3/settings_spec.rb
index 291f6dcc2aa..985bfbfa09c 100644
--- a/spec/requests/api/v3/settings_spec.rb
+++ b/spec/requests/api/v3/settings_spec.rb
@@ -7,7 +7,7 @@ describe API::V3::Settings, 'Settings' do
describe "GET /application/settings" do
it "returns application settings" do
get v3_api("/application/settings", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Hash
expect(json_response['default_projects_limit']).to eq(42)
expect(json_response['password_authentication_enabled']).to be_truthy
@@ -28,11 +28,11 @@ describe API::V3::Settings, 'Settings' do
it "updates application settings" do
put v3_api("/application/settings", admin),
- default_projects_limit: 3, password_authentication_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com',
+ default_projects_limit: 3, password_authentication_enabled_for_web: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com',
plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
- expect(json_response['password_authentication_enabled']).to be_falsey
+ expect(json_response['password_authentication_enabled_for_web']).to be_falsey
expect(json_response['repository_storage']).to eq('custom')
expect(json_response['repository_storages']).to eq(['custom'])
expect(json_response['koding_enabled']).to be_truthy
@@ -46,7 +46,7 @@ describe API::V3::Settings, 'Settings' do
it "returns a blank parameter error message" do
put v3_api("/application/settings", admin), koding_enabled: true
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('koding_url is missing')
end
end
@@ -55,7 +55,7 @@ describe API::V3::Settings, 'Settings' do
it "returns a blank parameter error message" do
put v3_api("/application/settings", admin), plantuml_enabled: true
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('plantuml_url is missing')
end
end
diff --git a/spec/requests/api/v3/snippets_spec.rb b/spec/requests/api/v3/snippets_spec.rb
index 79860725634..e8913039194 100644
--- a/spec/requests/api/v3/snippets_spec.rb
+++ b/spec/requests/api/v3/snippets_spec.rb
@@ -11,7 +11,7 @@ describe API::V3::Snippets do
get v3_api("/snippets/", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
public_snippet.id,
internal_snippet.id,
@@ -24,7 +24,7 @@ describe API::V3::Snippets do
create(:personal_snippet, :private)
get v3_api("/snippets/", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(0)
end
end
@@ -41,7 +41,7 @@ describe API::V3::Snippets do
it 'returns all snippets with public visibility from all users' do
get v3_api("/snippets/public", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
public_snippet.id,
public_snippet_other.id)
@@ -60,7 +60,7 @@ describe API::V3::Snippets do
it 'returns raw text' do
get v3_api("/snippets/#{snippet.id}/raw", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.content_type).to eq 'text/plain'
expect(response.body).to eq(snippet.content)
end
@@ -68,7 +68,7 @@ describe API::V3::Snippets do
it 'returns 404 for invalid snippet id' do
delete v3_api("/snippets/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end
@@ -88,7 +88,7 @@ describe API::V3::Snippets do
post v3_api("/snippets/", user), params
end.to change { PersonalSnippet.count }.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq(params[:title])
expect(json_response['file_name']).to eq(params[:file_name])
end
@@ -98,7 +98,7 @@ describe API::V3::Snippets do
post v3_api("/snippets/", user), params
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context 'when the snippet is spam' do
@@ -121,7 +121,7 @@ describe API::V3::Snippets do
it 'rejects the shippet' do
expect { create_snippet(visibility_level: Snippet::PUBLIC) }
.not_to change { Snippet.count }
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'creates a spam log' do
@@ -140,7 +140,7 @@ describe API::V3::Snippets do
put v3_api("/snippets/#{public_snippet.id}", user), content: new_content
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
public_snippet.reload
expect(public_snippet.content).to eq(new_content)
end
@@ -148,21 +148,21 @@ describe API::V3::Snippets do
it 'returns 404 for invalid snippet id' do
put v3_api("/snippets/1234", user), title: 'foo'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
it "returns 404 for another user's snippet" do
put v3_api("/snippets/#{public_snippet.id}", other_user), title: 'fubar'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
it 'returns 400 for missing parameters' do
put v3_api("/snippets/1234", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -172,14 +172,14 @@ describe API::V3::Snippets do
expect do
delete v3_api("/snippets/#{public_snippet.id}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change { PersonalSnippet.count }.by(-1)
end
it 'returns 404 for invalid snippet id' do
delete v3_api("/snippets/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end
diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb
index ae427541abb..30711c60faa 100644
--- a/spec/requests/api/v3/system_hooks_spec.rb
+++ b/spec/requests/api/v3/system_hooks_spec.rb
@@ -12,7 +12,7 @@ describe API::V3::SystemHooks do
it "returns authentication error" do
get v3_api("/hooks")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -20,7 +20,7 @@ describe API::V3::SystemHooks do
it "returns forbidden error" do
get v3_api("/hooks", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -28,7 +28,7 @@ describe API::V3::SystemHooks do
it "returns an array of hooks" do
get v3_api("/hooks", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['url']).to eq(hook.url)
expect(json_response.first['push_events']).to be false
@@ -43,14 +43,14 @@ describe API::V3::SystemHooks do
expect do
delete v3_api("/hooks/#{hook.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change { SystemHook.count }.by(-1)
end
it 'returns 404 if the system hook does not exist' do
delete v3_api('/hooks/12345', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb
index 1c4b25c47c3..e6ad005fa87 100644
--- a/spec/requests/api/v3/tags_spec.rb
+++ b/spec/requests/api/v3/tags_spec.rb
@@ -17,7 +17,7 @@ describe API::V3::Tags do
it 'returns the repository tags' do
get v3_api("/projects/#{project.id}/repository/tags", current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(tag_name)
end
@@ -40,7 +40,7 @@ describe API::V3::Tags do
it "returns an array of project tags" do
get v3_api("/projects/#{project.id}/repository/tags", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(tag_name)
end
@@ -55,7 +55,7 @@ describe API::V3::Tags do
it "returns an array of project tags with release info" do
get v3_api("/projects/#{project.id}/repository/tags", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(tag_name)
expect(json_response.first['message']).to eq('Version 1.1.0')
@@ -75,13 +75,13 @@ describe API::V3::Tags do
it 'deletes an existing tag' do
delete v3_api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['tag_name']).to eq(tag_name)
end
it 'raises 404 if the tag does not exist' do
delete v3_api("/projects/#{project.id}/repository/tags/foobar", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb
index 00446c7f29c..38a8994eb79 100644
--- a/spec/requests/api/v3/templates_spec.rb
+++ b/spec/requests/api/v3/templates_spec.rb
@@ -19,7 +19,7 @@ describe API::V3::Templates do
it 'returns a list of available gitignore templates' do
get v3_api(path)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to be > 15
end
@@ -29,7 +29,7 @@ describe API::V3::Templates do
it 'returns a list of available gitlab_ci_ymls' do
get v3_api(path)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).not_to be_nil
end
@@ -39,7 +39,7 @@ describe API::V3::Templates do
it 'adds a disclaimer on the top' do
get v3_api(path)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['content']).to start_with("# This file is a template,")
end
end
@@ -66,7 +66,7 @@ describe API::V3::Templates do
it 'returns a list of available license templates' do
get v3_api(path)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(12)
expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
@@ -77,7 +77,7 @@ describe API::V3::Templates do
it 'returns a list of available popular license templates' do
get v3_api("#{path}?popular=1")
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
expect(json_response.map { |l| l['key'] }).to include('apache-2.0')
@@ -159,7 +159,7 @@ describe API::V3::Templates do
let(:license_type) { 'muth-over9000' }
it 'returns a 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
index 7ccf387f2dc..e8e2f49d7a0 100644
--- a/spec/requests/api/v3/triggers_spec.rb
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -27,17 +27,17 @@ describe API::V3::Triggers do
context 'Handles errors' do
it 'returns bad request if token is missing' do
post v3_api("/projects/#{project.id}/trigger/builds"), ref: 'master'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns not found if project is not found' do
post v3_api('/projects/0/trigger/builds'), options.merge(ref: 'master')
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
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(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -46,7 +46,7 @@ describe API::V3::Triggers do
it 'creates builds' do
post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
pipeline.builds.reload
expect(pipeline.builds.pending.size).to eq(2)
expect(pipeline.builds.size).to eq(5)
@@ -54,7 +54,7 @@ describe API::V3::Triggers do
it 'returns bad request with no builds created if there\'s no commit for that ref' do
post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['base'])
.to contain_exactly('Reference not found')
end
@@ -66,19 +66,19 @@ describe API::V3::Triggers do
it 'validates variables to be a hash' do
post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq('variables is invalid')
end
it 'validates variables needs to be a map of key-valued strings' do
post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
end
it 'creates trigger request with variables' do
post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
pipeline.builds.reload
expect(pipeline.variables.map { |v| { v.key => v.value } }.first).to eq(variables)
expect(json_response['variables']).to eq(variables)
@@ -91,7 +91,7 @@ describe API::V3::Triggers do
expect do
post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
end.to change(project.builds, :count).by(5)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
context 'when ref contains a dot' do
@@ -102,7 +102,7 @@ describe API::V3::Triggers do
post v3_api("/projects/#{project.id}/ref/v.1-branch/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
end.to change(project.builds, :count).by(4)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
end
end
@@ -113,7 +113,7 @@ describe API::V3::Triggers do
it 'returns list of triggers' do
get v3_api("/projects/#{project.id}/triggers", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_a(Array)
expect(json_response[0]).to have_key('token')
@@ -124,7 +124,7 @@ describe API::V3::Triggers do
it 'does not return triggers list' do
get v3_api("/projects/#{project.id}/triggers", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -132,7 +132,7 @@ describe API::V3::Triggers do
it 'does not return triggers list' do
get v3_api("/projects/#{project.id}/triggers")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -142,14 +142,14 @@ describe API::V3::Triggers do
it 'returns trigger details' do
get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_a(Hash)
end
it 'responds with 404 Not Found if requesting non-existing trigger' do
get v3_api("/projects/#{project.id}/triggers/abcdef012345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -157,7 +157,7 @@ describe API::V3::Triggers do
it 'does not return triggers list' do
get v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -165,7 +165,7 @@ describe API::V3::Triggers do
it 'does not return triggers list' do
get v3_api("/projects/#{project.id}/triggers/#{trigger.token}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -177,7 +177,7 @@ describe API::V3::Triggers do
post v3_api("/projects/#{project.id}/triggers", user)
end.to change {project.triggers.count}.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response).to be_a(Hash)
end
end
@@ -186,7 +186,7 @@ describe API::V3::Triggers do
it 'does not create trigger' do
post v3_api("/projects/#{project.id}/triggers", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -194,7 +194,7 @@ describe API::V3::Triggers do
it 'does not create trigger' do
post v3_api("/projects/#{project.id}/triggers")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -205,14 +205,14 @@ describe API::V3::Triggers do
expect do
delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end.to change {project.triggers.count}.by(-1)
end
it 'responds with 404 Not Found if requesting non-existing trigger' do
delete v3_api("/projects/#{project.id}/triggers/abcdef012345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -220,7 +220,7 @@ describe API::V3::Triggers do
it 'does not delete trigger' do
delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -228,7 +228,7 @@ describe API::V3::Triggers do
it 'does not delete trigger' do
delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb
index 227b8d1b0c1..bbd05f240d2 100644
--- a/spec/requests/api/v3/users_spec.rb
+++ b/spec/requests/api/v3/users_spec.rb
@@ -12,7 +12,7 @@ describe API::V3::Users do
it 'returns an array of users' do
get v3_api('/users', user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
username = user.username
@@ -45,14 +45,14 @@ describe API::V3::Users do
context 'when unauthenticated' do
it 'returns authentication error' do
get v3_api("/users/#{user.id}/keys")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
context 'when authenticated' do
it 'returns 404 for non-existing user' do
get v3_api('/users/999999/keys', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -62,7 +62,7 @@ describe API::V3::Users do
get v3_api("/users/#{user.id}/keys", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(key.title)
end
@@ -88,14 +88,14 @@ describe API::V3::Users do
context 'when unauthenticated' do
it 'returns authentication error' do
get v3_api("/users/#{user.id}/emails")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
context 'when authenticated' do
it 'returns 404 for non-existing user' do
get v3_api('/users/999999/emails', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -105,7 +105,7 @@ describe API::V3::Users do
get v3_api("/users/#{user.id}/emails", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['email']).to eq(email.email)
end
@@ -113,7 +113,7 @@ describe API::V3::Users do
it "returns a 404 for invalid ID" do
put v3_api("/users/ASDF/emails", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -122,7 +122,7 @@ describe API::V3::Users do
context "when unauthenticated" do
it "returns authentication error" do
get v3_api("/user/keys")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -133,7 +133,7 @@ describe API::V3::Users do
get v3_api("/user/keys", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first["title"]).to eq(key.title)
end
@@ -144,7 +144,7 @@ describe API::V3::Users do
context "when unauthenticated" do
it "returns authentication error" do
get v3_api("/user/emails")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -155,7 +155,7 @@ describe API::V3::Users do
get v3_api("/user/emails", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first["email"]).to eq(email.email)
end
@@ -166,25 +166,25 @@ describe API::V3::Users do
before { admin }
it 'blocks existing user' do
put v3_api("/users/#{user.id}/block", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(user.reload.state).to eq('blocked')
end
it 'does not re-block ldap blocked users' do
put v3_api("/users/#{ldap_blocked_user.id}/block", admin)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
it 'does not be available for non admin users' do
put v3_api("/users/#{user.id}/block", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(user.reload.state).to eq('active')
end
it 'returns a 404 error if user id not found' do
put v3_api('/users/9999/block', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
end
@@ -195,38 +195,38 @@ describe API::V3::Users do
it 'unblocks existing user' do
put v3_api("/users/#{user.id}/unblock", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(user.reload.state).to eq('active')
end
it 'unblocks a blocked user' do
put v3_api("/users/#{blocked_user.id}/unblock", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(blocked_user.reload.state).to eq('active')
end
it 'does not unblock ldap blocked users' do
put v3_api("/users/#{ldap_blocked_user.id}/unblock", admin)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
it 'does not be available for non admin users' do
put v3_api("/users/#{user.id}/unblock", user)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(user.reload.state).to eq('active')
end
it 'returns a 404 error if user id not found' do
put v3_api('/users/9999/block', admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 for invalid ID" do
put v3_api("/users/ASDF/block", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -246,7 +246,7 @@ describe API::V3::Users do
get api("/users/#{user.id}/events", other_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_empty
end
end
@@ -262,7 +262,7 @@ describe API::V3::Users do
end
it 'responds with HTTP 200 OK' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'includes the push payload as a Hash' do
@@ -281,7 +281,7 @@ describe API::V3::Users do
it 'returns the "joined" event' do
get v3_api("/users/#{user.id}/events", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
@@ -327,7 +327,7 @@ describe API::V3::Users do
it 'returns a 404 error if not found' do
get v3_api('/users/420/events', user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 48592e12822..79ee6c126f6 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -13,7 +13,7 @@ describe API::Variables do
it 'returns project variables' do
get api("/projects/#{project.id}/variables", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_a(Array)
end
end
@@ -22,7 +22,7 @@ describe API::Variables do
it 'does not return project variables' do
get api("/projects/#{project.id}/variables", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -30,7 +30,7 @@ describe API::Variables do
it 'does not return project variables' do
get api("/projects/#{project.id}/variables")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -40,7 +40,7 @@ describe API::Variables do
it 'returns project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['value']).to eq(variable.value)
expect(json_response['protected']).to eq(variable.protected?)
end
@@ -48,7 +48,7 @@ describe API::Variables do
it 'responds with 404 Not Found if requesting non-existing variable' do
get api("/projects/#{project.id}/variables/non_existing_variable", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -56,7 +56,7 @@ describe API::Variables do
it 'does not return project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -64,7 +64,7 @@ describe API::Variables do
it 'does not return project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -76,7 +76,7 @@ describe API::Variables do
post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2', protected: true
end.to change {project.variables.count}.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_truthy
@@ -87,7 +87,7 @@ describe API::Variables do
post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
end.to change {project.variables.count}.by(1)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_falsey
@@ -98,7 +98,7 @@ describe API::Variables do
post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2'
end.to change {project.variables.count}.by(0)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
end
@@ -106,7 +106,7 @@ describe API::Variables do
it 'does not create variable' do
post api("/projects/#{project.id}/variables", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -114,7 +114,7 @@ describe API::Variables do
it 'does not create variable' do
post api("/projects/#{project.id}/variables")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -129,7 +129,7 @@ describe API::Variables do
updated_variable = project.variables.first
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(value_before).to eq(variable.value)
expect(updated_variable.value).to eq('VALUE_1_UP')
expect(updated_variable).to be_protected
@@ -138,7 +138,7 @@ describe API::Variables do
it 'responds with 404 Not Found if requesting non-existing variable' do
put api("/projects/#{project.id}/variables/non_existing_variable", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -146,7 +146,7 @@ describe API::Variables do
it 'does not update variable' do
put api("/projects/#{project.id}/variables/#{variable.key}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -154,7 +154,7 @@ describe API::Variables do
it 'does not update variable' do
put api("/projects/#{project.id}/variables/#{variable.key}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -165,14 +165,14 @@ describe API::Variables do
expect do
delete api("/projects/#{project.id}/variables/#{variable.key}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end.to change {project.variables.count}.by(-1)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
delete api("/projects/#{project.id}/variables/non_existing_variable", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -180,7 +180,7 @@ describe API::Variables do
it 'does not delete variable' do
delete api("/projects/#{project.id}/variables/#{variable.key}", user2)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -188,7 +188,7 @@ describe API::Variables do
it 'does not delete variable' do
delete api("/projects/#{project.id}/variables/#{variable.key}")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index 9e889d1eecf..65bd001e491 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -26,7 +26,7 @@ describe API::Wikis do
it 'returns the list of wiki pages without content' do
get api(url, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(2)
json_response.each_with_index do |page, index|
@@ -39,7 +39,7 @@ describe API::Wikis do
it 'returns the list of wiki pages with content' do
get api(url, user), with_content: 1
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(2)
json_response.each_with_index do |page, index|
@@ -54,14 +54,14 @@ describe API::Wikis do
it 'return the empty list of wiki pages' do
get api(url, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(0)
end
end
shared_examples_for 'returns wiki page' do
it 'returns the wiki page' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(4)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(page.content)
@@ -74,7 +74,7 @@ describe API::Wikis do
it 'creates the wiki page' do
post(api(url, user), payload)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response.size).to eq(4)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(payload[:content])
@@ -89,7 +89,7 @@ describe API::Wikis do
post(api(url, user), payload)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response.size).to eq(1)
expect(json_response['error']).to eq("#{part} is missing")
end
@@ -98,7 +98,7 @@ describe API::Wikis do
shared_examples_for 'updates wiki page' do
it 'updates the wiki page' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(4)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(payload[:content])
@@ -109,7 +109,7 @@ describe API::Wikis do
shared_examples_for '403 Forbidden' do
it 'returns 403 Forbidden' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(json_response.size).to eq(1)
expect(json_response['message']).to eq('403 Forbidden')
end
@@ -117,7 +117,7 @@ describe API::Wikis do
shared_examples_for '404 Wiki Page Not Found' do
it 'returns 404 Wiki Page Not Found' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response.size).to eq(1)
expect(json_response['message']).to eq('404 Wiki Page Not Found')
end
@@ -125,7 +125,7 @@ describe API::Wikis do
shared_examples_for '404 Project Not Found' do
it 'returns 404 Project Not Found' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
expect(json_response.size).to eq(1)
expect(json_response['message']).to eq('404 Project Not Found')
end
@@ -133,7 +133,7 @@ describe API::Wikis do
shared_examples_for '204 No Content' do
it 'returns 204 No Content' do
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index ecac40e301b..a16f98bec36 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -9,7 +9,7 @@ describe 'Git HTTP requests' do
context "when no credentials are provided" do
it "responds to downloads with status 401 Unauthorized (no project existence information leak)" do
download(path) do |response|
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
end
@@ -18,7 +18,7 @@ describe 'Git HTTP requests' do
context "when only username is provided" do
it "responds to downloads with status 401 Unauthorized" do
download(path, user: user.username) do |response|
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
end
@@ -28,7 +28,7 @@ describe 'Git HTTP requests' do
context "when authentication fails" do
it "responds to downloads with status 401 Unauthorized" do
download(path, user: user.username, password: "wrong-password") do |response|
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
end
@@ -37,7 +37,7 @@ describe 'Git HTTP requests' do
context "when authentication succeeds" do
it "does not respond to downloads with status 401 Unauthorized" do
download(path, user: user.username, password: user.password) do |response|
- expect(response).not_to have_http_status(:unauthorized)
+ expect(response).not_to have_gitlab_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to be_nil
end
end
@@ -49,7 +49,7 @@ describe 'Git HTTP requests' do
context "when no credentials are provided" do
it "responds to uploads with status 401 Unauthorized (no project existence information leak)" do
upload(path) do |response|
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
end
@@ -58,7 +58,7 @@ describe 'Git HTTP requests' do
context "when only username is provided" do
it "responds to uploads with status 401 Unauthorized" do
upload(path, user: user.username) do |response|
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
end
@@ -68,7 +68,7 @@ describe 'Git HTTP requests' do
context "when authentication fails" do
it "responds to uploads with status 401 Unauthorized" do
upload(path, user: user.username, password: "wrong-password") do |response|
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
end
@@ -77,7 +77,7 @@ describe 'Git HTTP requests' do
context "when authentication succeeds" do
it "does not respond to uploads with status 401 Unauthorized" do
upload(path, user: user.username, password: user.password) do |response|
- expect(response).not_to have_http_status(:unauthorized)
+ expect(response).not_to have_gitlab_http_status(:unauthorized)
expect(response.header['WWW-Authenticate']).to be_nil
end
end
@@ -88,7 +88,7 @@ describe 'Git HTTP requests' do
shared_examples_for 'pulls are allowed' do
it do
download(path, env) do |response|
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
@@ -97,7 +97,7 @@ describe 'Git HTTP requests' do
shared_examples_for 'pushes are allowed' do
it do
upload(path, env) do |response|
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
@@ -115,7 +115,7 @@ describe 'Git HTTP requests' do
context 'when authenticated' do
it 'rejects downloads and uploads with 404 Not Found' do
download_or_upload(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@@ -165,7 +165,7 @@ describe 'Git HTTP requests' do
it 'rejects pushes with 403 Forbidden' do
upload(path, env) do |response|
- expect(response).to have_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq(git_access_wiki_error(:write_to_wiki))
end
end
@@ -190,13 +190,13 @@ describe 'Git HTTP requests' do
it 'allows clones' do
download(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
end
end
it 'pushes are allowed' do
upload(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
end
end
end
@@ -205,14 +205,14 @@ describe 'Git HTTP requests' do
context 'and not on the team' do
it 'rejects clones with 404 Not Found' do
download(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to eq(git_access_error(:project_not_found))
end
end
it 'rejects pushes with 404 Not Found' do
upload(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to eq(git_access_error(:project_not_found))
end
end
@@ -253,7 +253,7 @@ describe 'Git HTTP requests' do
it 'rejects pushes with 403 Forbidden' do
upload(path, env) do |response|
- expect(response).to have_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:receive_pack_disabled_over_http))
end
end
@@ -264,7 +264,7 @@ describe 'Git HTTP requests' do
allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
download(path, env) do |response|
- expect(response).to have_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:upload_pack_disabled_over_http))
end
end
@@ -276,7 +276,7 @@ describe 'Git HTTP requests' do
it 'rejects pushes with 403 Forbidden' do
upload(path, env) do |response|
- expect(response).to have_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq(change_access_error(:push_code))
end
end
@@ -332,7 +332,7 @@ describe 'Git HTTP requests' do
it 'downloads get status 404 with "project was moved" message' do
clone_get(path, {})
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to match(project_moved_message)
end
end
@@ -355,7 +355,7 @@ describe 'Git HTTP requests' do
clone_get(path, env)
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
@@ -374,7 +374,7 @@ describe 'Git HTTP requests' do
project.team << [user, :master]
download(path, env) do |response|
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
@@ -382,7 +382,7 @@ describe 'Git HTTP requests' do
user.block
download('doesnt/exist.git', env) do |response|
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
@@ -392,7 +392,7 @@ describe 'Git HTTP requests' do
expect(Rack::Attack::Allow2Ban).to receive(:reset).twice
download(path, env) do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
@@ -401,7 +401,7 @@ describe 'Git HTTP requests' do
expect(Rack::Attack::Allow2Ban).to receive(:reset).twice
upload(path, env) do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
@@ -440,14 +440,14 @@ describe 'Git HTTP requests' do
context 'when username and password are provided' do
it 'rejects pulls with personal access token error message' do
download(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
it 'rejects the push attempt with personal access token error message' do
upload(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
@@ -463,19 +463,19 @@ describe 'Git HTTP requests' do
context 'when internal auth is disabled' do
before do
- allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled?) { false }
+ allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled_for_git?) { false }
end
it 'rejects pulls with personal access token error message' do
download(path, user: 'foo', password: 'bar') do |response|
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
it 'rejects pushes with personal access token error message' do
upload(path, user: 'foo', password: 'bar') do |response|
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
@@ -489,7 +489,7 @@ describe 'Git HTTP requests' do
it 'does not display the personal access token error message' do
upload(path, user: 'foo', password: 'bar') do |response|
- expect(response).to have_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
@@ -541,13 +541,13 @@ describe 'Git HTTP requests' do
it 'downloads get status 404 with "project was moved" message' do
clone_get(path, env)
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to match(project_moved_message)
end
it 'uploads get status 404 with "project was moved" message' do
upload(path, env) do |response|
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to match(project_moved_message)
end
end
@@ -557,13 +557,13 @@ describe 'Git HTTP requests' do
context "when the user doesn't have access to the project" do
it "pulls get status 404" do
download(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
it "uploads get status 404" do
upload(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@@ -595,7 +595,7 @@ describe 'Git HTTP requests' do
it "rejects pushes with 403 Forbidden" do
push_get(path, env)
- expect(response).to have_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:upload))
end
@@ -604,7 +604,7 @@ describe 'Git HTTP requests' do
it "rejects pulls for other project with 404 Not Found" do
clone_get("#{other_project.full_path}.git", env)
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to eq(git_access_error(:project_not_found))
end
end
@@ -627,7 +627,7 @@ describe 'Git HTTP requests' do
it 'rejects pulls with 403 Forbidden' do
clone_get path, env
- expect(response).to have_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:no_repo))
end
end
@@ -635,7 +635,7 @@ describe 'Git HTTP requests' do
it 'rejects pushes with 403 Forbidden' do
push_get path, env
- expect(response).to have_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to eq(git_access_error(:upload))
end
end
@@ -648,7 +648,7 @@ describe 'Git HTTP requests' do
it 'downloads from other project get status 403' do
clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
- expect(response).to have_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
@@ -660,7 +660,7 @@ describe 'Git HTTP requests' do
it 'downloads from other project get status 404' do
clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@@ -748,7 +748,7 @@ describe 'Git HTTP requests' do
end
it "returns the file" do
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
end
end
@@ -758,7 +758,7 @@ describe 'Git HTTP requests' do
end
it "returns not found" do
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@@ -783,7 +783,7 @@ describe 'Git HTTP requests' do
context "when the project doesn't exist" do
it "responds with status 404 Not Found" do
download(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@@ -800,7 +800,7 @@ describe 'Git HTTP requests' do
it "responds with status 200" do
clone_get(path, env) do |response|
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 41bf43a9bce..6f40a02aaa9 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -13,12 +13,12 @@ describe JwtController do
context 'existing service' do
subject! { get '/jwt/auth', parameters }
- it { expect(response).to have_http_status(200) }
+ it { expect(response).to have_gitlab_http_status(200) }
context 'returning custom http code' do
let(:service) { double(execute: { http_status: 505 }) }
- it { expect(response).to have_http_status(505) }
+ it { expect(response).to have_gitlab_http_status(505) }
end
end
@@ -41,7 +41,7 @@ describe JwtController do
subject! { get '/jwt/auth', parameters, headers }
- it { expect(response).to have_http_status(401) }
+ it { expect(response).to have_gitlab_http_status(401) }
end
context 'using personal access tokens' do
@@ -56,7 +56,7 @@ describe JwtController do
subject! { get '/jwt/auth', parameters, headers }
it 'authenticates correctly' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(service_class).to have_received(:new).with(nil, user, parameters)
end
end
@@ -75,7 +75,7 @@ describe JwtController do
context 'without personal token' do
it 'rejects the authorization attempt' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
@@ -85,7 +85,7 @@ describe JwtController do
let(:headers) { { authorization: credentials(user.username, access_token.token) } }
it 'accepts the authorization attempt' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
@@ -98,17 +98,17 @@ describe JwtController do
it 'rejects the authorization attempt' do
get '/jwt/auth', parameters, headers
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
context 'when internal auth is disabled' do
it 'rejects the authorization attempt with personal access token message' do
- allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled?) { false }
+ allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled_for_git?) { false }
get '/jwt/auth', parameters, headers
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
end
end
@@ -119,7 +119,7 @@ describe JwtController do
it 'accepts the authorization attempt' do
get '/jwt/auth', parameters
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'allows read access' do
@@ -132,7 +132,7 @@ describe JwtController do
context 'unknown service' do
subject! { get '/jwt/auth', service: 'unknown' }
- it { expect(response).to have_http_status(404) }
+ it { expect(response).to have_gitlab_http_status(404) }
end
def credentials(login, password)
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index bca5bf81c5c..c597623bc4d 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -41,7 +41,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with 501' do
- expect(response).to have_http_status(501)
+ expect(response).to have_gitlab_http_status(501)
expect(json_response).to include('message' => 'Git LFS is not enabled on this GitLab server, contact your admin.')
end
end
@@ -75,13 +75,13 @@ describe 'Git LFS API and storage' do
it 'responds with a 501 message on upload' do
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
- expect(response).to have_http_status(501)
+ expect(response).to have_gitlab_http_status(501)
end
it 'responds with a 501 message on download' do
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
- expect(response).to have_http_status(501)
+ expect(response).to have_gitlab_http_status(501)
end
end
@@ -93,13 +93,13 @@ describe 'Git LFS API and storage' do
it 'responds with a 501 message on upload' do
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
- expect(response).to have_http_status(501)
+ expect(response).to have_gitlab_http_status(501)
end
it 'responds with a 501 message on download' do
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
- expect(response).to have_http_status(501)
+ expect(response).to have_gitlab_http_status(501)
end
end
end
@@ -118,14 +118,14 @@ describe 'Git LFS API and storage' do
it 'responds with a 403 message on upload' do
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
end
it 'responds with a 403 message on download' do
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
end
end
@@ -138,14 +138,14 @@ describe 'Git LFS API and storage' do
it 'responds with a 200 message on upload' do
post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['objects'].first['size']).to eq(1575078)
end
it 'responds with a 200 message on download' do
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
end
@@ -160,7 +160,7 @@ describe 'Git LFS API and storage' do
shared_examples 'a deprecated' do
it 'responds with 501' do
- expect(response).to have_http_status(501)
+ expect(response).to have_gitlab_http_status(501)
end
it 'returns deprecated message' do
@@ -201,7 +201,7 @@ describe 'Git LFS API and storage' do
context 'and request comes from gitlab-workhorse' do
context 'without user being authorized' do
it 'responds with status 401' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -210,7 +210,7 @@ describe 'Git LFS API and storage' do
let(:sendfile) { 'X-Sendfile' }
it 'responds with status 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'responds with the file location' do
@@ -228,7 +228,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -272,7 +272,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -311,7 +311,7 @@ describe 'Git LFS API and storage' do
end
it 'rejects downloading code' do
- expect(response).to have_http_status(other_project_status)
+ expect(response).to have_gitlab_http_status(other_project_status)
end
end
end
@@ -351,7 +351,7 @@ describe 'Git LFS API and storage' do
let(:authorization) { authorize_user }
it 'responds with status 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -387,7 +387,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'with href to download' do
@@ -415,7 +415,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'with href to download' do
@@ -446,7 +446,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'with an 404 for specific object' do
@@ -483,7 +483,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'responds with upload hypermedia link for the new object' do
@@ -528,7 +528,7 @@ describe 'Git LFS API and storage' do
let(:update_user_permissions) { nil }
it 'responds with 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -536,7 +536,7 @@ describe 'Git LFS API and storage' do
let(:role) { :guest }
it 'responds with 403' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -564,7 +564,7 @@ describe 'Git LFS API and storage' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
it 'rejects downloading code' do
- expect(response).to have_http_status(other_project_status)
+ expect(response).to have_gitlab_http_status(other_project_status)
end
end
end
@@ -608,7 +608,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200 and href to download' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'responds with status 200 and href to download' do
@@ -636,7 +636,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with authorization required' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -654,6 +654,20 @@ describe 'Git LFS API and storage' do
}
end
+ shared_examples 'pushes new LFS objects' do
+ let(:sample_size) { 150.megabytes }
+ let(:sample_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
+
+ it 'responds with upload hypermedia link' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['objects']).to be_kind_of(Array)
+ expect(json_response['objects'].first['oid']).to eq(sample_oid)
+ expect(json_response['objects'].first['size']).to eq(sample_size)
+ expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.full_path}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}")
+ expect(json_response['objects'].first['actions']['upload']['header']).to eq('Authorization' => authorization)
+ end
+ end
+
describe 'when request is authenticated' do
describe 'when user has project push access' do
let(:authorization) { authorize_user }
@@ -669,7 +683,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'responds with links the object to the project' do
@@ -684,27 +698,7 @@ describe 'Git LFS API and storage' do
end
context 'when pushing a lfs object that does not exist' do
- let(:body) do
- {
- 'operation' => 'upload',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078 }
- ]
- }
- end
-
- it 'responds with status 200' do
- expect(response).to have_http_status(200)
- end
-
- it 'responds with upload hypermedia link' do
- expect(json_response['objects']).to be_kind_of(Array)
- expect(json_response['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
- expect(json_response['objects'].first['size']).to eq(1575078)
- expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.full_path}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
- expect(json_response['objects'].first['actions']['upload']['header']).to eq('Authorization' => authorization)
- end
+ it_behaves_like 'pushes new LFS objects'
end
context 'when pushing one new and one existing lfs object' do
@@ -725,7 +719,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'responds with upload hypermedia link for the new object' do
@@ -747,7 +741,7 @@ describe 'Git LFS API and storage' do
let(:authorization) { authorize_user }
it 'responds with 403' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -761,7 +755,7 @@ describe 'Git LFS API and storage' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 403 (not 404 because project is public)' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -772,7 +766,7 @@ describe 'Git LFS API and storage' do
# I'm not sure what this tests that is different from the previous test
it 'responds with 403 (not 404 because project is public)' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -781,10 +775,21 @@ describe 'Git LFS API and storage' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it 'responds with 403 (not 404 because project is public)' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
+
+ context 'when deploy key has project push access' do
+ let(:key) { create(:deploy_key, can_push: true) }
+ let(:authorization) { authorize_deploy_key }
+
+ let(:update_user_permissions) do
+ project.deploy_keys << key
+ end
+
+ it_behaves_like 'pushes new LFS objects'
+ end
end
context 'when user is not authenticated' do
@@ -794,13 +799,13 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 401' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
context 'when user does not have push access' do
it 'responds with status 401' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -820,7 +825,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 404' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -865,7 +870,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 401' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -875,7 +880,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 401' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -885,7 +890,7 @@ describe 'Git LFS API and storage' do
end
it 'does not recognize it as a valid lfs command' do
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
end
@@ -897,7 +902,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with 403' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -907,7 +912,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with 403' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -917,7 +922,7 @@ describe 'Git LFS API and storage' do
end
it 'does not recognize it as a valid lfs command' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -945,7 +950,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'uses the gitlab-workhorse content type' do
@@ -965,7 +970,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'lfs object is linked to the project' do
@@ -976,12 +981,12 @@ describe 'Git LFS API and storage' do
context 'invalid tempfiles' do
it 'rejects slashes in the tempfile name (path traversal' do
put_finalize('foo/bar')
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it 'rejects tempfile names that do not start with the oid' do
put_finalize("foo#{sample_oid}")
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -1010,7 +1015,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with 403 (not 404 because the build user can read the project)' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -1024,7 +1029,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with 404 (do not leak non-public project existence)' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -1037,7 +1042,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with 404 (do not leak non-public project existence)' do
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
end
@@ -1066,7 +1071,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'with location of lfs store and object details' do
@@ -1082,7 +1087,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'lfs object is linked to the source project' do
@@ -1110,7 +1115,7 @@ describe 'Git LFS API and storage' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 403 (not 404 because project is public)' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -1121,7 +1126,7 @@ describe 'Git LFS API and storage' do
# I'm not sure what this tests that is different from the previous test
it 'responds with 403 (not 404 because project is public)' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -1130,7 +1135,7 @@ describe 'Git LFS API and storage' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it 'responds with 403 (not 404 because project is public)' do
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -1155,7 +1160,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it 'links the lfs object to the project' do
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index a927de952d0..1a5ad9b04e4 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -37,7 +37,7 @@ describe 'OpenID Connect requests' do
it 'userinfo response is unauthorized' do
request_user_info
- expect(response).to have_http_status 403
+ expect(response).to have_gitlab_http_status 403
expect(response.body).to be_blank
end
end
@@ -107,6 +107,15 @@ describe 'OpenID Connect requests' do
end
end
+ # These 2 calls shouldn't actually throw, they should be handled as an
+ # unauthorized request, so we should be able to check the response.
+ #
+ # This was not possible due to an issue with Warden:
+ # https://github.com/hassox/warden/pull/162
+ #
+ # When the patch gets merged and we update Warden, these specs will need to
+ # updated to check the response instead of a raised exception.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/40218
context 'when user is blocked' do
it 'returns authentication error' do
access_grant
@@ -114,7 +123,7 @@ describe 'OpenID Connect requests' do
expect do
request_access_token
- end.to throw_symbol :warden
+ end.to raise_error UncaughtThrowError
end
end
@@ -125,7 +134,7 @@ describe 'OpenID Connect requests' do
expect do
request_access_token
- end.to throw_symbol :warden
+ end.to raise_error UncaughtThrowError
end
end
end
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index 6667ce771bd..286d8a884a4 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -99,19 +99,19 @@ describe 'cycle analytics events' do
it 'does not list the test events' do
get project_cycle_analytics_test_path(project, format: :json)
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
it 'does not list the staging events' do
get project_cycle_analytics_staging_path(project, format: :json)
- expect(response).to have_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
end
it 'lists the issue events' do
get project_cycle_analytics_issue_path(project, format: :json)
- expect(response).to have_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
end
end
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
new file mode 100644
index 00000000000..0fec14d0cce
--- /dev/null
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -0,0 +1,362 @@
+require 'spec_helper'
+
+describe 'Rack Attack global throttles' do
+ let(:settings) { Gitlab::CurrentSettings.current_application_settings }
+
+ # Start with really high limits and override them with low limits to ensure
+ # the right settings are being exercised
+ let(:settings_to_set) do
+ {
+ throttle_unauthenticated_requests_per_period: 100,
+ throttle_unauthenticated_period_in_seconds: 1,
+ throttle_authenticated_api_requests_per_period: 100,
+ throttle_authenticated_api_period_in_seconds: 1,
+ throttle_authenticated_web_requests_per_period: 100,
+ throttle_authenticated_web_period_in_seconds: 1
+ }
+ end
+
+ let(:requests_per_period) { 1 }
+ let(:period_in_seconds) { 10000 }
+ let(:period) { period_in_seconds.seconds }
+
+ let(:url_that_does_not_require_authentication) { '/users/sign_in' }
+ let(:url_that_requires_authentication) { '/dashboard/snippets' }
+ let(:api_partial_url) { '/todos' }
+
+ around do |example|
+ # Instead of test environment's :null_store so the throttles can increment
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
+
+ # Make time-dependent tests deterministic
+ Timecop.freeze { example.run }
+
+ Rack::Attack.cache.store = Rails.cache
+ end
+
+ # Requires let variables:
+ # * throttle_setting_prefix (e.g. "throttle_authenticated_api" or "throttle_authenticated_web")
+ # * get_args
+ # * other_user_get_args
+ shared_examples_for 'rate-limited token-authenticated requests' do
+ before do
+ # Set low limits
+ settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
+ settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
+ end
+
+ context 'when the throttle is enabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the rate limit' do
+ # At first, allow requests under the rate limit.
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ # the last straw
+ expect_rejection { get(*get_args) }
+ end
+
+ it 'allows requests after throttling and then waiting for the next period' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get(*get_args) }
+
+ Timecop.travel(period.from_now) do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get(*get_args) }
+ end
+ end
+
+ it 'counts requests from different users separately, even from the same IP' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ # would be over the limit if this wasn't a different user
+ get(*other_user_get_args)
+ expect(response).to have_http_status 200
+ end
+
+ it 'counts all requests from the same user, even via different IPs' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
+
+ expect_rejection { get(*get_args) }
+ end
+ end
+
+ context 'when the throttle is disabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+ end
+
+ describe 'unauthenticated requests' do
+ before do
+ # Set low limits
+ settings_to_set[:throttle_unauthenticated_requests_per_period] = requests_per_period
+ settings_to_set[:throttle_unauthenticated_period_in_seconds] = period_in_seconds
+ end
+
+ context 'when the throttle is enabled' do
+ before do
+ settings_to_set[:throttle_unauthenticated_enabled] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the rate limit' do
+ # At first, allow requests under the rate limit.
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+
+ # the last straw
+ expect_rejection { get url_that_does_not_require_authentication }
+ end
+
+ it 'allows requests after throttling and then waiting for the next period' do
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get url_that_does_not_require_authentication }
+
+ Timecop.travel(period.from_now) do
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get url_that_does_not_require_authentication }
+ end
+ end
+
+ it 'counts requests from different IPs separately' do
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
+
+ # would be over limit for the same IP
+ get url_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+ end
+
+ context 'when the throttle is disabled' do
+ before do
+ settings_to_set[:throttle_unauthenticated_enabled] = false
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+ end
+
+ describe 'API requests authenticated with personal access token', :api do
+ let(:user) { create(:user) }
+ let(:token) { create(:personal_access_token, user: user) }
+ let(:other_user) { create(:user) }
+ let(:other_user_token) { create(:personal_access_token, user: other_user) }
+ let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
+
+ context 'with the token in the query string' do
+ let(:get_args) { [api(api_partial_url, personal_access_token: token)] }
+ let(:other_user_get_args) { [api(api_partial_url, personal_access_token: other_user_token)] }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+
+ context 'with the token in the headers' do
+ let(:get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(token)) }
+ let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(other_user_token)) }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+ end
+
+ describe 'API requests authenticated with OAuth token', :api do
+ let(:user) { create(:user) }
+ let(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
+ let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") }
+ let(:other_user) { create(:user) }
+ let(:other_user_application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: other_user) }
+ let(:other_user_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: other_user.id, scopes: "api") }
+ let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
+
+ context 'with the token in the query string' do
+ let(:get_args) { [api(api_partial_url, oauth_access_token: token)] }
+ let(:other_user_get_args) { [api(api_partial_url, oauth_access_token: other_user_token)] }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+
+ context 'with the token in the headers' do
+ let(:get_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(token)) }
+ let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(other_user_token)) }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+ end
+
+ describe '"web" (non-API) requests authenticated with RSS token' do
+ let(:user) { create(:user) }
+ let(:other_user) { create(:user) }
+ let(:throttle_setting_prefix) { 'throttle_authenticated_web' }
+
+ context 'with the token in the query string' do
+ let(:get_args) { [rss_url(user), nil] }
+ let(:other_user_get_args) { [rss_url(other_user), nil] }
+
+ it_behaves_like 'rate-limited token-authenticated requests'
+ end
+ end
+
+ describe 'web requests authenticated with regular login' do
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+
+ # Set low limits
+ settings_to_set[:throttle_authenticated_web_requests_per_period] = requests_per_period
+ settings_to_set[:throttle_authenticated_web_period_in_seconds] = period_in_seconds
+ end
+
+ context 'when the throttle is enabled' do
+ before do
+ settings_to_set[:throttle_authenticated_web_enabled] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the rate limit' do
+ # At first, allow requests under the rate limit.
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ # the last straw
+ expect_rejection { get url_that_requires_authentication }
+ end
+
+ it 'allows requests after throttling and then waiting for the next period' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get url_that_requires_authentication }
+
+ Timecop.travel(period.from_now) do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get url_that_requires_authentication }
+ end
+ end
+
+ it 'counts requests from different users separately, even from the same IP' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ # would be over the limit if this wasn't a different user
+ login_as(create(:user))
+
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ it 'counts all requests from the same user, even via different IPs' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
+
+ expect_rejection { get url_that_requires_authentication }
+ end
+ end
+
+ context 'when the throttle is disabled' do
+ before do
+ settings_to_set[:throttle_authenticated_web_enabled] = false
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+ end
+
+ def api_get_args_with_token_headers(partial_url, token_headers)
+ ["/api/#{API::API.version}#{partial_url}", nil, token_headers]
+ end
+
+ def rss_url(user)
+ "/dashboard/projects.atom?rss_token=#{user.rss_token}"
+ end
+
+ def private_token_headers(user)
+ { 'HTTP_PRIVATE_TOKEN' => user.private_token }
+ end
+
+ def personal_access_token_headers(personal_access_token)
+ { 'HTTP_PRIVATE_TOKEN' => personal_access_token.token }
+ end
+
+ def oauth_token_headers(oauth_access_token)
+ { 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" }
+ end
+
+ def expect_rejection(&block)
+ yield
+
+ expect(response).to have_http_status(429)
+ end
+end
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
new file mode 100644
index 00000000000..71788028cbf
--- /dev/null
+++ b/spec/routing/group_routing_spec.rb
@@ -0,0 +1,133 @@
+require 'spec_helper'
+
+describe "Groups", "routing" do
+ let(:group_path) { 'complex.group-namegit' }
+ let!(:group) { create(:group, path: group_path) }
+
+ it "to #show" do
+ expect(get("/groups/#{group_path}")).to route_to('groups#show', id: group_path)
+ end
+
+ it "also supports nested groups" do
+ nested_group = create(:group, parent: group)
+ expect(get("/#{group_path}/#{nested_group.path}")).to route_to('groups#show', id: "#{group_path}/#{nested_group.path}")
+ end
+
+ it "also display group#show on the short path" do
+ expect(get("/#{group_path}")).to route_to('groups#show', id: group_path)
+ end
+
+ it "to #activity" do
+ expect(get("/groups/#{group_path}/-/activity")).to route_to('groups#activity', id: group_path)
+ end
+
+ it "to #issues" do
+ expect(get("/groups/#{group_path}/-/issues")).to route_to('groups#issues', id: group_path)
+ end
+
+ it "to #members" do
+ expect(get("/groups/#{group_path}/-/group_members")).to route_to('groups/group_members#index', group_id: group_path)
+ end
+
+ it "to #labels" do
+ expect(get("/groups/#{group_path}/-/labels")).to route_to('groups/labels#index', group_id: group_path)
+ end
+
+ it "to #milestones" do
+ expect(get("/groups/#{group_path}/-/milestones")).to route_to('groups/milestones#index', group_id: group_path)
+ end
+
+ describe 'legacy redirection' do
+ describe 'labels' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/labels", "/groups/complex.group-namegit/-/labels" do
+ let(:resource) { create(:group, parent: group, path: 'labels') }
+ end
+
+ context 'when requesting JSON' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/labels.json", "/groups/complex.group-namegit/-/labels.json" do
+ let(:resource) { create(:group, parent: group, path: 'labels') }
+ end
+ end
+ end
+
+ describe 'group_members' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/group_members", "/groups/complex.group-namegit/-/group_members" do
+ let(:resource) { create(:group, parent: group, path: 'group_members') }
+ end
+ end
+
+ describe 'avatar' do
+ it 'routes to the avatars controller' do
+ expect(delete("/groups/#{group_path}/-/avatar"))
+ .to route_to(group_id: group_path,
+ controller: 'groups/avatars',
+ action: 'destroy')
+ end
+ end
+
+ describe 'milestones' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones", "/groups/complex.group-namegit/-/milestones" do
+ let(:resource) { create(:group, parent: group, path: 'milestones') }
+ end
+
+ context 'nested routes' do
+ include RSpec::Rails::RequestExampleGroup
+
+ let(:milestone) { create(:milestone, group: group) }
+
+ it 'redirects the nested routes' do
+ request = get("/groups/#{group_path}/milestones/#{milestone.id}/merge_requests")
+ expect(request).to redirect_to("/groups/#{group_path}/-/milestones/#{milestone.id}/merge_requests")
+ end
+ end
+
+ context 'with a query string' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones?hello=world", "/groups/complex.group-namegit/-/milestones?hello=world" do
+ let(:resource) { create(:group, parent: group, path: 'milestones') }
+ end
+
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones?milestones=/milestones", "/groups/complex.group-namegit/-/milestones?milestones=/milestones" do
+ let(:resource) { create(:group, parent: group, path: 'milestones') }
+ end
+ end
+ end
+
+ describe 'edit' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/edit", "/groups/complex.group-namegit/-/edit" do
+ let(:resource) do
+ pending('still rejected because of the wildcard reserved word')
+ create(:group, parent: group, path: 'edit')
+ end
+ end
+ end
+
+ describe 'issues' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/issues", "/groups/complex.group-namegit/-/issues" do
+ let(:resource) { create(:group, parent: group, path: 'issues') }
+ end
+ end
+
+ describe 'merge_requests' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/merge_requests", "/groups/complex.group-namegit/-/merge_requests" do
+ let(:resource) { create(:group, parent: group, path: 'merge_requests') }
+ end
+ end
+
+ describe 'projects' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/projects", "/groups/complex.group-namegit/-/projects" do
+ let(:resource) { create(:group, parent: group, path: 'projects') }
+ end
+ end
+
+ describe 'activity' do
+ it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/activity", "/groups/complex.group-namegit/-/activity" do
+ let(:resource) { create(:group, parent: group, path: 'activity') }
+ end
+
+ it_behaves_like 'redirecting a legacy path', "/groups/activity/activity", "/groups/activity/-/activity" do
+ let!(:parent) { create(:group, path: 'activity') }
+ let(:resource) { create(:group, parent: parent, path: 'activity') }
+ end
+ end
+ end
+end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 39d44245c3f..fb1281a6b42 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -426,18 +426,23 @@ describe 'project routing' do
end
end
- # project_milestones GET /:project_id/milestones(.:format) milestones#index
- # POST /:project_id/milestones(.:format) milestones#create
- # new_project_milestone GET /:project_id/milestones/new(.:format) milestones#new
- # edit_project_milestone GET /:project_id/milestones/:id/edit(.:format) milestones#edit
- # project_milestone GET /:project_id/milestones/:id(.:format) milestones#show
- # PUT /:project_id/milestones/:id(.:format) milestones#update
- # DELETE /:project_id/milestones/:id(.:format) milestones#destroy
+ # project_milestones GET /:project_id/milestones(.:format) milestones#index
+ # POST /:project_id/milestones(.:format) milestones#create
+ # new_project_milestone GET /:project_id/milestones/new(.:format) milestones#new
+ # edit_project_milestone GET /:project_id/milestones/:id/edit(.:format) milestones#edit
+ # project_milestone GET /:project_id/milestones/:id(.:format) milestones#show
+ # PUT /:project_id/milestones/:id(.:format) milestones#update
+ # DELETE /:project_id/milestones/:id(.:format) milestones#destroy
+ # promote_project_milestone POST /:project_id/milestones/:id/promote milestones#promote
describe Projects::MilestonesController, 'routing' do
it_behaves_like 'RESTful project resources' do
let(:controller) { 'milestones' }
let(:actions) { [:index, :create, :new, :edit, :show, :update] }
end
+
+ it 'to #promote' do
+ expect(post('/gitlab/gitlabhq/milestones/1/promote')).to route_to('projects/milestones#promote', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "1")
+ end
end
# project_labels GET /:project_id/labels(.:format) labels#index
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index a45839b16f5..91aefa84d0e 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -135,7 +135,6 @@ end
# profile_history GET /profile/history(.:format) profile#history
# profile_password PUT /profile/password(.:format) profile#password_update
# profile_token GET /profile/token(.:format) profile#token
-# profile_reset_private_token PUT /profile/reset_private_token(.:format) profile#reset_private_token
# profile GET /profile(.:format) profile#show
# profile_update PUT /profile/update(.:format) profile#update
describe ProfilesController, "routing" do
@@ -147,10 +146,6 @@ describe ProfilesController, "routing" do
expect(get("/profile/audit_log")).to route_to('profiles#audit_log')
end
- it "to #reset_private_token" do
- expect(put("/profile/reset_private_token")).to route_to('profiles#reset_private_token')
- end
-
it "to #reset_rss_token" do
expect(put("/profile/reset_rss_token")).to route_to('profiles#reset_rss_token')
end
@@ -262,8 +257,10 @@ describe "Authentication", "routing" do
expect(post("/users/sign_in")).to route_to('sessions#create')
end
- it "DELETE /users/sign_out" do
- expect(delete("/users/sign_out")).to route_to('sessions#destroy')
+ # sign_out with GET instead of DELETE facilitates ad-hoc single-sign-out processes
+ # (https://gitlab.com/gitlab-org/gitlab-ce/issues/39708)
+ it "GET /users/sign_out" do
+ expect(get("/users/sign_out")).to route_to('sessions#destroy')
end
it "POST /users/password" do
@@ -283,42 +280,6 @@ describe "Authentication", "routing" do
end
end
-describe "Groups", "routing" do
- let(:name) { 'complex.group-namegit' }
-
- before do
- allow_any_instance_of(GroupUrlConstrainer).to receive(:matches?).and_return(true)
- end
-
- it "to #show" do
- expect(get("/groups/#{name}")).to route_to('groups#show', id: name)
- end
-
- it "also supports nested groups" do
- expect(get("/#{name}/#{name}")).to route_to('groups#show', id: "#{name}/#{name}")
- end
-
- it "also display group#show on the short path" do
- expect(get("/#{name}")).to route_to('groups#show', id: name)
- end
-
- it "to #activity" do
- expect(get("/groups/#{name}/activity")).to route_to('groups#activity', id: name)
- end
-
- it "to #issues" do
- expect(get("/groups/#{name}/issues")).to route_to('groups#issues', id: name)
- end
-
- it "to #members" do
- expect(get("/groups/#{name}/group_members")).to route_to('groups/group_members#index', group_id: name)
- end
-
- it "also display group#show with slash in the path" do
- expect(get('/group/subgroup')).to route_to('groups#show', id: 'group/subgroup')
- end
-end
-
describe HealthCheckController, 'routing' do
it 'to #index' do
expect(get('/health_check')).to route_to('health_check#index')
diff --git a/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb b/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb
new file mode 100644
index 00000000000..8899dc85384
--- /dev/null
+++ b/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb
@@ -0,0 +1,160 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/line_break_after_guard_clauses'
+
+describe RuboCop::Cop::LineBreakAfterGuardClauses do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ shared_examples 'examples with guard clause' do |title|
+ %w[if unless].each do |conditional|
+ it "flags violation for #{title} #{conditional} without line breaks" do
+ source = <<~RUBY
+ #{title} #{conditional} condition
+ do_stuff
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses.size).to eq(1)
+ offense = cop.offenses.first
+
+ expect(offense.line).to eq(1)
+ expect(cop.highlights).to eq(["#{title} #{conditional} condition"])
+ expect(offense.message).to eq('Add a line break after guard clauses')
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} with line break" do
+ source = <<~RUBY
+ #{title} #{conditional} condition
+
+ do_stuff
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} on multiple lines without line break" do
+ source = <<~RUBY
+ #{conditional} condition
+ #{title}
+ end
+ do_stuff
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by end keyword" do
+ source = <<~RUBY
+ def test
+ #{title} #{conditional} condition
+ end
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by elsif keyword" do
+ source = <<~RUBY
+ if model
+ #{title} #{conditional} condition
+ elsif
+ do_something
+ end
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by else keyword" do
+ source = <<~RUBY
+ if model
+ #{title} #{conditional} condition
+ else
+ do_something
+ end
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by when keyword" do
+ source = <<~RUBY
+ case model
+ when condition_a
+ #{title} #{conditional} condition
+ when condition_b
+ do_something
+ end
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by rescue keyword" do
+ source = <<~RUBY
+ begin
+ #{title} #{conditional} condition
+ rescue StandardError
+ do_something
+ end
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by ensure keyword" do
+ source = <<~RUBY
+ def foo
+ #{title} #{conditional} condition
+ ensure
+ do_something
+ end
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by another guard clause" do
+ source = <<~RUBY
+ #{title} #{conditional} condition
+ #{title} #{conditional} condition
+
+ do_stuff
+ RUBY
+ inspect_source(cop, source)
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "autocorrects #{title} #{conditional} guard clauses without line break" do
+ source = <<~RUBY
+ #{title} #{conditional} condition
+ do_stuff
+ RUBY
+ autocorrected = autocorrect_source(cop, source)
+
+ expected_source = <<~RUBY
+ #{title} #{conditional} condition
+
+ do_stuff
+ RUBY
+ expect(autocorrected).to eql(expected_source)
+ end
+ end
+ end
+
+ %w[return fail raise next break throw].each do |example|
+ it_behaves_like 'examples with guard clause', example
+ end
+end
diff --git a/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb b/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb
deleted file mode 100644
index 07cb3fc4a2e..00000000000
--- a/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-require 'spec_helper'
-
-require 'rubocop'
-require 'rubocop/rspec/support'
-
-require_relative '../../../../rubocop/cop/migration/add_column_with_default_to_large_table'
-
-describe RuboCop::Cop::Migration::AddColumnWithDefaultToLargeTable do
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- context 'in migration' do
- before do
- allow(cop).to receive(:in_migration?).and_return(true)
- end
-
- described_class::LARGE_TABLES.each do |table|
- it "registers an offense for the #{table} table" do
- inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- end
- end
- end
-
- it 'registers no offense for non-blacklisted tables' do
- inspect_source(cop, "add_column_with_default :table, :column, default: true")
-
- expect(cop.offenses).to be_empty
- end
- end
-
- context 'outside of migration' do
- it 'registers no offense' do
- table = described_class::LARGE_TABLES.sample
- inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
-
- expect(cop.offenses).to be_empty
- end
- end
-end
diff --git a/spec/rubocop/cop/migration/datetime_spec.rb b/spec/rubocop/cop/migration/datetime_spec.rb
index 388b086ce6a..b1dfcf1b048 100644
--- a/spec/rubocop/cop/migration/datetime_spec.rb
+++ b/spec/rubocop/cop/migration/datetime_spec.rb
@@ -9,6 +9,7 @@ describe RuboCop::Cop::Migration::Datetime do
include CopHelper
subject(:cop) { described_class.new }
+
let(:migration_with_datetime) do
%q(
class Users < ActiveRecord::Migration
@@ -22,6 +23,19 @@ describe RuboCop::Cop::Migration::Datetime do
)
end
+ let(:migration_with_timestamp) do
+ %q(
+ class Users < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column(:users, :username, :text)
+ add_column(:users, :last_sign_in, :timestamp)
+ end
+ end
+ )
+ end
+
let(:migration_without_datetime) do
%q(
class Users < ActiveRecord::Migration
@@ -58,6 +72,17 @@ describe RuboCop::Cop::Migration::Datetime do
aggregate_failures do
expect(cop.offenses.size).to eq(1)
expect(cop.offenses.map(&:line)).to eq([7])
+ expect(cop.offenses.first.message).to include('datetime')
+ end
+ end
+
+ it 'registers an offense when the ":timestamp" data type is used' do
+ inspect_source(cop, migration_with_timestamp)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([7])
+ expect(cop.offenses.first.message).to include('timestamp')
end
end
@@ -81,6 +106,7 @@ describe RuboCop::Cop::Migration::Datetime do
context 'outside of migration' do
it 'registers no offense' do
inspect_source(cop, migration_with_datetime)
+ inspect_source(cop, migration_with_timestamp)
inspect_source(cop, migration_without_datetime)
inspect_source(cop, migration_with_datetime_with_timezone)
diff --git a/spec/rubocop/cop/migration/update_large_table_spec.rb b/spec/rubocop/cop/migration/update_large_table_spec.rb
new file mode 100644
index 00000000000..17b19e139e4
--- /dev/null
+++ b/spec/rubocop/cop/migration/update_large_table_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/update_large_table'
+
+describe RuboCop::Cop::Migration::UpdateLargeTable do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ shared_examples 'large tables' do |update_method|
+ described_class::LARGE_TABLES.each do |table|
+ it "registers an offense for the #{table} table" do
+ inspect_source(cop, "#{update_method} :#{table}, :column, default: true")
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+ end
+ end
+
+ context 'for the add_column_with_default method' do
+ include_examples 'large tables', 'add_column_with_default'
+ end
+
+ context 'for the update_column_in_batches method' do
+ include_examples 'large tables', 'update_column_in_batches'
+ end
+
+ it 'registers no offense for non-blacklisted tables' do
+ inspect_source(cop, "add_column_with_default :table, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it 'registers no offense for non-blacklisted methods' do
+ table = described_class::LARGE_TABLES.sample
+
+ inspect_source(cop, "some_other_method :#{table}, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'outside of migration' do
+ let(:table) { described_class::LARGE_TABLES.sample }
+
+ it 'registers no offense for add_column_with_default' do
+ inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it 'registers no offense for update_column_in_batches' do
+ inspect_source(cop, "add_column_with_default :#{table}, :column, default: true")
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+end
diff --git a/spec/rubocop/cop/rspec/env_assignment_spec.rb b/spec/rubocop/cop/rspec/env_assignment_spec.rb
new file mode 100644
index 00000000000..4e859b6f6fa
--- /dev/null
+++ b/spec/rubocop/cop/rspec/env_assignment_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/rspec/env_assignment'
+
+describe RuboCop::Cop::RSpec::EnvAssignment do
+ include CopHelper
+
+ OFFENSE_CALL_SINGLE_QUOTES_KEY = %(ENV['FOO'] = 'bar').freeze
+ OFFENSE_CALL_DOUBLE_QUOTES_KEY = %(ENV["FOO"] = 'bar').freeze
+
+ let(:source_file) { 'spec/foo_spec.rb' }
+
+ subject(:cop) { described_class.new }
+
+ shared_examples 'an offensive ENV#[]= call' do |content|
+ it "registers an offense for `#{content}`" do
+ inspect_source(cop, content, source_file)
+
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ expect(cop.highlights).to eq([content])
+ end
+ end
+
+ shared_examples 'an autocorrected ENV#[]= call' do |content, autocorrected_content|
+ it "registers an offense for `#{content}` and autocorrects it to `#{autocorrected_content}`" do
+ autocorrected = autocorrect_source(cop, content, source_file)
+
+ expect(autocorrected).to eql(autocorrected_content)
+ end
+ end
+
+ context 'in a spec file' do
+ before do
+ allow(cop).to receive(:in_spec?).and_return(true)
+ end
+
+ context 'with a key using single quotes' do
+ it_behaves_like 'an offensive ENV#[]= call', OFFENSE_CALL_SINGLE_QUOTES_KEY
+ it_behaves_like 'an autocorrected ENV#[]= call', OFFENSE_CALL_SINGLE_QUOTES_KEY, %(stub_env('FOO', 'bar'))
+ end
+
+ context 'with a key using double quotes' do
+ it_behaves_like 'an offensive ENV#[]= call', OFFENSE_CALL_DOUBLE_QUOTES_KEY
+ it_behaves_like 'an autocorrected ENV#[]= call', OFFENSE_CALL_DOUBLE_QUOTES_KEY, %(stub_env("FOO", 'bar'))
+ end
+ end
+
+ context 'outside of a spec file' do
+ it "does not register an offense for `#{OFFENSE_CALL_SINGLE_QUOTES_KEY}` in a non-spec file" do
+ inspect_source(cop, OFFENSE_CALL_SINGLE_QUOTES_KEY)
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb b/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb
new file mode 100644
index 00000000000..278662d32ea
--- /dev/null
+++ b/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/rspec/verbose_include_metadata'
+
+describe RuboCop::Cop::RSpec::VerboseIncludeMetadata do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ let(:source_file) { 'foo_spec.rb' }
+
+ # Override `CopHelper#inspect_source` to always appear to be in a spec file,
+ # so that our RSpec-only cop actually runs
+ def inspect_source(*args)
+ super(*args, source_file)
+ end
+
+ shared_examples 'examples with include syntax' do |title|
+ it "flags violation for #{title} examples that uses verbose include syntax" do
+ inspect_source(cop, "#{title} 'Test', js: true do; end")
+
+ expect(cop.offenses.size).to eq(1)
+ offense = cop.offenses.first
+
+ expect(offense.line).to eq(1)
+ expect(cop.highlights).to eq(["#{title} 'Test', js: true"])
+ expect(offense.message).to eq('Use `:js` instead of `js: true`.')
+ end
+
+ it "doesn't flag violation for #{title} examples that uses compact include syntax", :aggregate_failures do
+ inspect_source(cop, "#{title} 'Test', :js do; end")
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "doesn't flag violation for #{title} examples that uses flag: symbol" do
+ inspect_source(cop, "#{title} 'Test', flag: :symbol do; end")
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it "autocorrects #{title} examples that uses verbose syntax into compact syntax" do
+ autocorrected = autocorrect_source(cop, "#{title} 'Test', js: true do; end", source_file)
+
+ expect(autocorrected).to eql("#{title} 'Test', :js do; end")
+ end
+ end
+
+ %w(describe context feature example_group it specify example scenario its).each do |example|
+ it_behaves_like 'examples with include syntax', example
+ end
+end
diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb
new file mode 100644
index 00000000000..87c7b2ad36e
--- /dev/null
+++ b/spec/serializers/cluster_application_entity_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe ClusterApplicationEntity do
+ describe '#as_json' do
+ let(:application) { build(:cluster_applications_helm) }
+ subject { described_class.new(application).as_json }
+
+ it 'has name' do
+ expect(subject[:name]).to eq(application.name)
+ end
+
+ it 'has status' do
+ expect(subject[:status]).to eq(:not_installable)
+ end
+
+ it 'has no status_reason' do
+ expect(subject[:status_reason]).to be_nil
+ end
+
+ context 'when application is errored' do
+ let(:application) { build(:cluster_applications_helm, :errored) }
+
+ it 'has corresponded data' do
+ expect(subject[:status]).to eq(:errored)
+ expect(subject[:status_reason]).not_to be_nil
+ expect(subject[:status_reason]).to eq(application.status_reason)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb
index 2c7f49974f1..d6a43fd0f00 100644
--- a/spec/serializers/cluster_entity_spec.rb
+++ b/spec/serializers/cluster_entity_spec.rb
@@ -1,22 +1,51 @@
require 'spec_helper'
describe ClusterEntity do
- set(:cluster) { create(:gcp_cluster, :errored) }
- let(:request) { double('request') }
+ describe '#as_json' do
+ subject { described_class.new(cluster).as_json }
- let(:entity) do
- described_class.new(cluster)
- end
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
- describe '#as_json' do
- subject { entity.as_json }
+ context 'when status is creating' do
+ let(:provider) { create(:cluster_provider_gcp, :creating) }
+
+ it 'has corresponded data' do
+ expect(subject[:status]).to eq(:creating)
+ expect(subject[:status_reason]).to be_nil
+ end
+ end
+
+ context 'when status is errored' do
+ let(:provider) { create(:cluster_provider_gcp, :errored) }
- it 'contains status' do
- expect(subject[:status]).to eq(:errored)
+ it 'has corresponded data' do
+ expect(subject[:status]).to eq(:errored)
+ expect(subject[:status_reason]).to eq(provider.status_reason)
+ end
+ end
end
- it 'contains status reason' do
- expect(subject[:status_reason]).to eq('general error')
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
+
+ it 'has corresponded data' do
+ expect(subject[:status]).to eq(:created)
+ expect(subject[:status_reason]).to be_nil
+ end
+ end
+
+ context 'when no application has been installed' do
+ let(:cluster) { create(:cluster) }
+ subject { described_class.new(cluster).as_json[:applications]}
+
+ it 'contains helm as not_installable' do
+ expect(subject).not_to be_empty
+
+ helm = subject[0]
+ expect(helm[:name]).to eq('helm')
+ expect(helm[:status]).to eq(:not_installable)
+ end
end
end
end
diff --git a/spec/serializers/cluster_serializer_spec.rb b/spec/serializers/cluster_serializer_spec.rb
index 1ac6784d28f..5e9f7a45891 100644
--- a/spec/serializers/cluster_serializer_spec.rb
+++ b/spec/serializers/cluster_serializer_spec.rb
@@ -1,18 +1,23 @@
require 'spec_helper'
describe ClusterSerializer do
- let(:serializer) do
- described_class.new
- end
-
describe '#represent_status' do
- subject { serializer.represent_status(resource) }
+ subject { described_class.new.represent_status(cluster) }
+
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
+ let(:provider) { create(:cluster_provider_gcp, :errored) }
+
+ it 'serializes only status' do
+ expect(subject.keys).to contain_exactly(:status, :status_reason, :applications)
+ end
+ end
- context 'when represents only status' do
- let(:resource) { create(:gcp_cluster, :errored) }
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
it 'serializes only status' do
- expect(subject.keys).to contain_exactly(:status, :status_reason)
+ expect(subject.keys).to contain_exactly(:status, :status_reason, :applications)
end
end
end
diff --git a/spec/serializers/container_tag_entity_spec.rb b/spec/serializers/container_tag_entity_spec.rb
index 6dcc5204516..4beb50c70f8 100644
--- a/spec/serializers/container_tag_entity_spec.rb
+++ b/spec/serializers/container_tag_entity_spec.rb
@@ -22,7 +22,7 @@ describe ContainerTagEntity do
end
it 'exposes required informations' do
- expect(subject).to include(:name, :location, :revision, :total_size, :created_at)
+ expect(subject).to include(:name, :location, :revision, :short_revision, :total_size, :created_at)
end
context 'when user can manage repositories' do
diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb
new file mode 100644
index 00000000000..452754d7a79
--- /dev/null
+++ b/spec/serializers/group_child_entity_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+describe GroupChildEntity do
+ include Gitlab::Routing.url_helpers
+
+ let(:user) { create(:user) }
+ let(:request) { double('request') }
+ let(:entity) { described_class.new(object, request: request) }
+ subject(:json) { entity.as_json }
+
+ before do
+ allow(request).to receive(:current_user).and_return(user)
+ end
+
+ shared_examples 'group child json' do
+ it 'renders json' do
+ is_expected.not_to be_nil
+ end
+
+ %w[id
+ full_name
+ avatar_url
+ name
+ description
+ visibility
+ type
+ can_edit
+ visibility
+ permission
+ relative_path].each do |attribute|
+ it "includes #{attribute}" do
+ expect(json[attribute.to_sym]).to be_present
+ end
+ end
+ end
+
+ describe 'for a project' do
+ let(:object) do
+ create(:project, :with_avatar,
+ description: 'Awesomeness')
+ end
+
+ before do
+ object.add_master(user)
+ end
+
+ it 'has the correct type' do
+ expect(json[:type]).to eq('project')
+ end
+
+ it 'includes the star count' do
+ expect(json[:star_count]).to be_present
+ end
+
+ it 'has the correct edit path' do
+ expect(json[:edit_path]).to eq(edit_project_path(object))
+ end
+
+ it_behaves_like 'group child json'
+ end
+
+ describe 'for a group', :nested_groups do
+ let(:object) do
+ create(:group, :nested, :with_avatar,
+ description: 'Awesomeness')
+ end
+
+ before do
+ object.add_owner(user)
+ end
+
+ it 'has the correct type' do
+ expect(json[:type]).to eq('group')
+ end
+
+ it 'counts projects and subgroups as children' do
+ create(:project, namespace: object)
+ create(:group, parent: object)
+
+ expect(json[:children_count]).to eq(2)
+ end
+
+ %w[children_count leave_path parent_id number_projects_with_delimiter number_users_with_delimiter project_count subgroup_count].each do |attribute|
+ it "includes #{attribute}" do
+ expect(json[attribute.to_sym]).to be_present
+ end
+ end
+
+ it 'allows an owner to leave when there is another one' do
+ object.add_owner(create(:user))
+
+ expect(json[:can_leave]).to be_truthy
+ end
+
+ it 'has the correct edit path' do
+ expect(json[:edit_path]).to eq(edit_group_path(object))
+ end
+
+ it_behaves_like 'group child json'
+ end
+end
diff --git a/spec/serializers/group_child_serializer_spec.rb b/spec/serializers/group_child_serializer_spec.rb
new file mode 100644
index 00000000000..5541ada3750
--- /dev/null
+++ b/spec/serializers/group_child_serializer_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe GroupChildSerializer do
+ let(:request) { double('request') }
+ let(:user) { create(:user) }
+ subject(:serializer) { described_class.new(current_user: user) }
+
+ describe '#represent' do
+ context 'for groups' do
+ it 'can render a single group' do
+ expect(serializer.represent(build(:group))).to be_kind_of(Hash)
+ end
+
+ it 'can render a collection of groups' do
+ expect(serializer.represent(build_list(:group, 2))).to be_kind_of(Array)
+ end
+ end
+
+ context 'with a hierarchy', :nested_groups do
+ let(:parent) { create(:group) }
+
+ subject(:serializer) do
+ described_class.new(current_user: user).expand_hierarchy(parent)
+ end
+
+ it 'expands the subgroups' do
+ subgroup = create(:group, parent: parent)
+ subsub_group = create(:group, parent: subgroup)
+
+ json = serializer.represent([subgroup, subsub_group]).first
+ subsub_group_json = json[:children].first
+
+ expect(json[:id]).to eq(subgroup.id)
+ expect(subsub_group_json).not_to be_nil
+ expect(subsub_group_json[:id]).to eq(subsub_group.id)
+ end
+
+ it 'can render a nested tree' do
+ subgroup1 = create(:group, parent: parent)
+ subsub_group1 = create(:group, parent: subgroup1)
+ subgroup2 = create(:group, parent: parent)
+
+ json = serializer.represent([subgroup1, subsub_group1, subgroup1, subgroup2])
+ subgroup1_json = json.first
+ subsub_group1_json = subgroup1_json[:children].first
+
+ expect(json.size).to eq(2)
+ expect(subgroup1_json[:id]).to eq(subgroup1.id)
+ expect(subsub_group1_json[:id]).to eq(subsub_group1.id)
+ end
+
+ context 'without a specified parent' do
+ subject(:serializer) do
+ described_class.new(current_user: user).expand_hierarchy
+ end
+
+ it 'can render a tree' do
+ subgroup = create(:group, parent: parent)
+
+ json = serializer.represent([parent, subgroup])
+ parent_json = json.first
+
+ expect(parent_json[:id]).to eq(parent.id)
+ expect(parent_json[:children].first[:id]).to eq(subgroup.id)
+ end
+ end
+ end
+
+ context 'for projects' do
+ it 'can render a single project' do
+ expect(serializer.represent(build(:project))).to be_kind_of(Hash)
+ end
+
+ it 'can render a collection of projects' do
+ expect(serializer.represent(build_list(:project, 2))).to be_kind_of(Array)
+ end
+
+ context 'with a hierarchy', :nested_groups do
+ let(:parent) { create(:group) }
+
+ subject(:serializer) do
+ described_class.new(current_user: user).expand_hierarchy(parent)
+ end
+
+ it 'can render a nested tree' do
+ subgroup1 = create(:group, parent: parent)
+ project1 = create(:project, namespace: subgroup1)
+ subgroup2 = create(:group, parent: parent)
+ project2 = create(:project, namespace: subgroup2)
+
+ json = serializer.represent([project1, project2, subgroup1, subgroup2])
+ project1_json, project2_json = json.map { |group_json| group_json[:children].first }
+
+ expect(json.size).to eq(2)
+ expect(project1_json[:id]).to eq(project1.id)
+ expect(project2_json[:id]).to eq(project2.id)
+ end
+
+ it 'returns an array when an array of a single instance was given' do
+ project = create(:project, namespace: parent)
+
+ json = serializer.represent([project])
+
+ expect(json).to be_kind_of(Array)
+ expect(json.size).to eq(1)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb
new file mode 100644
index 00000000000..caa3e41402b
--- /dev/null
+++ b/spec/serializers/issue_entity_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe IssueEntity do
+ let(:project) { create(:project) }
+ let(:resource) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+
+ let(:request) { double('request', current_user: user) }
+
+ subject { described_class.new(resource, request: request).as_json }
+
+ it 'has Issuable attributes' do
+ expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id,
+ :title, :updated_by_id, :created_at, :updated_at, :milestone, :labels)
+ end
+
+ it 'has time estimation attributes' do
+ expect(subject).to include(:time_estimate, :total_time_spent, :human_time_estimate, :human_total_time_spent)
+ end
+end
diff --git a/spec/serializers/issue_serializer_spec.rb b/spec/serializers/issue_serializer_spec.rb
new file mode 100644
index 00000000000..75578816e75
--- /dev/null
+++ b/spec/serializers/issue_serializer_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe IssueSerializer do
+ let(:resource) { create(:issue) }
+ let(:user) { create(:user) }
+ let(:json_entity) do
+ described_class.new(current_user: user)
+ .represent(resource, serializer: serializer)
+ .with_indifferent_access
+ end
+
+ context 'non-sidebar issue serialization' do
+ let(:serializer) { nil }
+
+ it 'matches issue json schema' do
+ expect(json_entity).to match_schema('entities/issue')
+ end
+ end
+
+ context 'sidebar issue serialization' do
+ let(:serializer) { 'sidebar' }
+
+ it 'matches sidebar issue json schema' do
+ expect(json_entity).to match_schema('entities/issue_sidebar')
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_basic_serializer_spec.rb b/spec/serializers/merge_request_basic_serializer_spec.rb
index 4daf5a59d0c..1fad8e6bc5d 100644
--- a/spec/serializers/merge_request_basic_serializer_spec.rb
+++ b/spec/serializers/merge_request_basic_serializer_spec.rb
@@ -4,9 +4,13 @@ describe MergeRequestBasicSerializer do
let(:resource) { create(:merge_request) }
let(:user) { create(:user) }
- subject { described_class.new.represent(resource) }
+ let(:json_entity) do
+ described_class.new(current_user: user)
+ .represent(resource, serializer: 'basic')
+ .with_indifferent_access
+ end
- it 'has important MergeRequest attributes' do
- expect(subject).to include(:merge_status)
+ it 'matches basic merge request json' do
+ expect(json_entity).to match_schema('entities/merge_request_basic')
end
end
diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb
index 4aeb593da44..f9285049c0d 100644
--- a/spec/serializers/merge_request_entity_spec.rb
+++ b/spec/serializers/merge_request_entity_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe MergeRequestEntity do
- let(:project) { create :project }
+ let(:project) { create :project, :repository }
let(:resource) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
@@ -30,8 +30,17 @@ describe MergeRequestEntity do
:assign_to_closing)
end
+ it 'has Issuable attributes' do
+ expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id,
+ :title, :updated_by_id, :created_at, :updated_at, :milestone, :labels)
+ end
+
+ it 'has time estimation attributes' do
+ expect(subject).to include(:time_estimate, :total_time_spent, :human_time_estimate, :human_total_time_spent)
+ end
+
it 'has important MergeRequest attributes' do
- expect(subject).to include(:diff_head_sha, :merge_commit_message,
+ expect(subject).to include(:state, :deleted_at, :diff_head_sha, :merge_commit_message,
:has_conflicts, :has_ci, :merge_path,
:conflict_resolution_path,
:cancel_merge_when_pipeline_succeeds_path,
diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb
index 73fbecc153d..e3abefa6d63 100644
--- a/spec/serializers/merge_request_serializer_spec.rb
+++ b/spec/serializers/merge_request_serializer_spec.rb
@@ -9,11 +9,11 @@ describe MergeRequestSerializer do
end
describe '#represent' do
- let(:opts) { { basic: basic } }
- subject { serializer.represent(merge_request, basic: basic) }
+ let(:opts) { { serializer: serializer_entity } }
+ subject { serializer.represent(merge_request, serializer: serializer_entity) }
- context 'when basic param is truthy' do
- let(:basic) { true }
+ context 'when passing basic serializer param' do
+ let(:serializer_entity) { 'basic' }
it 'calls super class #represent with correct params' do
expect_any_instance_of(BaseSerializer).to receive(:represent)
@@ -23,8 +23,8 @@ describe MergeRequestSerializer do
end
end
- context 'when basic param is falsy' do
- let(:basic) { false }
+ context 'when serializer param is falsy' do
+ let(:serializer_entity) { nil }
it 'calls super class #represent with correct params' do
expect_any_instance_of(BaseSerializer).to receive(:represent)
diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb
index f60d1843581..45e18086894 100644
--- a/spec/serializers/pipeline_details_entity_spec.rb
+++ b/spec/serializers/pipeline_details_entity_spec.rb
@@ -107,7 +107,7 @@ describe PipelineDetailsEntity do
it 'contains stages' do
expect(subject).to include(:details)
expect(subject[:details]).to include(:stages)
- expect(subject[:details][:stages].first).to include(name: 'external')
+ expect(subject[:details][:stages].first).to include(name: 'test')
end
end
diff --git a/spec/services/applications/create_service_spec.rb b/spec/services/applications/create_service_spec.rb
new file mode 100644
index 00000000000..47a2a9d6403
--- /dev/null
+++ b/spec/services/applications/create_service_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe ::Applications::CreateService do
+ let(:user) { create(:user) }
+ let(:params) { attributes_for(:application) }
+ let(:request) { ActionController::TestRequest.new(remote_ip: '127.0.0.1') }
+
+ subject { described_class.new(user, params) }
+
+ it 'creates an application' do
+ expect { subject.execute(request) }.to change { Doorkeeper::Application.count }.by(1)
+ end
+end
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 1c2d0b3e0dc..9128280eb5a 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -43,6 +43,21 @@ describe Auth::ContainerRegistryAuthenticationService do
end
end
+ shared_examples 'a browsable' do
+ let(:access) do
+ [{ 'type' => 'registry',
+ 'name' => 'catalog',
+ 'actions' => ['*'] }]
+ end
+
+ it_behaves_like 'a valid token'
+ it_behaves_like 'not a container repository factory'
+
+ it 'has the correct scope' do
+ expect(payload).to include('access' => access)
+ end
+ end
+
shared_examples 'an accessible' do
let(:access) do
[{ 'type' => 'repository',
@@ -51,7 +66,10 @@ describe Auth::ContainerRegistryAuthenticationService do
end
it_behaves_like 'a valid token'
- it { expect(payload).to include('access' => access) }
+
+ it 'has the correct scope' do
+ expect(payload).to include('access' => access)
+ end
end
shared_examples 'an inaccessible' do
@@ -117,6 +135,17 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'user authorization' do
let(:current_user) { create(:user) }
+ context 'for registry catalog' do
+ let(:current_params) do
+ { scope: "registry:catalog:*" }
+ end
+
+ context 'disallow browsing for users without Gitlab admin rights' do
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
context 'for private project' do
let(:project) { create(:project) }
@@ -490,6 +519,16 @@ describe Auth::ContainerRegistryAuthenticationService do
end
end
+ context 'registry catalog browsing authorized as admin' do
+ let(:current_user) { create(:user, :admin) }
+
+ let(:current_params) do
+ { scope: "registry:catalog:*" }
+ end
+
+ it_behaves_like 'a browsable'
+ end
+
context 'unauthorized' do
context 'disallow to use scope-less authentication' do
it_behaves_like 'a forbidden'
@@ -536,5 +575,14 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'not a container repository factory'
end
end
+
+ context 'for registry catalog' do
+ let(:current_params) do
+ { scope: "registry:catalog:*" }
+ end
+
+ it_behaves_like 'a forbidden'
+ it_behaves_like 'not a container repository factory'
+ end
end
end
diff --git a/spec/services/base_count_service_spec.rb b/spec/services/base_count_service_spec.rb
new file mode 100644
index 00000000000..5ec8ed0976d
--- /dev/null
+++ b/spec/services/base_count_service_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe BaseCountService, :use_clean_rails_memory_store_caching do
+ let(:service) { described_class.new }
+
+ describe '#relation_for_count' do
+ it 'raises NotImplementedError' do
+ expect { service.relation_for_count }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#count' do
+ it 'returns the number of values' do
+ expect(service)
+ .to receive(:cache_key)
+ .and_return('foo')
+
+ expect(service)
+ .to receive(:uncached_count)
+ .and_return(5)
+
+ expect(service.count).to eq(5)
+ end
+ end
+
+ describe '#uncached_count' do
+ it 'returns the uncached number of values' do
+ expect(service)
+ .to receive(:relation_for_count)
+ .and_return(double(:relation, count: 5))
+
+ expect(service.uncached_count).to eq(5)
+ end
+ end
+
+ describe '#refresh_cache' do
+ it 'refreshes the cache' do
+ allow(service)
+ .to receive(:cache_key)
+ .and_return('foo')
+
+ allow(service)
+ .to receive(:uncached_count)
+ .and_return(4)
+
+ service.refresh_cache
+
+ expect(Rails.cache.fetch(service.cache_key, raw: service.raw?)).to eq(4)
+ end
+ end
+
+ describe '#delete_cache' do
+ it 'deletes the cache' do
+ allow(service)
+ .to receive(:cache_key)
+ .and_return('foo')
+
+ allow(service)
+ .to receive(:uncached_count)
+ .and_return(4)
+
+ service.refresh_cache
+ service.delete_cache
+
+ expect(Rails.cache.fetch(service.cache_key, raw: service.raw?)).to be_nil
+ end
+ end
+
+ describe '#raw?' do
+ it 'returns false' do
+ expect(service.raw?).to eq(false)
+ end
+ end
+
+ describe '#cache_key' do
+ it 'raises NotImplementedError' do
+ expect { service.cache_key }.to raise_error(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/services/ci/create_cluster_service_spec.rb b/spec/services/ci/create_cluster_service_spec.rb
deleted file mode 100644
index 6e7398fbffa..00000000000
--- a/spec/services/ci/create_cluster_service_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require 'spec_helper'
-
-describe Ci::CreateClusterService do
- describe '#execute' do
- let(:access_token) { 'xxx' }
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:result) { described_class.new(project, user, params).execute(access_token) }
-
- context 'when correct params' do
- let(:params) do
- {
- gcp_project_id: 'gcp-project',
- gcp_cluster_name: 'test-cluster',
- gcp_cluster_zone: 'us-central1-a',
- gcp_cluster_size: 1
- }
- end
-
- it 'creates a cluster object' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
- expect { result }.to change { Gcp::Cluster.count }.by(1)
- expect(result.gcp_project_id).to eq('gcp-project')
- expect(result.gcp_cluster_name).to eq('test-cluster')
- expect(result.gcp_cluster_zone).to eq('us-central1-a')
- expect(result.gcp_cluster_size).to eq(1)
- expect(result.gcp_token).to eq(access_token)
- end
- end
-
- context 'when invalid params' do
- let(:params) do
- {
- gcp_project_id: 'gcp-project',
- gcp_cluster_name: 'test-cluster',
- gcp_cluster_zone: 'us-central1-a',
- gcp_cluster_size: 'ABC'
- }
- end
-
- it 'returns an error' do
- expect(ClusterProvisionWorker).not_to receive(:perform_async)
- expect { result }.to change { Gcp::Cluster.count }.by(0)
- end
- end
- end
-end
diff --git a/spec/services/ci/fetch_gcp_operation_service_spec.rb b/spec/services/ci/fetch_gcp_operation_service_spec.rb
deleted file mode 100644
index 7792979c5cb..00000000000
--- a/spec/services/ci/fetch_gcp_operation_service_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-require 'spec_helper'
-require 'google/apis'
-
-describe Ci::FetchGcpOperationService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster) }
- let(:operation) { double }
-
- context 'when suceeded' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_operations).and_return(operation)
- end
-
- it 'fetch the gcp operaion' do
- expect { |b| described_class.new.execute(cluster, &b) }
- .to yield_with_args(operation)
- end
- end
-
- context 'when raises an error' do
- let(:error) { Google::Apis::ServerError.new('a') }
-
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_operations).and_raise(error)
- end
-
- it 'sets an error to cluster object' do
- expect { |b| described_class.new.execute(cluster, &b) }
- .not_to yield_with_args
- expect(cluster.reload).to be_errored
- end
- end
- end
-end
diff --git a/spec/services/ci/finalize_cluster_creation_service_spec.rb b/spec/services/ci/finalize_cluster_creation_service_spec.rb
deleted file mode 100644
index def3709fdb4..00000000000
--- a/spec/services/ci/finalize_cluster_creation_service_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-require 'spec_helper'
-
-describe Ci::FinalizeClusterCreationService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster) }
- let(:result) { described_class.new.execute(cluster) }
-
- context 'when suceeded to get cluster from api' do
- let(:gke_cluster) { double }
-
- before do
- allow(gke_cluster).to receive(:endpoint).and_return('111.111.111.111')
- allow(gke_cluster).to receive(:master_auth).and_return(spy)
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_get).and_return(gke_cluster)
- end
-
- context 'when suceeded to get kubernetes token' do
- let(:kubernetes_token) { 'abc' }
-
- before do
- allow_any_instance_of(Ci::FetchKubernetesTokenService)
- .to receive(:execute).and_return(kubernetes_token)
- end
-
- it 'executes integration cluster' do
- expect_any_instance_of(Ci::IntegrateClusterService).to receive(:execute)
- described_class.new.execute(cluster)
- end
- end
-
- context 'when failed to get kubernetes token' do
- before do
- allow_any_instance_of(Ci::FetchKubernetesTokenService)
- .to receive(:execute).and_return(nil)
- end
-
- it 'sets an error to cluster object' do
- described_class.new.execute(cluster)
-
- expect(cluster.reload).to be_errored
- end
- end
- end
-
- context 'when failed to get cluster from api' do
- let(:error) { Google::Apis::ServerError.new('a') }
-
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_get).and_raise(error)
- end
-
- it 'sets an error to cluster object' do
- described_class.new.execute(cluster)
-
- expect(cluster.reload).to be_errored
- end
- end
- end
-end
diff --git a/spec/services/ci/integrate_cluster_service_spec.rb b/spec/services/ci/integrate_cluster_service_spec.rb
deleted file mode 100644
index 3a79c205bd1..00000000000
--- a/spec/services/ci/integrate_cluster_service_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-require 'spec_helper'
-
-describe Ci::IntegrateClusterService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster, :custom_project_namespace) }
- let(:endpoint) { '123.123.123.123' }
- let(:ca_cert) { 'ca_cert_xxx' }
- let(:token) { 'token_xxx' }
- let(:username) { 'username_xxx' }
- let(:password) { 'password_xxx' }
-
- before do
- described_class
- .new.execute(cluster, endpoint, ca_cert, token, username, password)
-
- cluster.reload
- end
-
- context 'when correct params' do
- it 'creates a cluster object' do
- expect(cluster.endpoint).to eq(endpoint)
- expect(cluster.ca_cert).to eq(ca_cert)
- expect(cluster.kubernetes_token).to eq(token)
- expect(cluster.username).to eq(username)
- expect(cluster.password).to eq(password)
- expect(cluster.service.active).to be_truthy
- expect(cluster.service.api_url).to eq(cluster.api_url)
- expect(cluster.service.ca_pem).to eq(ca_cert)
- expect(cluster.service.namespace).to eq(cluster.project_namespace)
- expect(cluster.service.token).to eq(token)
- end
- end
-
- context 'when invalid params' do
- let(:endpoint) { nil }
-
- it 'sets an error to cluster object' do
- expect(cluster).to be_errored
- end
- end
- end
-end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 214adc9960f..0ce41e7c7ee 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -292,6 +292,30 @@ describe Ci::ProcessPipelineService, '#execute' do
end
end
+ context 'when there is only one manual action' do
+ before do
+ create_build('deploy', stage_idx: 0, when: 'manual', allow_failure: true)
+
+ process_pipeline
+ end
+
+ it 'skips the pipeline' do
+ expect(pipeline.reload).to be_skipped
+ end
+
+ context 'when the action was played' do
+ before do
+ play_manual_action('deploy')
+ end
+
+ it 'queues the action and pipeline' do
+ expect(all_builds_statuses).to eq(%w[pending])
+
+ expect(pipeline.reload).to be_pending
+ end
+ end
+ end
+
context 'when blocking manual actions are defined' do
before do
create_build('code:test', stage_idx: 0)
diff --git a/spec/services/ci/provision_cluster_service_spec.rb b/spec/services/ci/provision_cluster_service_spec.rb
deleted file mode 100644
index 5ce5c788314..00000000000
--- a/spec/services/ci/provision_cluster_service_spec.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-require 'spec_helper'
-
-describe Ci::ProvisionClusterService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster) }
- let(:operation) { spy }
-
- shared_examples 'error' do
- it 'sets an error to cluster object' do
- described_class.new.execute(cluster)
-
- expect(cluster.reload).to be_errored
- end
- end
-
- context 'when suceeded to request provision' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_create).and_return(operation)
- end
-
- context 'when operation status is RUNNING' do
- before do
- allow(operation).to receive(:status).and_return('RUNNING')
- end
-
- context 'when suceeded to parse gcp operation id' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:parse_operation_id).and_return('operation-123')
- end
-
- context 'when cluster status is scheduled' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:parse_operation_id).and_return('operation-123')
- end
-
- it 'schedules a worker for status minitoring' do
- expect(WaitForClusterCreationWorker).to receive(:perform_in)
-
- described_class.new.execute(cluster)
- end
- end
-
- context 'when cluster status is creating' do
- before do
- cluster.make_creating!
- end
-
- it_behaves_like 'error'
- end
- end
-
- context 'when failed to parse gcp operation id' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:parse_operation_id).and_return(nil)
- end
-
- it_behaves_like 'error'
- end
- end
-
- context 'when operation status is others' do
- before do
- allow(operation).to receive(:status).and_return('others')
- end
-
- it_behaves_like 'error'
- end
- end
-
- context 'when failed to request provision' do
- let(:error) { Google::Apis::ServerError.new('a') }
-
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_create).and_raise(error)
- end
-
- it_behaves_like 'error'
- end
- end
-end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 9db3568abee..b61d1cb765e 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -160,8 +160,9 @@ describe Ci::RetryBuildService do
expect(new_build).to be_created
end
- it 'does mark old build as retried' do
+ it 'does mark old build as retried in the database and on the instance' do
expect(new_build).to be_latest
+ expect(build).to be_retried
expect(build.reload).to be_retried
end
end
diff --git a/spec/services/ci/update_cluster_service_spec.rb b/spec/services/ci/update_cluster_service_spec.rb
deleted file mode 100644
index a289385b88f..00000000000
--- a/spec/services/ci/update_cluster_service_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'spec_helper'
-
-describe Ci::UpdateClusterService do
- describe '#execute' do
- let(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service) }
-
- before do
- described_class.new(cluster.project, cluster.user, params).execute(cluster)
-
- cluster.reload
- end
-
- context 'when correct params' do
- context 'when enabled is true' do
- let(:params) { { 'enabled' => 'true' } }
-
- it 'enables cluster and overwrite kubernetes service' do
- expect(cluster.enabled).to be_truthy
- expect(cluster.service.active).to be_truthy
- expect(cluster.service.api_url).to eq(cluster.api_url)
- expect(cluster.service.ca_pem).to eq(cluster.ca_cert)
- expect(cluster.service.namespace).to eq(cluster.project_namespace)
- expect(cluster.service.token).to eq(cluster.kubernetes_token)
- end
- end
-
- context 'when enabled is false' do
- let(:params) { { 'enabled' => 'false' } }
-
- it 'disables cluster and kubernetes service' do
- expect(cluster.enabled).to be_falsy
- expect(cluster.service.active).to be_falsy
- end
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
new file mode 100644
index 00000000000..75fc05d36e9
--- /dev/null
+++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
@@ -0,0 +1,91 @@
+require 'spec_helper'
+
+describe Clusters::Applications::CheckInstallationProgressService do
+ RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
+
+ let(:application) { create(:cluster_applications_helm, :installing) }
+ let(:service) { described_class.new(application) }
+ let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN }
+ let(:errors) { nil }
+
+ shared_examples 'a terminated installation' do
+ it 'removes the installation POD' do
+ expect(service).to receive(:remove_installation_pod).once
+
+ service.execute
+ end
+ end
+
+ shared_examples 'a not yet terminated installation' do |a_phase|
+ let(:phase) { a_phase }
+
+ context "when phase is #{a_phase}" do
+ context 'when not timeouted' do
+ it 'reschedule a new check' do
+ expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
+ expect(service).not_to receive(:remove_installation_pod)
+
+ service.execute
+
+ expect(application).to be_installing
+ expect(application.status_reason).to be_nil
+ end
+ end
+
+ context 'when timeouted' do
+ let(:application) { create(:cluster_applications_helm, :timeouted) }
+
+ it_behaves_like 'a terminated installation'
+
+ it 'make the application errored' do
+ expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
+
+ service.execute
+
+ expect(application).to be_errored
+ expect(application.status_reason).to match(/\btimeouted\b/)
+ end
+ end
+ end
+ end
+
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+
+ allow(service).to receive(:installation_errors).and_return(errors)
+ allow(service).to receive(:remove_installation_pod).and_return(nil)
+ end
+
+ describe '#execute' do
+ context 'when installation POD succeeded' do
+ let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
+
+ it_behaves_like 'a terminated installation'
+
+ it 'make the application installed' do
+ expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
+
+ service.execute
+
+ expect(application).to be_installed
+ expect(application.status_reason).to be_nil
+ end
+ end
+
+ context 'when installation POD failed' do
+ let(:phase) { Gitlab::Kubernetes::Pod::FAILED }
+ let(:errors) { 'test installation failed' }
+
+ it_behaves_like 'a terminated installation'
+
+ it 'make the application errored' do
+ service.execute
+
+ expect(application).to be_errored
+ expect(application.status_reason).to eq(errors)
+ end
+ end
+
+ RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
+ end
+end
diff --git a/spec/services/clusters/applications/install_service_spec.rb b/spec/services/clusters/applications/install_service_spec.rb
new file mode 100644
index 00000000000..054a49ffedf
--- /dev/null
+++ b/spec/services/clusters/applications/install_service_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe Clusters::Applications::InstallService do
+ describe '#execute' do
+ let(:application) { create(:cluster_applications_helm, :scheduled) }
+ let(:service) { described_class.new(application) }
+ let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm) }
+
+ before do
+ allow(service).to receive(:helm_api).and_return(helm_client)
+ end
+
+ context 'when there are no errors' do
+ before do
+ expect(helm_client).to receive(:install).with(application.install_command)
+ allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil)
+ end
+
+ it 'make the application installing' do
+ expect(application.cluster).not_to be_nil
+ service.execute
+
+ expect(application).to be_installing
+ end
+
+ it 'schedule async installation status check' do
+ expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
+
+ service.execute
+ end
+ end
+
+ context 'when k8s cluster communication fails' do
+ before do
+ error = KubeException.new(500, 'system failure', nil)
+ expect(helm_client).to receive(:install).with(application.install_command).and_raise(error)
+ end
+
+ it 'make the application errored' do
+ service.execute
+
+ expect(application).to be_errored
+ expect(application.status_reason).to match(/kubernetes error:/i)
+ end
+ end
+
+ context 'when application cannot be persisted' do
+ let(:application) { build(:cluster_applications_helm, :scheduled) }
+
+ it 'make the application errored' do
+ expect(application).to receive(:make_installing!).once.and_raise(ActiveRecord::RecordInvalid)
+ expect(helm_client).not_to receive(:install)
+
+ service.execute
+
+ expect(application).to be_errored
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/applications/schedule_installation_service_spec.rb b/spec/services/clusters/applications/schedule_installation_service_spec.rb
new file mode 100644
index 00000000000..cf95361c935
--- /dev/null
+++ b/spec/services/clusters/applications/schedule_installation_service_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Clusters::Applications::ScheduleInstallationService do
+ def count_scheduled
+ application_class&.with_status(:scheduled)&.count || 0
+ end
+
+ shared_examples 'a failing service' do
+ it 'raise an exception' do
+ expect(ClusterInstallAppWorker).not_to receive(:perform_async)
+ count_before = count_scheduled
+
+ expect { service.execute }.to raise_error(StandardError)
+ expect(count_scheduled).to eq(count_before)
+ end
+ end
+
+ describe '#execute' do
+ let(:application_class) { Clusters::Applications::Helm }
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let(:service) { described_class.new(project, nil, cluster: cluster, application_class: application_class) }
+
+ it 'creates a new application' do
+ expect { service.execute }.to change { application_class.count }.by(1)
+ end
+
+ it 'make the application scheduled' do
+ expect(ClusterInstallAppWorker).to receive(:perform_async).with(application_class.application_name, kind_of(Numeric)).once
+
+ expect { service.execute }.to change { application_class.with_status(:scheduled).count }.by(1)
+ end
+
+ context 'when installation is already in progress' do
+ let(:application) { create(:cluster_applications_helm, :installing) }
+ let(:cluster) { application.cluster }
+
+ it_behaves_like 'a failing service'
+ end
+
+ context 'when application_class is nil' do
+ let(:application_class) { nil }
+
+ it_behaves_like 'a failing service'
+ end
+
+ context 'when application cannot be persisted' do
+ before do
+ expect_any_instance_of(application_class).to receive(:make_scheduled!).once.and_raise(ActiveRecord::RecordInvalid)
+ end
+
+ it_behaves_like 'a failing service'
+ end
+ end
+end
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
new file mode 100644
index 00000000000..5b6edb73beb
--- /dev/null
+++ b/spec/services/clusters/create_service_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Clusters::CreateService do
+ let(:access_token) { 'xxx' }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:result) { described_class.new(project, user, params).execute(access_token) }
+
+ context 'when provider is gcp' do
+ context 'when correct params' do
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: 'gcp-project',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a'
+ }
+ }
+ end
+
+ it 'creates a cluster object and performs a worker' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { result }
+ .to change { Clusters::Cluster.count }.by(1)
+ .and change { Clusters::Providers::Gcp.count }.by(1)
+
+ expect(result.name).to eq('test-cluster')
+ expect(result.user).to eq(user)
+ expect(result.project).to eq(project)
+ expect(result.provider.gcp_project_id).to eq('gcp-project')
+ expect(result.provider.zone).to eq('us-central1-a')
+ expect(result.provider.num_nodes).to eq(1)
+ expect(result.provider.machine_type).to eq('machine_type-a')
+ expect(result.provider.access_token).to eq(access_token)
+ expect(result.platform).to be_nil
+ end
+ end
+
+ context 'when invalid params' do
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: '!!!!!!!',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a'
+ }
+ }
+ end
+
+ it 'returns an error' do
+ expect(ClusterProvisionWorker).not_to receive(:perform_async)
+ expect { result }.to change { Clusters::Cluster.count }.by(0)
+ expect(result.errors[:"provider_gcp.gcp_project_id"]).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/fetch_operation_service_spec.rb b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
new file mode 100644
index 00000000000..e2fa93904c5
--- /dev/null
+++ b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::FetchOperationService do
+ include GoogleApi::CloudPlatformHelpers
+
+ describe '#execute' do
+ let(:provider) { create(:cluster_provider_gcp, :creating) }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+ let(:operation_id) { provider.operation_id }
+
+ shared_examples 'success' do
+ it 'yields' do
+ expect { |b| described_class.new.execute(provider, &b) }
+ .to yield_with_args
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ expect { |b| described_class.new.execute(provider, &b) }
+ .not_to yield_with_args
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to fetch operation' do
+ before do
+ stub_cloud_platform_get_zone_operation(gcp_project_id, zone, operation_id)
+ end
+
+ it_behaves_like 'success'
+ end
+
+ context 'when Internal Server Error happened' do
+ before do
+ stub_cloud_platform_get_zone_operation_error(gcp_project_id, zone, operation_id)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
new file mode 100644
index 00000000000..0cf91307589
--- /dev/null
+++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
@@ -0,0 +1,111 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::FinalizeCreationService do
+ include GoogleApi::CloudPlatformHelpers
+ include KubernetesHelpers
+
+ describe '#execute' do
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ let(:provider) { cluster.provider }
+ let(:platform) { cluster.platform }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+ let(:cluster_name) { cluster.name }
+
+ shared_examples 'success' do
+ it 'configures provider and kubernetes' do
+ described_class.new.execute(provider)
+
+ expect(provider).to be_created
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to fetch gke cluster info' do
+ let(:endpoint) { '111.111.111.111' }
+ let(:api_url) { 'https://' + endpoint }
+ let(:username) { 'sample-username' }
+ let(:password) { 'sample-password' }
+
+ before do
+ stub_cloud_platform_get_zone_cluster(
+ gcp_project_id, zone, cluster_name,
+ {
+ endpoint: endpoint,
+ username: username,
+ password: password
+ }
+ )
+
+ stub_kubeclient_discover(api_url)
+ end
+
+ context 'when suceeded to fetch kuberenetes token' do
+ let(:token) { 'sample-token' }
+
+ before do
+ stub_kubeclient_get_secrets(
+ api_url,
+ {
+ token: Base64.encode64(token)
+ } )
+ end
+
+ it_behaves_like 'success'
+
+ it 'has corresponded data' do
+ described_class.new.execute(provider)
+ cluster.reload
+ provider.reload
+ platform.reload
+
+ expect(provider.endpoint).to eq(endpoint)
+ expect(platform.api_url).to eq(api_url)
+ expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert))
+ expect(platform.username).to eq(username)
+ expect(platform.password).to eq(password)
+ expect(platform.token).to eq(token)
+ end
+ end
+
+ context 'when default-token is not found' do
+ before do
+ stub_kubeclient_get_secrets(api_url, metadata_name: 'aaaa')
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when token is empty' do
+ before do
+ stub_kubeclient_get_secrets(api_url, token: '')
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when failed to fetch kuberenetes token' do
+ before do
+ stub_kubeclient_get_secrets_error(api_url)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when failed to fetch gke cluster info' do
+ before do
+ stub_cloud_platform_get_zone_cluster_error(gcp_project_id, zone, cluster_name)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/provision_service_spec.rb b/spec/services/clusters/gcp/provision_service_spec.rb
new file mode 100644
index 00000000000..f48afdc83b2
--- /dev/null
+++ b/spec/services/clusters/gcp/provision_service_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::ProvisionService do
+ include GoogleApi::CloudPlatformHelpers
+
+ describe '#execute' do
+ let(:provider) { create(:cluster_provider_gcp, :scheduled) }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+
+ shared_examples 'success' do
+ it 'schedules a worker for status minitoring' do
+ expect(WaitForClusterCreationWorker).to receive(:perform_in)
+
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_creating
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to request provision' do
+ before do
+ stub_cloud_platform_create_cluster(gcp_project_id, zone)
+ end
+
+ it_behaves_like 'success'
+ end
+
+ context 'when operation status is unexpected' do
+ before do
+ stub_cloud_platform_create_cluster(
+ gcp_project_id, zone,
+ {
+ "status": 'unexpected'
+ } )
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when selfLink is unexpected' do
+ before do
+ stub_cloud_platform_create_cluster(
+ gcp_project_id, zone,
+ {
+ "selfLink": 'unexpected'
+ })
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when Internal Server Error happened' do
+ before do
+ stub_cloud_platform_create_cluster_error(gcp_project_id, zone)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/verify_provision_status_service_spec.rb b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
new file mode 100644
index 00000000000..2ee2fa51f63
--- /dev/null
+++ b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe Clusters::Gcp::VerifyProvisionStatusService do
+ include GoogleApi::CloudPlatformHelpers
+
+ describe '#execute' do
+ let(:provider) { create(:cluster_provider_gcp, :creating) }
+ let(:gcp_project_id) { provider.gcp_project_id }
+ let(:zone) { provider.zone }
+ let(:operation_id) { provider.operation_id }
+
+ shared_examples 'continue_creation' do
+ it 'schedules a worker for status minitoring' do
+ expect(WaitForClusterCreationWorker).to receive(:perform_in)
+
+ described_class.new.execute(provider)
+ end
+ end
+
+ shared_examples 'finalize_creation' do
+ it 'schedules a worker for status minitoring' do
+ expect_any_instance_of(Clusters::Gcp::FinalizeCreationService).to receive(:execute)
+
+ described_class.new.execute(provider)
+ end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ described_class.new.execute(provider)
+
+ expect(provider.reload).to be_errored
+ end
+ end
+
+ context 'when operation status is RUNNING' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'RUNNING',
+ "startTime": 1.minute.ago.strftime("%FT%TZ")
+ } )
+ end
+
+ it_behaves_like 'continue_creation'
+
+ context 'when cluster creation time exceeds timeout' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'RUNNING',
+ "startTime": 30.minutes.ago.strftime("%FT%TZ")
+ } )
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when operation status is PENDING' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'PENDING',
+ "startTime": 1.minute.ago.strftime("%FT%TZ")
+ } )
+ end
+
+ it_behaves_like 'continue_creation'
+ end
+
+ context 'when operation status is DONE' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'DONE'
+ } )
+ end
+
+ it_behaves_like 'finalize_creation'
+ end
+
+ context 'when operation status is unexpected' do
+ before do
+ stub_cloud_platform_get_zone_operation(
+ gcp_project_id, zone, operation_id,
+ {
+ "status": 'unexpected'
+ } )
+ end
+
+ it_behaves_like 'error'
+ end
+
+ context 'when failed to get operation status' do
+ before do
+ stub_cloud_platform_get_zone_operation_error(gcp_project_id, zone, operation_id)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb
new file mode 100644
index 00000000000..2d91a21035d
--- /dev/null
+++ b/spec/services/clusters/update_service_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Clusters::UpdateService do
+ describe '#execute' do
+ subject { described_class.new(cluster.project, cluster.user, params).execute(cluster) }
+
+ let(:cluster) { create(:cluster, :project, :provided_by_user) }
+
+ context 'when correct params' do
+ context 'when enabled is true' do
+ let(:params) { { enabled: true } }
+
+ it 'enables cluster' do
+ is_expected.to eq(true)
+ expect(cluster.enabled).to be_truthy
+ end
+ end
+
+ context 'when enabled is false' do
+ let(:params) { { enabled: false } }
+
+ it 'disables cluster' do
+ is_expected.to eq(true)
+ expect(cluster.enabled).to be_falsy
+ end
+ end
+
+ context 'when namespace is specified' do
+ let(:params) do
+ {
+ platform_kubernetes_attributes: {
+ namespace: 'custom-namespace'
+ }
+ }
+ end
+
+ it 'updates namespace' do
+ is_expected.to eq(true)
+ expect(cluster.platform.namespace).to eq('custom-namespace')
+ end
+ end
+ end
+
+ context 'when invalid params' do
+ let(:params) do
+ {
+ platform_kubernetes_attributes: {
+ namespace: '!!!'
+ }
+ }
+ end
+
+ it 'returns false' do
+ is_expected.to eq(false)
+ expect(cluster.errors[:"platform_kubernetes.namespace"]).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb
index 5a9eb359ee1..0de02576203 100644
--- a/spec/services/delete_merged_branches_service_spec.rb
+++ b/spec/services/delete_merged_branches_service_spec.rb
@@ -42,6 +42,14 @@ describe DeleteMergedBranchesService do
expect(project.repository.branch_names).to include('improve/awesome')
end
+ it 'ignores protected tags' do
+ create(:protected_tag, project: project, name: 'improve/*')
+
+ service.execute
+
+ expect(project.repository.branch_names).not_to include('improve/awesome')
+ end
+
context 'user without rights' do
let(:user) { create(:user) }
diff --git a/spec/services/events/render_service_spec.rb b/spec/services/events/render_service_spec.rb
new file mode 100644
index 00000000000..b4a4a44d07b
--- /dev/null
+++ b/spec/services/events/render_service_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Events::RenderService do
+ describe '#execute' do
+ let!(:note) { build(:note) }
+ let!(:event) { build(:event, target: note, project: note.project) }
+ let!(:user) { build(:user) }
+
+ context 'when the request format is atom' do
+ it 'renders the note inside events' do
+ expect(Banzai::ObjectRenderer).to receive(:new)
+ .with(event.project, user,
+ only_path: false,
+ xhtml: true)
+ .and_call_original
+
+ expect_any_instance_of(Banzai::ObjectRenderer)
+ .to receive(:render).with([note], :note)
+
+ described_class.new(user).execute([event], atom_request: true)
+ end
+ end
+
+ context 'when the request format is not atom' do
+ it 'renders the note inside events' do
+ expect(Banzai::ObjectRenderer).to receive(:new)
+ .with(event.project, user, {})
+ .and_call_original
+
+ expect_any_instance_of(Banzai::ObjectRenderer)
+ .to receive(:render).with([note], :note)
+
+ described_class.new(user).execute([event], atom_request: false)
+ end
+ end
+ end
+end
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
new file mode 100644
index 00000000000..b8fa3e3d124
--- /dev/null
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe Issuable::CommonSystemNotesService do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:issuable) { create(:issue) }
+
+ shared_examples 'system note creation' do |update_params, note_text|
+ subject { described_class.new(project, user).execute(issuable, [])}
+
+ before do
+ issuable.assign_attributes(update_params)
+ issuable.save
+ end
+
+ it 'creates 1 system note with the correct content' do
+ expect { subject }.to change { Note.count }.from(0).to(1)
+
+ note = Note.last
+ expect(note.note).to match(note_text)
+ expect(note.noteable_type).to eq(issuable.class.name)
+ end
+ end
+
+ shared_examples 'WIP notes creation' do |wip_action|
+ subject { described_class.new(project, user).execute(issuable, []) }
+
+ it 'creates WIP toggle and title change notes' do
+ expect { subject }.to change { Note.count }.from(0).to(2)
+
+ expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**")
+ expect(Note.second.note).to match('changed title')
+ end
+ end
+
+ describe '#execute' do
+ it_behaves_like 'system note creation', { title: 'New title' }, 'changed title'
+ it_behaves_like 'system note creation', { description: 'New description' }, 'changed the description'
+ it_behaves_like 'system note creation', { discussion_locked: true }, 'locked this issue'
+ it_behaves_like 'system note creation', { time_estimate: 5 }, 'changed time estimate'
+
+ context 'when new label is added' do
+ before do
+ label = create(:label, project: project)
+ issuable.labels << label
+ end
+
+ it_behaves_like 'system note creation', {}, /added ~\w+ label/
+ end
+
+ context 'when new milestone is assigned' do
+ before do
+ milestone = create(:milestone, project: project)
+ issuable.milestone_id = milestone.id
+ end
+
+ it_behaves_like 'system note creation', {}, 'changed milestone'
+ end
+
+ context 'with merge requests WIP note' do
+ context 'adding WIP note' do
+ let(:issuable) { create(:merge_request, title: "merge request") }
+
+ it_behaves_like 'system note creation', { title: "WIP merge request" }, 'marked as a **Work In Progress**'
+
+ context 'and changing title' do
+ before do
+ issuable.update_attribute(:title, "WIP changed title")
+ end
+
+ it_behaves_like 'WIP notes creation', 'marked'
+ end
+ end
+
+ context 'removing WIP note' do
+ let(:issuable) { create(:merge_request, title: "WIP merge request") }
+
+ it_behaves_like 'system note creation', { title: "merge request" }, 'unmarked as a **Work In Progress**'
+
+ context 'and changing title' do
+ before do
+ issuable.update_attribute(:title, "changed title")
+ end
+
+ it_behaves_like 'WIP notes creation', 'unmarked'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb
new file mode 100644
index 00000000000..d74d98c6079
--- /dev/null
+++ b/spec/services/issuable/destroy_service_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Issuable::DestroyService do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ subject(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ context 'when issuable is an issue' do
+ let!(:issue) { create(:issue, project: project, author: user) }
+
+ it 'destroys the issue' do
+ expect { service.execute(issue) }.to change { project.issues.count }.by(-1)
+ end
+
+ it 'updates open issues count cache' do
+ expect_any_instance_of(Projects::OpenIssuesCountService).to receive(:refresh_cache)
+
+ service.execute(issue)
+ end
+ end
+
+ context 'when issuable is a merge request' do
+ let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user) }
+
+ it 'destroys the merge request' do
+ expect { service.execute(merge_request) }.to change { project.merge_requests.count }.by(-1)
+ end
+
+ it 'updates open merge requests count cache' do
+ expect_any_instance_of(Projects::OpenMergeRequestsCountService).to receive(:refresh_cache)
+
+ service.execute(merge_request)
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index bdbe0a353fb..f07b81e842a 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -267,6 +267,30 @@ describe Issues::UpdateService, :mailer do
end
end
+ context 'when a new assignee added' do
+ subject { update_issue(assignees: issue.assignees + [user2]) }
+
+ it 'creates only 1 new todo' do
+ expect { subject }.to change { Todo.count }.by(1)
+ end
+
+ it 'creates a todo for new assignee' do
+ subject
+
+ attributes = {
+ project: project,
+ author: user,
+ user: user2,
+ target_id: issue.id,
+ target_type: issue.class.name,
+ action: Todo::ASSIGNED,
+ state: :pending
+ }
+
+ expect(Todo.where(attributes).count).to eq(1)
+ end
+ end
+
context 'when the milestone change' do
it 'marks todos as done' do
update_issue(milestone: create(:milestone))
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index b46c419de14..fee293760f5 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -29,13 +29,27 @@ describe MergeRequests::BuildService do
before do
project.team << [user, :guest]
+ end
+ def stub_compare
allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare)
allow(project).to receive(:commit).and_return(commit_1)
allow(project).to receive(:commit).and_return(commit_2)
end
- describe 'execute' do
+ describe '#execute' do
+ it 'calls the compare service with the correct arguments' do
+ expect(CompareService).to receive(:new)
+ .with(project, Gitlab::Git::BRANCH_REF_PREFIX + source_branch)
+ .and_call_original
+
+ expect_any_instance_of(CompareService).to receive(:execute)
+ .with(project, Gitlab::Git::BRANCH_REF_PREFIX + target_branch)
+ .and_call_original
+
+ merge_request
+ end
+
context 'missing source branch' do
let(:source_branch) { '' }
@@ -52,6 +66,10 @@ describe MergeRequests::BuildService do
let(:target_branch) { nil }
let(:commits) { Commit.decorate([commit_1], project) }
+ before do
+ stub_compare
+ end
+
it 'creates compare object with target branch as default branch' do
expect(merge_request.compare).to be_present
expect(merge_request.target_branch).to eq(project.default_branch)
@@ -77,6 +95,10 @@ describe MergeRequests::BuildService do
context 'no commits in the diff' do
let(:commits) { [] }
+ before do
+ stub_compare
+ end
+
it 'allows the merge request to be created' do
expect(merge_request.can_be_created).to eq(true)
end
@@ -89,6 +111,10 @@ describe MergeRequests::BuildService do
context 'one commit in the diff' do
let(:commits) { Commit.decorate([commit_1], project) }
+ before do
+ stub_compare
+ end
+
it 'allows the merge request to be created' do
expect(merge_request.can_be_created).to eq(true)
end
@@ -149,6 +175,10 @@ describe MergeRequests::BuildService do
context 'more than one commit in the diff' do
let(:commits) { Commit.decorate([commit_1, commit_2], project) }
+ before do
+ stub_compare
+ end
+
it 'allows the merge request to be created' do
expect(merge_request.can_be_created).to eq(true)
end
diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb
index 23982b9e6e1..0b32c51a16f 100644
--- a/spec/services/merge_requests/conflicts/list_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/list_service_spec.rb
@@ -35,7 +35,7 @@ describe MergeRequests::Conflicts::ListService do
it 'returns a falsey value when the MR has a missing ref after a force push' do
merge_request = create_merge_request('conflict-resolvable')
service = conflicts_service(merge_request)
- allow(service.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError)
+ allow_any_instance_of(Rugged::Repository).to receive(:merge_commits).and_raise(Rugged::OdbError)
expect(service.can_be_resolved_in_ui?).to be_falsey
end
diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index 9c9b0c4c4a1..5376083e7f5 100644
--- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -6,11 +6,7 @@ describe MergeRequests::Conflicts::ResolveService do
let(:project) { create(:project, :public, :repository) }
let(:forked_project) do
- forked_project = fork_project(project, user)
- TestEnv.copy_repo(forked_project,
- bare_repo: TestEnv.forked_repo_path_bare,
- refs: TestEnv::FORKED_BRANCH_SHA)
- forked_project
+ fork_project_with_submodules(project, user)
end
let(:merge_request) do
@@ -111,25 +107,27 @@ describe MergeRequests::Conflicts::ResolveService do
branch_name: 'conflict-start')
end
- def resolve_conflicts
+ subject do
described_class.new(merge_request_from_fork).execute(user, params)
end
it 'gets conflicts from the source project' do
+ # REFACTOR NOTE: We used to test that `project.repository.rugged` wasn't
+ # used in this case, but since the refactor, for simplification,
+ # we always use that repository for read only operations.
expect(forked_project.repository.rugged).to receive(:merge_commits).and_call_original
- expect(project.repository.rugged).not_to receive(:merge_commits)
- resolve_conflicts
+ subject
end
it 'creates a commit with the message' do
- resolve_conflicts
+ subject
expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message])
end
it 'creates a commit with the correct parents' do
- resolve_conflicts
+ subject
expect(merge_request_from_fork.source_branch_head.parents.map(&:id))
.to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', target_head])
@@ -204,14 +202,19 @@ describe MergeRequests::Conflicts::ResolveService do
}
end
- it 'raises a MissingResolution error' do
+ it 'raises a ResolutionError error' do
expect { service.execute(user, invalid_params) }
- .to raise_error(Gitlab::Conflict::File::MissingResolution)
+ .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
end
end
context 'when the content of a file is unchanged' do
- let(:list_service) { MergeRequests::Conflicts::ListService.new(merge_request) }
+ let(:resolver) do
+ MergeRequests::Conflicts::ListService.new(merge_request).conflicts.resolver
+ end
+ let(:regex_conflict) do
+ resolver.conflict_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb')
+ end
let(:invalid_params) do
{
@@ -223,16 +226,16 @@ describe MergeRequests::Conflicts::ResolveService do
}, {
old_path: 'files/ruby/regex.rb',
new_path: 'files/ruby/regex.rb',
- content: list_service.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content
+ content: regex_conflict.content
}
],
commit_message: 'This is a commit message!'
}
end
- it 'raises a MissingResolution error' do
+ it 'raises a ResolutionError error' do
expect { service.execute(user, invalid_params) }
- .to raise_error(Gitlab::Conflict::File::MissingResolution)
+ .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
end
end
@@ -250,9 +253,9 @@ describe MergeRequests::Conflicts::ResolveService do
}
end
- it 'raises a MissingFiles error' do
+ it 'raises a ResolutionError error' do
expect { service.execute(user, invalid_params) }
- .to raise_error(described_class::MissingFiles)
+ .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
end
end
end
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index 313f87ae1f6..a7ab389b357 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -6,8 +6,10 @@ describe MergeRequests::CreateFromIssueService do
let(:label_ids) { create_pair(:label, project: project).map(&:id) }
let(:milestone_id) { create(:milestone, project: project).id }
let(:issue) { create(:issue, project: project, milestone_id: milestone_id) }
+ let(:custom_source_branch) { 'custom-source-branch' }
subject(:service) { described_class.new(project, user, issue_iid: issue.iid) }
+ subject(:service_with_custom_source_branch) { described_class.new(project, user, issue_iid: issue.iid, branch_name: custom_source_branch) }
before do
project.add_developer(user)
@@ -17,8 +19,8 @@ describe MergeRequests::CreateFromIssueService do
it 'returns an error with invalid issue iid' do
result = described_class.new(project, user, issue_iid: -1).execute
- expect(result[:status]).to eq :error
- expect(result[:message]).to eq 'Invalid issue iid'
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Invalid issue iid')
end
it 'delegates issue search to IssuesFinder' do
@@ -53,6 +55,12 @@ describe MergeRequests::CreateFromIssueService do
expect(project.repository.branch_exists?(issue.to_branch_name)).to be_truthy
end
+ it 'creates a branch using passed name' do
+ service_with_custom_source_branch.execute
+
+ expect(project.repository.branch_exists?(custom_source_branch)).to be_truthy
+ end
+
it 'creates a system note' do
expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, issue.to_branch_name)
@@ -72,19 +80,25 @@ describe MergeRequests::CreateFromIssueService do
it 'sets the merge request author to current user' do
result = service.execute
- expect(result[:merge_request].author).to eq user
+ expect(result[:merge_request].author).to eq(user)
end
it 'sets the merge request source branch to the new issue branch' do
result = service.execute
- expect(result[:merge_request].source_branch).to eq issue.to_branch_name
+ expect(result[:merge_request].source_branch).to eq(issue.to_branch_name)
+ end
+
+ it 'sets the merge request source branch to the passed branch name' do
+ result = service_with_custom_source_branch.execute
+
+ expect(result[:merge_request].source_branch).to eq(custom_source_branch)
end
it 'sets the merge request target branch to the project default branch' do
result = service.execute
- expect(result[:merge_request].target_branch).to eq project.default_branch
+ expect(result[:merge_request].target_branch).to eq(project.default_branch)
end
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 80213d093f1..f86f1ac2443 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -12,55 +12,6 @@ describe MergeRequests::MergeService do
end
describe '#execute' do
- context 'MergeRequest#merge_jid' do
- let(:service) do
- described_class.new(project, user, commit_message: 'Awesome message')
- end
-
- before do
- merge_request.update_column(:merge_jid, 'hash-123')
- end
-
- it 'is cleaned when no error is raised' do
- service.execute(merge_request)
-
- expect(merge_request.reload.merge_jid).to be_nil
- end
-
- it 'is cleaned when expected error is raised' do
- 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 cleaned when merge request is not mergeable' do
- allow(merge_request).to receive(:mergeable?).and_return(false)
-
- service.execute(merge_request)
-
- expect(merge_request.reload.merge_jid).to be_nil
- end
-
- it 'is cleaned when no source is found' do
- allow(merge_request).to receive(:diff_head_sha).and_return(nil)
-
- service.execute(merge_request)
-
- expect(merge_request.reload.merge_jid).to be_nil
- end
-
- it 'is not cleaned when unexpected error is raised' do
- service = described_class.new(project, user, commit_message: 'Awesome message')
- allow(service).to receive(:commit).and_raise(StandardError)
-
- 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') }
@@ -185,7 +136,7 @@ describe MergeRequests::MergeService do
context 'source branch removal' do
context 'when the source branch is protected' do
let(:service) do
- described_class.new(project, user, should_remove_source_branch: '1')
+ described_class.new(project, user, 'should_remove_source_branch' => true)
end
before do
@@ -200,7 +151,7 @@ describe MergeRequests::MergeService do
context 'when the source branch is the default branch' do
let(:service) do
- described_class.new(project, user, should_remove_source_branch: '1')
+ described_class.new(project, user, 'should_remove_source_branch' => true)
end
before do
@@ -215,10 +166,10 @@ describe MergeRequests::MergeService do
context 'when the source branch can be removed' do
context 'when MR author set the source branch to be removed' do
- let(:service) do
- merge_request.merge_params['force_remove_source_branch'] = '1'
- merge_request.save!
- described_class.new(project, user, commit_message: 'Awesome message')
+ let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
+
+ before do
+ merge_request.update_attribute(:merge_params, { 'force_remove_source_branch' => '1' })
end
it 'removes the source branch using the author user' do
@@ -227,11 +178,20 @@ describe MergeRequests::MergeService do
.and_call_original
service.execute(merge_request)
end
+
+ context 'when the merger set the source branch not to be removed' do
+ let(:service) { described_class.new(project, user, commit_message: 'Awesome message', 'should_remove_source_branch' => false) }
+
+ it 'does not delete the source branch' do
+ expect(DeleteBranchService).not_to receive(:new)
+ service.execute(merge_request)
+ end
+ end
end
context 'when MR merger set the source branch to be removed' do
let(:service) do
- described_class.new(project, user, commit_message: 'Awesome message', should_remove_source_branch: '1')
+ described_class.new(project, user, commit_message: 'Awesome message', 'should_remove_source_branch' => true)
end
it 'removes the source branch using the current user' do
@@ -288,6 +248,28 @@ describe MergeRequests::MergeService do
expect(merge_request.merge_error).to include(error_message)
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
+
+ context "when fast-forward merge is not allowed" do
+ before do
+ allow_any_instance_of(Repository).to receive(:ancestor?).and_return(nil)
+ end
+
+ %w(semi-linear ff).each do |merge_method|
+ it "logs and saves error if merge is #{merge_method} only" do
+ merge_method = 'rebase_merge' if merge_method == 'semi-linear'
+ merge_request.project.update(merge_method: merge_method)
+ error_message = 'Only fast-forward merge is allowed for your project. Please update your source branch'
+ allow(service).to receive(:execute_hooks)
+
+ service.execute(merge_request)
+
+ expect(merge_request).to be_open
+ expect(merge_request.merge_commit_sha).to be_nil
+ expect(merge_request.merge_error).to include(error_message)
+ expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
+ end
+ end
+ end
end
end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 62dbe362ec8..a2c05761f6b 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -61,7 +61,7 @@ describe MergeRequests::RefreshService do
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks)
- .with(@merge_request, 'update', @oldrev)
+ .with(@merge_request, 'update', old_rev: @oldrev)
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
@@ -87,7 +87,7 @@ describe MergeRequests::RefreshService do
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks)
- .with(@merge_request, 'update', @oldrev)
+ .with(@merge_request, 'update', old_rev: @oldrev)
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
@@ -182,7 +182,7 @@ describe MergeRequests::RefreshService do
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks)
- .with(@fork_merge_request, 'update', @oldrev)
+ .with(@fork_merge_request, 'update', old_rev: @oldrev)
expect(@merge_request.notes).to be_empty
expect(@merge_request).to be_open
@@ -264,7 +264,7 @@ describe MergeRequests::RefreshService do
it 'refreshes the merge request' do
expect(refresh_service).to receive(:execute_hooks)
- .with(@fork_merge_request, 'update', Gitlab::Git::BLANK_SHA)
+ .with(@fork_merge_request, 'update', old_rev: Gitlab::Git::BLANK_SHA)
allow_any_instance_of(Repository).to receive(:merge_base).and_return(@oldrev)
refresh_service.execute(Gitlab::Git::BLANK_SHA, @newrev, 'refs/heads/master')
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index b11a1b31f32..7a66b809550 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -65,7 +65,7 @@ describe MergeRequests::UpdateService, :mailer do
end
end
- it 'mathces base expectations' do
+ it 'matches base expectations' do
expect(@merge_request).to be_valid
expect(@merge_request.title).to eq('New title')
expect(@merge_request.assignee).to eq(user2)
@@ -79,7 +79,16 @@ describe MergeRequests::UpdateService, :mailer do
it 'executes hooks with update action' do
expect(service).to have_received(:execute_hooks)
- .with(@merge_request, 'update')
+ .with(
+ @merge_request,
+ 'update',
+ old_associations: {
+ labels: [],
+ mentioned_users: [user2],
+ assignees: [user3],
+ total_time_spent: 0
+ }
+ )
end
it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
@@ -126,10 +135,10 @@ describe MergeRequests::UpdateService, :mailer do
end
it 'creates system note about discussion lock' do
- note = find_note('locked this issue')
+ note = find_note('locked this merge request')
expect(note).not_to be_nil
- expect(note.note).to eq 'locked this issue'
+ expect(note.note).to eq 'locked this merge request'
end
context 'when not including source branch removal options' do
diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb
index 5739386dd0d..af35e17bfa7 100644
--- a/spec/services/milestones/destroy_service_spec.rb
+++ b/spec/services/milestones/destroy_service_spec.rb
@@ -4,8 +4,8 @@ describe Milestones::DestroyService do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) }
- let(:issue) { create(:issue, project: project, milestone: milestone) }
- let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
+ let!(:issue) { create(:issue, project: project, milestone: milestone) }
+ let!(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
before do
project.team << [user, :master]
diff --git a/spec/services/milestones/promote_service_spec.rb b/spec/services/milestones/promote_service_spec.rb
new file mode 100644
index 00000000000..a0a2843b676
--- /dev/null
+++ b/spec/services/milestones/promote_service_spec.rb
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe Milestones::PromoteService do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+ let(:user) { create(:user) }
+ let(:milestone_title) { 'project milestone' }
+ let(:milestone) { create(:milestone, project: project, title: milestone_title) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ before do
+ group.add_master(user)
+ end
+
+ context 'validations' do
+ it 'raises error if milestone does not belong to a project' do
+ allow(milestone).to receive(:project_milestone?).and_return(false)
+
+ expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError)
+ end
+
+ it 'raises error if project does not belong to a group' do
+ project.update(namespace: user.namespace)
+
+ expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError)
+ end
+
+ it 'does not promote milestone and update issuables if promoted milestone is not valid' do
+ issue = create(:issue, milestone: milestone, project: project)
+ merge_request = create(:merge_request, milestone: milestone, source_project: project)
+ allow_any_instance_of(Milestone).to receive(:valid?).and_return(false)
+
+ expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError)
+
+ expect(milestone.reload).to be_persisted
+ expect(issue.reload.milestone).to eq(milestone)
+ expect(merge_request.reload.milestone).to eq(milestone)
+ end
+ end
+
+ context 'without duplicated milestone titles across projects' do
+ it 'promotes project milestone to group milestone' do
+ promoted_milestone = service.execute(milestone)
+
+ expect(promoted_milestone).to be_group_milestone
+ end
+
+ it 'does not update issuables without milestone with the new promoted milestone' do
+ issue_without_milestone = create(:issue, project: project, milestone: nil)
+ merge_request_without_milestone = create(:merge_request, milestone: nil, source_project: project)
+
+ service.execute(milestone)
+
+ expect(issue_without_milestone.reload.milestone).to be_nil
+ expect(merge_request_without_milestone.reload.milestone).to be_nil
+ end
+
+ it 'sets issuables with new promoted milestone' do
+ issue = create(:issue, milestone: milestone, project: project)
+ merge_request = create(:merge_request, milestone: milestone, source_project: project)
+
+ promoted_milestone = service.execute(milestone)
+
+ expect(promoted_milestone).to be_group_milestone
+ expect(issue.reload.milestone).to eq(promoted_milestone)
+ expect(merge_request.reload.milestone).to eq(promoted_milestone)
+ end
+ end
+
+ context 'with duplicated milestone titles across projects' do
+ let(:project_2) { create(:project, namespace: group) }
+ let!(:milestone_2) { create(:milestone, project: project_2, title: milestone_title) }
+
+ it 'deletes project milestones with the same title' do
+ promoted_milestone = service.execute(milestone)
+
+ expect(promoted_milestone).to be_group_milestone
+ expect(promoted_milestone).to be_valid
+ expect(Milestone.exists?(milestone.id)).to be_falsy
+ expect(Milestone.exists?(milestone_2.id)).to be_falsy
+ end
+
+ it 'does not update issuables without milestone with the new promoted milestone' do
+ issue_without_milestone_1 = create(:issue, project: project, milestone: nil)
+ issue_without_milestone_2 = create(:issue, project: project_2, milestone: nil)
+ merge_request_without_milestone_1 = create(:merge_request, milestone: nil, source_project: project)
+ merge_request_without_milestone_2 = create(:merge_request, milestone: nil, source_project: project_2)
+
+ service.execute(milestone)
+
+ expect(issue_without_milestone_1.reload.milestone).to be_nil
+ expect(issue_without_milestone_2.reload.milestone).to be_nil
+ expect(merge_request_without_milestone_1.reload.milestone).to be_nil
+ expect(merge_request_without_milestone_2.reload.milestone).to be_nil
+ end
+
+ it 'sets all issuables with new promoted milestone' do
+ issue = create(:issue, milestone: milestone, project: project)
+ issue_2 = create(:issue, milestone: milestone_2, project: project_2)
+ merge_request = create(:merge_request, milestone: milestone, source_project: project)
+ merge_request_2 = create(:merge_request, milestone: milestone_2, source_project: project_2)
+
+ promoted_milestone = service.execute(milestone)
+
+ expect(issue.reload.milestone).to eq(promoted_milestone)
+ expect(issue_2.reload.milestone).to eq(promoted_milestone)
+ expect(merge_request.reload.milestone).to eq(promoted_milestone)
+ expect(merge_request_2.reload.milestone).to eq(promoted_milestone)
+ end
+ end
+ end
+end
diff --git a/spec/services/notes/render_service_spec.rb b/spec/services/notes/render_service_spec.rb
new file mode 100644
index 00000000000..faac498037f
--- /dev/null
+++ b/spec/services/notes/render_service_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Notes::RenderService do
+ describe '#execute' do
+ it 'renders a Note' do
+ note = double(:note)
+ project = double(:project)
+ wiki = double(:wiki)
+ user = double(:user)
+
+ expect(Banzai::ObjectRenderer).to receive(:new)
+ .with(project, user,
+ requested_path: 'foo',
+ project_wiki: wiki,
+ ref: 'bar',
+ only_path: nil,
+ xhtml: false)
+ .and_call_original
+
+ expect_any_instance_of(Banzai::ObjectRenderer)
+ .to receive(:render).with([note], :note)
+
+ described_class.new(user).execute([note], project,
+ requested_path: 'foo',
+ project_wiki: wiki,
+ ref: 'bar',
+ only_path: nil,
+ xhtml: false)
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index b64ca5be8fc..db5de572b6d 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -280,6 +280,7 @@ describe NotificationService, :mailer do
next if member.id == @u_disabled.id
# Author should not be notified
next if member.id == note.author.id
+
should_email(member)
end
@@ -327,6 +328,7 @@ describe NotificationService, :mailer do
next if member.id == @u_disabled.id
# Author should not be notified
next if member.id == note.author.id
+
should_email(member)
end
@@ -731,6 +733,18 @@ describe NotificationService, :mailer do
should_not_email(@u_participating)
end
+ it "doesn't send multiple email when a user is subscribed to multiple given labels" do
+ subscriber_to_both = create(:user) do |user|
+ [label_1, label_2].each { |label| label.toggle_subscription(user, project) }
+ end
+
+ notification.relabeled_issue(issue, [label_1, label_2], @u_disabled)
+
+ should_email(subscriber_to_label_1)
+ should_email(subscriber_to_label_2)
+ should_email(subscriber_to_both)
+ end
+
context 'confidential issues' do
let(:author) { create(:user) }
let(:assignee) { create(:user) }
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index c90bad46295..0bec2054f50 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::DestroyService do
+ include ProjectForksHelper
+
let!(:user) { create(:user) }
let!(:project) { create(:project, :repository, namespace: user.namespace) }
let!(:path) { project.repository.path_to_repo }
@@ -212,6 +214,21 @@ describe Projects::DestroyService do
end
end
+ context 'for a forked project with LFS objects' do
+ let(:forked_project) { fork_project(project, user) }
+
+ before do
+ project.lfs_objects << create(:lfs_object)
+ forked_project.forked_project_link.destroy
+ forked_project.reload
+ end
+
+ it 'destroys the fork' do
+ expect { destroy_project(forked_project, user) }
+ .not_to raise_error
+ end
+ end
+
context 'as the root of a fork network' do
let!(:fork_network) { create(:fork_network, root_project: project) }
diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb
new file mode 100644
index 00000000000..ffb270d277e
--- /dev/null
+++ b/spec/services/projects/group_links/create_service_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Projects::GroupLinks::CreateService, '#execute' do
+ let(:user) { create :user }
+ let(:group) { create :group }
+ let(:project) { create :project }
+ let(:opts) do
+ {
+ link_group_access: '30',
+ expires_at: nil
+ }
+ end
+ let(:subject) { described_class.new(project, user, opts) }
+
+ it 'adds group to project' do
+ expect { subject.execute(group) }.to change { project.project_group_links.count }.from(0).to(1)
+ end
+
+ it 'returns false if group is blank' do
+ expect { subject.execute(nil) }.not_to change { project.project_group_links.count }
+ end
+end
diff --git a/spec/services/projects/group_links/destroy_service_spec.rb b/spec/services/projects/group_links/destroy_service_spec.rb
new file mode 100644
index 00000000000..336ee01ae50
--- /dev/null
+++ b/spec/services/projects/group_links/destroy_service_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Projects::GroupLinks::DestroyService, '#execute' do
+ let(:group_link) { create :project_group_link }
+ let(:project) { group_link.project }
+ let(:user) { create :user }
+ let(:subject) { described_class.new(project, user) }
+
+ it 'removes group from project' do
+ expect { subject.execute(group_link) }.to change { project.project_group_links.count }.from(1).to(0)
+ end
+
+ it 'returns false if group_link is blank' do
+ expect { subject.execute(nil) }.not_to change { project.project_group_links.count }
+ end
+end
diff --git a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
new file mode 100644
index 00000000000..50e59954f73
--- /dev/null
+++ b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Projects::HashedStorage::MigrateAttachmentsService do
+ subject(:service) { described_class.new(project) }
+ let(:project) { create(:project) }
+ let(:legacy_storage) { Storage::LegacyProject.new(project) }
+ let(:hashed_storage) { Storage::HashedProject.new(project) }
+
+ let!(:upload) { Upload.find_by(path: file_uploader.relative_path) }
+ let(:file_uploader) { build(:file_uploader, project: project) }
+ let(:old_path) { File.join(base_path(legacy_storage), upload.path) }
+ let(:new_path) { File.join(base_path(hashed_storage), upload.path) }
+
+ context '#execute' do
+ context 'when succeeds' do
+ it 'moves attachments to hashed storage layout' do
+ expect(File.file?(old_path)).to be_truthy
+ expect(File.file?(new_path)).to be_falsey
+ expect(File.exist?(base_path(legacy_storage))).to be_truthy
+ expect(File.exist?(base_path(hashed_storage))).to be_falsey
+ expect(FileUtils).to receive(:mv).with(base_path(legacy_storage), base_path(hashed_storage)).and_call_original
+
+ service.execute
+
+ expect(File.exist?(base_path(hashed_storage))).to be_truthy
+ expect(File.exist?(base_path(legacy_storage))).to be_falsey
+ expect(File.file?(old_path)).to be_falsey
+ expect(File.file?(new_path)).to be_truthy
+ end
+ end
+
+ context 'when original folder does not exist anymore' do
+ before do
+ FileUtils.rm_rf(base_path(legacy_storage))
+ end
+
+ it 'skips moving folders and go to next' do
+ expect(FileUtils).not_to receive(:mv).with(base_path(legacy_storage), base_path(hashed_storage))
+
+ service.execute
+
+ expect(File.exist?(base_path(hashed_storage))).to be_falsey
+ expect(File.file?(new_path)).to be_falsey
+ end
+ end
+
+ context 'when target folder already exists' do
+ before do
+ FileUtils.mkdir_p(base_path(hashed_storage))
+ end
+
+ it 'raises AttachmentMigrationError' do
+ expect(FileUtils).not_to receive(:mv).with(base_path(legacy_storage), base_path(hashed_storage))
+
+ expect { service.execute }.to raise_error(Projects::HashedStorage::AttachmentMigrationError)
+ end
+ end
+ end
+
+ def base_path(storage)
+ FileUploader.dynamic_path_builder(storage.disk_path)
+ end
+end
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
new file mode 100644
index 00000000000..3a3e47fd9c0
--- /dev/null
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+describe Projects::HashedStorage::MigrateRepositoryService do
+ let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:project) { create(:project, :empty_repo, :wiki_repo) }
+ let(:service) { described_class.new(project) }
+ let(:legacy_storage) { Storage::LegacyProject.new(project) }
+ let(:hashed_storage) { Storage::HashedProject.new(project) }
+
+ describe '#execute' do
+ before do
+ allow(service).to receive(:gitlab_shell) { gitlab_shell }
+ end
+
+ context 'when succeeds' do
+ it 'renames project and wiki repositories' do
+ service.execute
+
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy
+ end
+
+ it 'updates project to be hashed and not read-only' do
+ service.execute
+
+ expect(project.hashed_storage?(:repository)).to be_truthy
+ expect(project.repository_read_only).to be_falsey
+ end
+
+ it 'move operation is called for both repositories' do
+ expect_move_repository(project.disk_path, hashed_storage.disk_path)
+ expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki")
+
+ service.execute
+ end
+ end
+
+ context 'when one move fails' do
+ it 'rollsback repositories to original name' do
+ from_name = project.disk_path
+ to_name = hashed_storage.disk_path
+ allow(service).to receive(:move_repository).and_call_original
+ allow(service).to receive(:move_repository).with(from_name, to_name).once { false } # will disable first move only
+
+ expect(service).to receive(:rollback_folder_move).and_call_original
+
+ service.execute
+
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_falsey
+ expect(project.repository_read_only?).to be_falsey
+ end
+
+ context 'when rollback fails' do
+ let(:from_name) { legacy_storage.disk_path }
+ let(:to_name) { hashed_storage.disk_path }
+
+ before do
+ hashed_storage.ensure_storage_path_exists
+ gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
+ end
+
+ it 'does not try to move nil repository over hashed' do
+ expect(gitlab_shell).not_to receive(:mv_repository).with(project.repository_storage_path, from_name, to_name)
+ expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki")
+
+ service.execute
+ end
+ end
+ end
+
+ def expect_move_repository(from_name, to_name)
+ expect(gitlab_shell).to receive(:mv_repository).with(project.repository_storage_path, from_name, to_name).and_call_original
+ end
+ end
+end
diff --git a/spec/services/projects/hashed_storage_migration_service_spec.rb b/spec/services/projects/hashed_storage_migration_service_spec.rb
index aa1988d29d6..466f0b5d7c2 100644
--- a/spec/services/projects/hashed_storage_migration_service_spec.rb
+++ b/spec/services/projects/hashed_storage_migration_service_spec.rb
@@ -1,74 +1,44 @@
require 'spec_helper'
describe Projects::HashedStorageMigrationService do
- let(:gitlab_shell) { Gitlab::Shell.new }
let(:project) { create(:project, :empty_repo, :wiki_repo) }
- let(:service) { described_class.new(project) }
- let(:legacy_storage) { Storage::LegacyProject.new(project) }
- let(:hashed_storage) { Storage::HashedProject.new(project) }
+ subject(:service) { described_class.new(project) }
describe '#execute' do
- before do
- allow(service).to receive(:gitlab_shell) { gitlab_shell }
- end
-
- context 'when succeeds' do
- it 'renames project and wiki repositories' do
- service.execute
+ context 'repository migration' do
+ let(:repository_service) { Projects::HashedStorage::MigrateRepositoryService.new(project, subject.logger) }
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_truthy
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy
- end
+ it 'delegates migration to Projects::HashedStorage::MigrateRepositoryService' do
+ expect(Projects::HashedStorage::MigrateRepositoryService).to receive(:new).with(project, subject.logger).and_return(repository_service)
+ expect(repository_service).to receive(:execute)
- it 'updates project to be hashed and not read-only' do
service.execute
-
- expect(project.hashed_storage?).to be_truthy
- expect(project.repository_read_only).to be_falsey
end
- it 'move operation is called for both repositories' do
- expect_move_repository(project.disk_path, hashed_storage.disk_path)
- expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki")
+ it 'does not delegate migration if repository is already migrated' do
+ project.storage_version = ::Project::LATEST_STORAGE_VERSION
+ expect(Projects::HashedStorage::MigrateRepositoryService).not_to receive(:new)
service.execute
end
end
- context 'when one move fails' do
- it 'rollsback repositories to original name' do
- from_name = project.disk_path
- to_name = hashed_storage.disk_path
- allow(service).to receive(:move_repository).and_call_original
- allow(service).to receive(:move_repository).with(from_name, to_name).once { false } # will disable first move only
+ context 'attachments migration' do
+ let(:attachments_service) { Projects::HashedStorage::MigrateAttachmentsService.new(project, subject.logger) }
- expect(service).to receive(:rollback_folder_move).and_call_original
+ it 'delegates migration to Projects::HashedStorage::MigrateRepositoryService' do
+ expect(Projects::HashedStorage::MigrateAttachmentsService).to receive(:new).with(project, subject.logger).and_return(attachments_service)
+ expect(attachments_service).to receive(:execute)
service.execute
-
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_falsey
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_falsey
end
- context 'when rollback fails' do
- before do
- from_name = legacy_storage.disk_path
- to_name = hashed_storage.disk_path
+ it 'does not delegate migration if attachments are already migrated' do
+ project.storage_version = ::Project::LATEST_STORAGE_VERSION
+ expect(Projects::HashedStorage::MigrateAttachmentsService).not_to receive(:new)
- hashed_storage.ensure_storage_path_exists
- gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
- end
-
- it 'does not try to move nil repository over hashed' do
- expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki")
-
- service.execute
- end
+ service.execute
end
end
-
- def expect_move_repository(from_name, to_name)
- expect(gitlab_shell).to receive(:mv_repository).with(project.repository_storage_path, from_name, to_name).and_call_original
- end
end
end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 034065aab00..bf7facaec99 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -6,6 +6,41 @@ describe Projects::ImportService do
subject { described_class.new(project, user) }
+ describe '#async?' do
+ it 'returns true for an asynchronous importer' do
+ importer_class = double(:importer, async?: true)
+
+ allow(subject).to receive(:has_importer?).and_return(true)
+ allow(subject).to receive(:importer_class).and_return(importer_class)
+
+ expect(subject).to be_async
+ end
+
+ it 'returns false for a regular importer' do
+ importer_class = double(:importer, async?: false)
+
+ allow(subject).to receive(:has_importer?).and_return(true)
+ allow(subject).to receive(:importer_class).and_return(importer_class)
+
+ expect(subject).not_to be_async
+ end
+
+ it 'returns false when the importer does not define #async?' do
+ importer_class = double(:importer)
+
+ allow(subject).to receive(:has_importer?).and_return(true)
+ allow(subject).to receive(:importer_class).and_return(importer_class)
+
+ expect(subject).not_to be_async
+ end
+
+ it 'returns false when the importer does not exist' do
+ allow(subject).to receive(:has_importer?).and_return(false)
+
+ expect(subject).not_to be_async
+ end
+ end
+
describe '#execute' do
context 'with unknown url' do
before do
@@ -37,21 +72,24 @@ describe Projects::ImportService do
end
context 'with a Github repository' do
- it 'succeeds if repository import is successfully' do
- expect_any_instance_of(Github::Import).to receive(:execute).and_return(true)
+ it 'succeeds if repository import was scheduled' do
+ expect_any_instance_of(Gitlab::GithubImport::ParallelImporter)
+ .to receive(:execute)
+ .and_return(true)
result = subject.execute
expect(result[:status]).to eq :success
end
- it 'fails if repository import fails' do
- expect_any_instance_of(Repository).to receive(:fetch_remote).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
+ it 'fails if repository import was not scheduled' do
+ expect_any_instance_of(Gitlab::GithubImport::ParallelImporter)
+ .to receive(:execute)
+ .and_return(false)
result = subject.execute
expect(result[:status]).to eq :error
- expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - The remote data could not be imported."
end
end
@@ -92,47 +130,22 @@ describe Projects::ImportService do
end
it 'succeeds if importer succeeds' do
- allow_any_instance_of(Github::Import).to receive(:execute).and_return(true)
+ allow_any_instance_of(Gitlab::GithubImport::ParallelImporter)
+ .to receive(:execute).and_return(true)
result = subject.execute
expect(result[:status]).to eq :success
end
- it 'flushes various caches' do
- allow_any_instance_of(Github::Import).to receive(:execute)
- .and_return(true)
-
- expect_any_instance_of(Repository).to receive(:expire_content_cache)
-
- subject.execute
- end
-
it 'fails if importer fails' do
- allow_any_instance_of(Github::Import).to receive(:execute).and_return(false)
-
- result = subject.execute
-
- expect(result[:status]).to eq :error
- expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.full_path} - The remote data could not be imported."
- end
-
- it 'fails if importer raise an error' do
- allow_any_instance_of(Github::Import).to receive(:execute).and_raise(Projects::ImportService::Error.new('Github: failed to connect API'))
+ allow_any_instance_of(Gitlab::GithubImport::ParallelImporter)
+ .to receive(:execute)
+ .and_return(false)
result = subject.execute
expect(result[:status]).to eq :error
- expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.full_path} - Github: failed to connect API"
- end
-
- it 'expires content cache after error' do
- allow_any_instance_of(Project).to receive(:repository_exists?).and_return(false)
-
- expect_any_instance_of(Repository).to receive(:fetch_remote).and_raise(Gitlab::Shell::Error.new)
- expect_any_instance_of(Repository).to receive(:expire_content_cache)
-
- subject.execute
end
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 2459f371a91..2b1337bee7e 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -42,6 +42,18 @@ describe Projects::TransferService do
expect(service).to receive(:execute_system_hooks)
end
end
+
+ it 'disk path has moved' do
+ old_path = project.repository.disk_path
+ old_full_path = project.repository.full_path
+
+ transfer_project(project, user, group)
+
+ expect(project.repository.disk_path).not_to eq(old_path)
+ expect(project.repository.full_path).not_to eq(old_full_path)
+ expect(project.disk_path).not_to eq(old_path)
+ expect(project.disk_path).to start_with(group.path)
+ end
end
context 'when transfer fails' do
@@ -188,6 +200,26 @@ describe Projects::TransferService do
end
end
+ context 'when hashed storage in use' do
+ let(:hashed_project) { create(:project, :repository, :hashed, namespace: user.namespace) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ it 'does not move the directory' do
+ old_path = hashed_project.repository.disk_path
+ old_full_path = hashed_project.repository.full_path
+
+ transfer_project(hashed_project, user, group)
+ project.reload
+
+ expect(hashed_project.repository.disk_path).to eq(old_path)
+ expect(hashed_project.repository.full_path).to eq(old_full_path)
+ expect(hashed_project.disk_path).to eq(old_path)
+ end
+ end
+
describe 'refreshing project authorizations' do
let(:group) { create(:group) }
let(:owner) { project.namespace.owner }
diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb
index 50d3a4ec982..2bba71fef4f 100644
--- a/spec/services/projects/unlink_fork_service_spec.rb
+++ b/spec/services/projects/unlink_fork_service_spec.rb
@@ -12,6 +12,9 @@ describe Projects::UnlinkForkService do
context 'with opened merge request on the source project' do
let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: fork_link.forked_from_project) }
+ let(:merge_request2) { create(:merge_request, source_project: forked_project, target_project: fork_project(project)) }
+ let(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) }
+
let(:mr_close_service) { MergeRequests::CloseService.new(forked_project, user) }
before do
@@ -22,9 +25,14 @@ describe Projects::UnlinkForkService do
it 'close all pending merge requests' do
expect(mr_close_service).to receive(:execute).with(merge_request)
+ expect(mr_close_service).to receive(:execute).with(merge_request2)
subject.execute
end
+
+ it 'does not close merge requests for the project being unlinked' do
+ expect(mr_close_service).not_to receive(:execute).with(merge_request_in_fork)
+ end
end
it 'remove fork relation' do
@@ -53,4 +61,14 @@ describe Projects::UnlinkForkService do
expect(source.forks_count).to be_zero
end
+
+ context 'when the original project was deleted' do
+ it 'does not fail when the original project is deleted' do
+ source = forked_project.forked_from_project
+ source.destroy
+ forked_project.reload
+
+ expect { subject.execute }.not_to raise_error
+ end
+ end
end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 031366d1825..d4ac1f6ad81 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -52,6 +52,11 @@ describe Projects::UpdatePagesService do
expect(project.pages_deployed?).to be_falsey
expect(execute).to eq(:success)
expect(project.pages_deployed?).to be_truthy
+
+ # Check that all expected files are extracted
+ %w[index.html zero .hidden/file].each do |filename|
+ expect(File.exist?(File.join(project.public_pages_path, filename))).to be_truthy
+ end
end
it 'limits pages size' do
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 3da222e2ed8..fcd71857af3 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -1,198 +1,222 @@
require 'spec_helper'
-describe Projects::UpdateService, '#execute' do
+describe Projects::UpdateService do
include ProjectForksHelper
- let(:gitlab_shell) { Gitlab::Shell.new }
let(:user) { create(:user) }
- let(:admin) { create(:admin) }
-
let(:project) do
create(:project, creator: user, namespace: user.namespace)
end
- context 'when changing visibility level' do
- context 'when visibility_level is INTERNAL' do
- it 'updates the project to internal' do
- result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
-
- expect(result).to eq({ status: :success })
- expect(project).to be_internal
- end
- end
-
- context 'when visibility_level is PUBLIC' do
- it 'updates the project to public' do
- result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- expect(result).to eq({ status: :success })
- expect(project).to be_public
- end
- end
-
- context 'when visibility levels are restricted to PUBLIC only' do
- before do
- stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
- end
+ describe '#execute' do
+ let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:admin) { create(:admin) }
+ context 'when changing visibility level' do
context 'when visibility_level is INTERNAL' do
it 'updates the project to internal' do
result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+
expect(result).to eq({ status: :success })
expect(project).to be_internal
end
end
context 'when visibility_level is PUBLIC' do
- it 'does not update the project to public' do
+ it 'updates the project to public' do
result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ expect(result).to eq({ status: :success })
+ expect(project).to be_public
+ end
+ end
- expect(result).to eq({ status: :error, message: 'New visibility level not allowed!' })
- expect(project).to be_private
+ context 'when visibility levels are restricted to PUBLIC only' do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
- context 'when updated by an admin' do
- it 'updates the project to public' do
- result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ context 'when visibility_level is INTERNAL' do
+ it 'updates the project to internal' do
+ result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
expect(result).to eq({ status: :success })
- expect(project).to be_public
+ expect(project).to be_internal
end
end
- end
- end
- context 'When project visibility is higher than parent group' do
- let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
+ context 'when visibility_level is PUBLIC' do
+ it 'does not update the project to public' do
+ result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- before do
- project.update(namespace: group, visibility_level: group.visibility_level)
+ expect(result).to eq({ status: :error, message: 'New visibility level not allowed!' })
+ expect(project).to be_private
+ end
+
+ context 'when updated by an admin' do
+ it 'updates the project to public' do
+ result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ expect(result).to eq({ status: :success })
+ expect(project).to be_public
+ end
+ end
+ end
end
- it 'does not update project visibility level' do
- result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ context 'When project visibility is higher than parent group' do
+ let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
+
+ before do
+ project.update(namespace: group, visibility_level: group.visibility_level)
+ end
+
+ it 'does not update project visibility level' do
+ result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- expect(result).to eq({ status: :error, message: 'Visibility level public is not allowed in a internal group.' })
- expect(project.reload).to be_internal
+ expect(result).to eq({ status: :error, message: 'Visibility level public is not allowed in a internal group.' })
+ expect(project.reload).to be_internal
+ end
end
end
- end
- describe 'when updating project that has forks' do
- let(:project) { create(:project, :internal) }
- let(:forked_project) { fork_project(project) }
+ describe 'when updating project that has forks' do
+ let(:project) { create(:project, :internal) }
+ let(:forked_project) { fork_project(project) }
- it 'updates forks visibility level when parent set to more restrictive' do
- opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
+ it 'updates forks visibility level when parent set to more restrictive' do
+ opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
- expect(project).to be_internal
- expect(forked_project).to be_internal
+ expect(project).to be_internal
+ expect(forked_project).to be_internal
- expect(update_project(project, admin, opts)).to eq({ status: :success })
+ expect(update_project(project, admin, opts)).to eq({ status: :success })
- expect(project).to be_private
- expect(forked_project.reload).to be_private
- end
+ expect(project).to be_private
+ expect(forked_project.reload).to be_private
+ end
- it 'does not update forks visibility level when parent set to less restrictive' do
- opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
+ it 'does not update forks visibility level when parent set to less restrictive' do
+ opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
- expect(project).to be_internal
- expect(forked_project).to be_internal
+ expect(project).to be_internal
+ expect(forked_project).to be_internal
- expect(update_project(project, admin, opts)).to eq({ status: :success })
+ expect(update_project(project, admin, opts)).to eq({ status: :success })
- expect(project).to be_public
- expect(forked_project.reload).to be_internal
+ expect(project).to be_public
+ expect(forked_project.reload).to be_internal
+ end
end
- end
- context 'when updating a default branch' do
- let(:project) { create(:project, :repository) }
+ context 'when updating a default branch' do
+ let(:project) { create(:project, :repository) }
- it 'changes a default branch' do
- update_project(project, admin, default_branch: 'feature')
+ it 'changes a default branch' do
+ update_project(project, admin, default_branch: 'feature')
- expect(Project.find(project.id).default_branch).to eq 'feature'
- end
+ expect(Project.find(project.id).default_branch).to eq 'feature'
+ end
- it 'does not change a default branch' do
- # The branch 'unexisted-branch' does not exist.
- update_project(project, admin, default_branch: 'unexisted-branch')
+ it 'does not change a default branch' do
+ # The branch 'unexisted-branch' does not exist.
+ update_project(project, admin, default_branch: 'unexisted-branch')
- expect(Project.find(project.id).default_branch).to eq 'master'
+ expect(Project.find(project.id).default_branch).to eq 'master'
+ end
end
- end
- context 'when updating a project that contains container images' do
- before do
- stub_container_registry_config(enabled: true)
- stub_container_registry_tags(repository: /image/, tags: %w[rc1])
- create(:container_repository, project: project, name: :image)
- end
+ context 'when updating a project that contains container images' do
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(repository: /image/, tags: %w[rc1])
+ create(:container_repository, project: project, name: :image)
+ end
- it 'does not allow to rename the project' do
- result = update_project(project, admin, path: 'renamed')
+ it 'does not allow to rename the project' do
+ result = update_project(project, admin, path: 'renamed')
- expect(result).to include(status: :error)
- expect(result[:message]).to match(/contains container registry tags/)
- end
+ expect(result).to include(status: :error)
+ expect(result[:message]).to match(/contains container registry tags/)
+ end
- it 'allows to update other settings' do
- result = update_project(project, admin, public_builds: true)
+ it 'allows to update other settings' do
+ result = update_project(project, admin, public_builds: true)
- expect(result[:status]).to eq :success
- expect(project.reload.public_builds).to be true
+ expect(result[:status]).to eq :success
+ expect(project.reload.public_builds).to be true
+ end
end
- end
- context 'when renaming a project' do
- let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ context 'when renaming a project' do
+ let(:repository_storage) { 'default' }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
- context 'with legacy storage' do
- before do
- gitlab_shell.add_repository(repository_storage, "#{user.namespace.full_path}/existing")
- end
+ context 'with legacy storage' do
+ before do
+ gitlab_shell.add_repository(repository_storage, "#{user.namespace.full_path}/existing")
+ end
+
+ after do
+ gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ end
+
+ it 'does not allow renaming when new path matches existing repository on disk' do
+ result = update_project(project, admin, path: 'existing')
- after do
- gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ expect(result).to include(status: :error)
+ expect(result[:message]).to match('There is already a repository with that name on disk')
+ expect(project).not_to be_valid
+ expect(project.errors.messages).to have_key(:base)
+ expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk')
+ end
end
- it 'does not allow renaming when new path matches existing repository on disk' do
- result = update_project(project, admin, path: 'existing')
+ context 'with hashed storage' do
+ let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
- expect(result).to include(status: :error)
- expect(result[:message]).to match('There is already a repository with that name on disk')
- expect(project).not_to be_valid
- expect(project.errors.messages).to have_key(:base)
- expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk')
+ before do
+ stub_application_setting(hashed_storage_enabled: true)
+ end
+
+ it 'does not check if new path matches existing repository on disk' do
+ expect(project).not_to receive(:repository_with_same_path_already_exists?)
+
+ result = update_project(project, admin, path: 'existing')
+
+ expect(result).to include(status: :success)
+ end
end
end
- context 'with hashed storage' do
- let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
+ 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' })
- before do
- stub_application_setting(hashed_storage_enabled: true)
+ expect(result).to eq({
+ status: :error,
+ message: "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
+ })
end
+ end
+ end
- it 'does not check if new path matches existing repository on disk' do
- expect(project).not_to receive(:repository_with_same_path_already_exists?)
+ describe '#run_auto_devops_pipeline?' do
+ subject { described_class.new(project, user, params).run_auto_devops_pipeline? }
- result = update_project(project, admin, path: 'existing')
+ context 'when neither pipeline setting is true' do
+ let(:params) { {} }
- expect(result).to include(status: :success)
- end
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when run_auto_devops_pipeline_explicit is true' do
+ let(:params) { { run_auto_devops_pipeline_explicit: 'true' } }
+
+ it { is_expected.to eq(true) }
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' })
+ context 'when run_auto_devops_pipeline_implicit is true' do
+ let(:params) { { run_auto_devops_pipeline_implicit: 'true' } }
- expect(result).to eq({
- status: :error,
- message: "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
- })
+ it { is_expected.to eq(true) }
end
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 6926ac85de3..c35177f6ebc 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -207,7 +207,11 @@ describe QuickActions::InterpretService do
it 'populates spend_time: 3600 if content contains /spend 1h' do
_, updates = service.execute(content, issuable)
- expect(updates).to eq(spend_time: { duration: 3600, user: developer })
+ expect(updates).to eq(spend_time: {
+ duration: 3600,
+ user: developer,
+ spent_at: DateTime.now.to_date
+ })
end
end
@@ -215,7 +219,39 @@ describe QuickActions::InterpretService do
it 'populates spend_time: -1800 if content contains /spend -30m' do
_, updates = service.execute(content, issuable)
- expect(updates).to eq(spend_time: { duration: -1800, user: developer })
+ expect(updates).to eq(spend_time: {
+ duration: -1800,
+ user: developer,
+ spent_at: DateTime.now.to_date
+ })
+ end
+ end
+
+ shared_examples 'spend command with valid date' do
+ it 'populates spend time: 1800 with date in date type format' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(spend_time: {
+ duration: 1800,
+ user: developer,
+ spent_at: Date.parse(date)
+ })
+ end
+ end
+
+ shared_examples 'spend command with invalid date' do
+ it 'will not create any note and timelog' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq({})
+ end
+ end
+
+ shared_examples 'spend command with future date' do
+ it 'will not create any note and timelog' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq({})
end
end
@@ -669,6 +705,22 @@ describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ it_behaves_like 'spend command with valid date' do
+ let(:date) { '2016-02-02' }
+ let(:content) { "/spend 30m #{date}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'spend command with invalid date' do
+ let(:content) { '/spend 30m 17-99-99' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'spend command with future date' do
+ let(:content) { '/spend 30m 6017-10-10' }
+ let(:issuable) { issue }
+ end
+
it_behaves_like 'empty command' do
let(:content) { '/spend' }
let(:issuable) { issue }
diff --git a/spec/services/search/global_service_spec.rb b/spec/services/search/global_service_spec.rb
index 1309240b430..d8dba26e194 100644
--- a/spec/services/search/global_service_spec.rb
+++ b/spec/services/search/global_service_spec.rb
@@ -35,8 +35,8 @@ describe Search::GlobalService do
expect(results.objects('projects')).to match_array [internal_project, public_project]
end
- it 'namespace name is searchable' do
- results = described_class.new(user, search: found_project.namespace.path).execute
+ it 'project name is searchable' do
+ results = described_class.new(user, search: found_project.name).execute
expect(results.objects('projects')).to match_array [found_project]
end
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index 8b5d9187785..46cd10cdc12 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -63,11 +63,54 @@ describe SystemHooksService do
:group_id, :user_id, :user_username, :user_name, :user_email, :group_access
)
end
+
+ it 'includes the correct project visibility level' do
+ data = event_data(project, :create)
+
+ expect(data[:project_visibility]).to eq('private')
+ end
+
+ context 'group_rename' do
+ it 'contains old and new path' do
+ allow(group).to receive(:path_was).and_return('old-path')
+
+ data = event_data(group, :rename)
+
+ expect(data).to include(:event_name, :name, :created_at, :updated_at, :full_path, :path, :group_id, :old_path, :old_full_path)
+ expect(data[:path]).to eq(group.path)
+ expect(data[:full_path]).to eq(group.path)
+ expect(data[:old_path]).to eq(group.path_was)
+ expect(data[:old_full_path]).to eq(group.path_was)
+ end
+
+ it 'contains old and new full_path for subgroup' do
+ subgroup = create(:group, parent: group)
+ allow(subgroup).to receive(:path_was).and_return('old-path')
+
+ data = event_data(subgroup, :rename)
+
+ expect(data[:full_path]).to eq(subgroup.full_path)
+ expect(data[:old_path]).to eq('old-path')
+ end
+ end
+
+ context 'user_rename' do
+ it 'contains old and new username' do
+ allow(user).to receive(:username_was).and_return('old-username')
+
+ data = event_data(user, :rename)
+
+ expect(data).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username, :old_username)
+ expect(data[:username]).to eq(user.username)
+ expect(data[:old_username]).to eq(user.username_was)
+ end
+ end
end
context 'event names' do
it { expect(event_name(user, :create)).to eq "user_create" }
it { expect(event_name(user, :destroy)).to eq "user_destroy" }
+ it { expect(event_name(user, :rename)).to eq 'user_rename' }
it { expect(event_name(project, :create)).to eq "project_create" }
it { expect(event_name(project, :destroy)).to eq "project_destroy" }
it { expect(event_name(project, :rename)).to eq "project_rename" }
@@ -79,6 +122,7 @@ describe SystemHooksService do
it { expect(event_name(key, :destroy)).to eq 'key_destroy' }
it { expect(event_name(group, :create)).to eq 'group_create' }
it { expect(event_name(group, :destroy)).to eq 'group_destroy' }
+ it { expect(event_name(group, :rename)).to eq 'group_rename' }
it { expect(event_name(group_member, :create)).to eq 'user_add_to_group' }
it { expect(event_name(group_member, :destroy)).to eq 'user_remove_from_group' }
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index b1241cd8d0b..a918383ecd2 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -502,20 +502,6 @@ describe SystemNoteService do
end
end
- describe '.cross_reference?' do
- it 'is truthy when text begins with expected text' do
- expect(described_class.cross_reference?('mentioned in something')).to be_truthy
- end
-
- it 'is truthy when text begins with legacy capitalized expected text' do
- expect(described_class.cross_reference?('mentioned in something')).to be_truthy
- end
-
- it 'is falsey when text does not begin with expected text' do
- expect(described_class.cross_reference?('this is a note')).to be_falsey
- end
- end
-
describe '.cross_reference_disallowed?' do
context 'when mentioner is not a MergeRequest' do
it 'is falsey' do
@@ -984,31 +970,33 @@ describe SystemNoteService do
end
end
- describe '.remove_merge_request_wip' do
- let(:noteable) { create(:issue, project: project, title: 'WIP: Lorem ipsum') }
+ describe '.handle_merge_request_wip' do
+ context 'adding wip note' do
+ let(:noteable) { create(:merge_request, source_project: project, title: 'WIP Lorem ipsum') }
- subject { described_class.remove_merge_request_wip(noteable, project, author) }
+ subject { described_class.handle_merge_request_wip(noteable, project, author) }
- it_behaves_like 'a system note' do
- let(:action) { 'title' }
- end
+ it_behaves_like 'a system note' do
+ let(:action) { 'title' }
+ end
- it 'sets the note text' do
- expect(subject.note).to eq 'unmarked as a **Work In Progress**'
+ it 'sets the note text' do
+ expect(subject.note).to eq 'marked as a **Work In Progress**'
+ end
end
- end
- describe '.add_merge_request_wip' do
- let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') }
+ context 'removing wip note' do
+ let(:noteable) { create(:merge_request, source_project: project, title: 'Lorem ipsum') }
- subject { described_class.add_merge_request_wip(noteable, project, author) }
+ subject { described_class.handle_merge_request_wip(noteable, project, author) }
- it_behaves_like 'a system note' do
- let(:action) { 'title' }
- end
+ it_behaves_like 'a system note' do
+ let(:action) { 'title' }
+ end
- it 'sets the note text' do
- expect(subject.note).to eq 'marked as a **Work In Progress**'
+ it 'sets the note text' do
+ expect(subject.note).to eq 'unmarked as a **Work In Progress**'
+ end
end
end
@@ -1145,4 +1133,42 @@ describe SystemNoteService do
it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue" }
end
end
+
+ describe '.discussion_lock' do
+ subject { described_class.discussion_lock(noteable, author) }
+
+ context 'discussion unlocked' do
+ it_behaves_like 'a system note' do
+ let(:action) { 'unlocked' }
+ end
+
+ it 'creates the note text correctly' do
+ [:issue, :merge_request].each do |type|
+ issuable = create(type)
+
+ expect(described_class.discussion_lock(issuable, author).note)
+ .to eq("unlocked this #{type.to_s.titleize.downcase}")
+ end
+ end
+ end
+
+ context 'discussion locked' do
+ before do
+ noteable.update_attribute(:discussion_locked, true)
+ end
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'locked' }
+ end
+
+ it 'creates the note text correctly' do
+ [:issue, :merge_request].each do |type|
+ issuable = create(type, discussion_locked: true)
+
+ expect(described_class.discussion_lock(issuable, author).note)
+ .to eq("locked this #{type.to_s.titleize.downcase}")
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index a9b34a5258a..dc2673abc73 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -248,11 +248,11 @@ describe TodoService do
end
end
- describe '#destroy_issue' do
+ describe '#destroy_issuable' do
it 'refresh the todos count cache for the user' do
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
- service.destroy_issue(issue, john_doe)
+ service.destroy_issuable(issue, john_doe)
end
end
@@ -643,14 +643,6 @@ describe TodoService do
end
end
- describe '#destroy_merge_request' do
- it 'refresh the todos count cache for the user' do
- expect(john_doe).to receive(:update_todos_count_cache).and_call_original
-
- service.destroy_merge_request(mr_assigned, john_doe)
- end
- end
-
describe '#reassigned_merge_request' do
it 'creates a pending todo for new assignee' do
mr_unassigned.update_attribute(:assignee, john_doe)
diff --git a/spec/services/users/keys_count_service_spec.rb b/spec/services/users/keys_count_service_spec.rb
new file mode 100644
index 00000000000..a188cf86772
--- /dev/null
+++ b/spec/services/users/keys_count_service_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Users::KeysCountService, :use_clean_rails_memory_store_caching do
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(user) }
+
+ describe '#count' do
+ before do
+ create(:personal_key, user: user)
+ end
+
+ it 'returns the number of SSH keys as an Integer' do
+ expect(service.count).to eq(1)
+ end
+
+ it 'caches the number of keys in Redis' do
+ service.delete_cache
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ 2.times { service.count }
+ end
+
+ expect(recorder.count).to eq(1)
+ end
+ end
+
+ describe '#refresh_cache' do
+ it 'refreshes the Redis cache' do
+ Rails.cache.write(service.cache_key, 10)
+ service.refresh_cache
+
+ expect(Rails.cache.fetch(service.cache_key, raw: true)).to be_zero
+ end
+ end
+
+ describe '#delete_cache' do
+ it 'removes the cache' do
+ service.count
+ service.delete_cache
+
+ expect(Rails.cache.fetch(service.cache_key, raw: true)).to be_nil
+ end
+ end
+
+ describe '#uncached_count' do
+ it 'returns the number of SSH keys' do
+ expect(service.uncached_count).to be_zero
+ end
+
+ it 'does not cache the number of keys' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ 2.times { service.uncached_count }
+ end
+
+ expect(recorder.count).to be > 0
+ end
+ end
+
+ describe '#cache_key' do
+ it 'returns the cache key' do
+ expect(service.cache_key).to eq("users/key-count-service/#{user.id}")
+ end
+ end
+end
diff --git a/spec/services/users/last_push_event_service_spec.rb b/spec/services/users/last_push_event_service_spec.rb
index 956358738fe..2b6c0267a0f 100644
--- a/spec/services/users/last_push_event_service_spec.rb
+++ b/spec/services/users/last_push_event_service_spec.rb
@@ -22,7 +22,6 @@ describe Users::LastPushEventService do
it 'caches the event for the origin project when pushing to a fork' do
source = build(:project, id: 5)
- allow(project).to receive(:forked?).and_return(true)
allow(project).to receive(:forked_from_project).and_return(source)
expect(service).to receive(:set_key)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 48cacba6a8a..6310ea1b52b 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -48,7 +48,12 @@ RSpec.configure do |config|
config.include Warden::Test::Helpers, type: :request
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
+ config.include CookieHelper, :js
+ config.include InputHelper, :js
+ config.include SelectionHelper, :js
+ config.include InspectRequests, :js
config.include WaitForRequests, :js
+ config.include LiveDebugger, :js
config.include StubConfiguration
config.include EmailHelpers, :mailer, type: :mailer
config.include TestEnv
diff --git a/spec/support/api/issues_resolving_discussions_shared_examples.rb b/spec/support/api/issues_resolving_discussions_shared_examples.rb
index d26d279363c..d2d6260dfa8 100644
--- a/spec/support/api/issues_resolving_discussions_shared_examples.rb
+++ b/spec/support/api/issues_resolving_discussions_shared_examples.rb
@@ -1,6 +1,6 @@
shared_examples 'creating an issue resolving discussions through the API' do
it 'creates a new project issue' do
- expect(response).to have_http_status(:created)
+ expect(response).to have_gitlab_http_status(:created)
end
it 'resolves the discussions in a merge request' do
diff --git a/spec/support/api/members_shared_examples.rb b/spec/support/api/members_shared_examples.rb
index dab71a35a55..8d910e52eda 100644
--- a/spec/support/api/members_shared_examples.rb
+++ b/spec/support/api/members_shared_examples.rb
@@ -6,6 +6,6 @@ shared_examples 'a 404 response when source is private' do
it 'returns 404' do
route
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb
index 4bb5113957e..d9080b02541 100644
--- a/spec/support/api/milestones_shared_examples.rb
+++ b/spec/support/api/milestones_shared_examples.rb
@@ -10,7 +10,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'returns milestones list' do
get api(route, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(milestone.title)
@@ -19,13 +19,13 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'returns a 401 error if user not authenticated' do
get api(route)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns an array of active milestones' do
get api("#{route}/?state=active", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -35,7 +35,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'returns an array of closed milestones' do
get api("#{route}/?state=closed", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -47,7 +47,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
get api(route, user), iids: [closed_milestone.iid, other_milestone.iid]
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.map { |m| m['id'] }).to match_array([closed_milestone.id, other_milestone.id])
@@ -56,7 +56,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'does not return any milestone if none found' do
get api(route, user), iids: [Milestone.maximum(:iid).succ]
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -75,7 +75,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'returns a milestone by searching for title' do
get api(route, user), search: 'version2'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.size).to eq(1)
expect(json_response.first['title']).to eq milestone.title
@@ -85,7 +85,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'returns a milestones by searching for description' do
get api(route, user), search: 'open'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.size).to eq(1)
expect(json_response.first['title']).to eq milestone.title
@@ -97,7 +97,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'returns a milestone by id' do
get api(resource_route, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(milestone.title)
expect(json_response['iid']).to eq(milestone.iid)
end
@@ -105,7 +105,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'returns a milestone by id' do
get api(resource_route, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(milestone.title)
expect(json_response['iid']).to eq(milestone.iid)
end
@@ -113,13 +113,13 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'returns 401 error if user not authenticated' do
get api(resource_route)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns a 404 error if milestone id not found' do
get api("#{route}/1234", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -127,7 +127,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'creates a new milestone' do
post api(route, user), title: 'new milestone'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('new milestone')
expect(json_response['description']).to be_nil
end
@@ -136,7 +136,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
post api(route, user),
title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['description']).to eq('release')
expect(json_response['due_date']).to eq('2013-03-02')
expect(json_response['start_date']).to eq('2013-02-02')
@@ -145,20 +145,20 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'returns a 400 error if title is missing' do
post api(route, user)
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'returns a 400 error if params are invalid (duplicate title)' do
post api(route, user),
title: milestone.title, description: 'release', due_date: '2013-03-02'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'creates a new milestone with reserved html characters' do
post api(route, user), title: 'foo & bar 1.1 -> 2.2'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2')
expect(json_response['description']).to be_nil
end
@@ -169,7 +169,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
put api(resource_route, user),
title: 'updated title'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('updated title')
end
@@ -178,7 +178,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
put api(resource_route, user), due_date: nil
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['due_date']).to be_nil
end
@@ -186,13 +186,13 @@ shared_examples_for 'group and project milestones' do |route_definition|
put api("#{route}/1234", user),
title: 'updated title'
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'closes milestone' do
put api(resource_route, user),
state_event: 'close'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['state']).to eq('closed')
end
@@ -207,7 +207,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'returns issues for a particular milestone' do
get api(issues_route, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['milestone']['title']).to eq(milestone.title)
@@ -228,14 +228,14 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'matches V4 response schema for a list of issues' do
get api(issues_route, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/issues')
end
it 'returns a 401 error if user not authenticated' do
get api(issues_route)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
describe 'confidential issues' do
@@ -265,7 +265,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'returns confidential issues to team members' do
get api(issues_route, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
# 2 for projects, 3 for group(which has another project with an issue)
@@ -279,7 +279,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
get api(issues_route, member)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
@@ -289,7 +289,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
it 'does not return confidential issues to regular users' do
get api(issues_route, create(:user))
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
@@ -302,7 +302,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
get api(issues_route, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
# 2 for projects, 3 for group(which has another project with an issue)
@@ -325,7 +325,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
another_merge_request
get api(merge_requests_route, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['title']).to eq(merge_request.title)
@@ -349,20 +349,20 @@ shared_examples_for 'group and project milestones' do |route_definition|
get api(not_found_route, user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 if the user has no access to the milestone' do
new_user = create :user
get api(merge_requests_route, new_user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 401 error if user not authenticated' do
get api(merge_requests_route)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
it 'returns merge_requests ordered by position asc' do
@@ -372,7 +372,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
get api(merge_requests_route, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb
index 57e28e040d7..06ae8792c61 100644
--- a/spec/support/api/scopes/read_user_shared_examples.rb
+++ b/spec/support/api/scopes/read_user_shared_examples.rb
@@ -6,7 +6,7 @@ shared_examples_for 'allows the "read_user" scope' do
it 'returns a "200" response' do
get api_call.call(path, user, personal_access_token: token)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -16,7 +16,7 @@ shared_examples_for 'allows the "read_user" scope' do
it 'returns a "200" response' do
get api_call.call(path, user, personal_access_token: token)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -27,10 +27,10 @@ shared_examples_for 'allows the "read_user" scope' do
stub_container_registry_config(enabled: true)
end
- it 'returns a "401" response' do
+ it 'returns a "403" response' do
get api_call.call(path, user, personal_access_token: token)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -44,7 +44,7 @@ shared_examples_for 'allows the "read_user" scope' do
it 'returns a "200" response' do
get api_call.call(path, user, oauth_access_token: token)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -54,7 +54,7 @@ shared_examples_for 'allows the "read_user" scope' do
it 'returns a "200" response' do
get api_call.call(path, user, oauth_access_token: token)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
end
@@ -64,7 +64,7 @@ shared_examples_for 'allows the "read_user" scope' do
it 'returns a "403" response' do
get api_call.call(path, user, oauth_access_token: token)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
@@ -74,10 +74,10 @@ shared_examples_for 'does not allow the "read_user" scope' do
context 'when the requesting token has the "read_user" scope' do
let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) }
- it 'returns a "401" response' do
+ it 'returns a "403" response' do
post api_call.call(path, user, personal_access_token: token), attributes_for(:user, projects_limit: 3)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(403)
end
end
end
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
index 16a3cf06be7..af1083f4bfd 100644
--- a/spec/support/api/time_tracking_shared_examples.rb
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -15,7 +15,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
it "sets the time estimate for #{issuable_name}" do
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '1w'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['human_time_estimate']).to eq('1w')
end
@@ -28,7 +28,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
it 'does not modify the original estimate' do
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: 'foo'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(issuable.reload.human_time_estimate).to eq('1w')
end
end
@@ -37,7 +37,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
it 'updates the estimate' do
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '3w1h'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(issuable.reload.human_time_estimate).to eq('3w 1h')
end
end
@@ -54,7 +54,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
it "resets the time estimate for #{issuable_name}" do
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['time_estimate']).to eq(0)
end
end
@@ -73,7 +73,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '2h'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['human_total_time_spent']).to eq('2h')
end
@@ -84,7 +84,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '-1h'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['total_time_spent']).to eq(3600)
end
end
@@ -96,7 +96,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '-1w'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/)
end
end
@@ -112,7 +112,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
it "resets spent time for #{issuable_name}" do
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['total_time_spent']).to eq(0)
end
end
@@ -124,7 +124,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_stats", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['total_time_spent']).to eq(1800)
expect(json_response['time_estimate']).to eq(3600)
end
diff --git a/spec/support/api/v3/time_tracking_shared_examples.rb b/spec/support/api/v3/time_tracking_shared_examples.rb
index f982b10d999..afe0f4cecda 100644
--- a/spec/support/api/v3/time_tracking_shared_examples.rb
+++ b/spec/support/api/v3/time_tracking_shared_examples.rb
@@ -11,7 +11,7 @@ shared_examples 'V3 time tracking endpoints' do |issuable_name|
it "sets the time estimate for #{issuable_name}" do
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['human_time_estimate']).to eq('1w')
end
@@ -24,7 +24,7 @@ shared_examples 'V3 time tracking endpoints' do |issuable_name|
it 'does not modify the original estimate' do
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(issuable.reload.human_time_estimate).to eq('1w')
end
end
@@ -33,7 +33,7 @@ shared_examples 'V3 time tracking endpoints' do |issuable_name|
it 'updates the estimate' do
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(issuable.reload.human_time_estimate).to eq('3w 1h')
end
end
@@ -50,7 +50,7 @@ shared_examples 'V3 time tracking endpoints' do |issuable_name|
it "resets the time estimate for #{issuable_name}" do
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['time_estimate']).to eq(0)
end
end
@@ -69,7 +69,7 @@ shared_examples 'V3 time tracking endpoints' do |issuable_name|
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
duration: '2h'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['human_total_time_spent']).to eq('2h')
end
@@ -80,7 +80,7 @@ shared_examples 'V3 time tracking endpoints' do |issuable_name|
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
duration: '-1h'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['total_time_spent']).to eq(3600)
end
end
@@ -92,7 +92,7 @@ shared_examples 'V3 time tracking endpoints' do |issuable_name|
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
duration: '-1w'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/)
end
end
@@ -108,7 +108,7 @@ shared_examples 'V3 time tracking endpoints' do |issuable_name|
it "resets spent time for #{issuable_name}" do
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['total_time_spent']).to eq(0)
end
end
@@ -120,7 +120,7 @@ shared_examples 'V3 time tracking endpoints' do |issuable_name|
get v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['total_time_spent']).to eq(1800)
expect(json_response['time_estimate']).to eq(3600)
end
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index 01aca74274c..ac0c7a9b493 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -18,21 +18,23 @@ module ApiHelpers
#
# Returns the relative path to the requested API resource
def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil)
- "/api/#{version}#{path}" +
+ full_path = "/api/#{version}#{path}"
- # Normalize query string
- (path.index('?') ? '' : '?') +
+ if oauth_access_token
+ query_string = "access_token=#{oauth_access_token.token}"
+ elsif personal_access_token
+ query_string = "private_token=#{personal_access_token.token}"
+ elsif user
+ personal_access_token = create(:personal_access_token, user: user)
+ query_string = "private_token=#{personal_access_token.token}"
+ end
- if personal_access_token.present?
- "&private_token=#{personal_access_token.token}"
- elsif oauth_access_token.present?
- "&access_token=#{oauth_access_token.token}"
- # Append private_token if given a User object
- elsif user.respond_to?(:private_token)
- "&private_token=#{user.private_token}"
- else
- ''
- end
+ if query_string
+ full_path << (path.index('?') ? '&' : '?')
+ full_path << query_string
+ end
+
+ full_path
end
# Temporary helper method for simplifying V3 exclusive API specs
diff --git a/spec/support/bare_repo_operations.rb b/spec/support/bare_repo_operations.rb
new file mode 100644
index 00000000000..38d11992dc2
--- /dev/null
+++ b/spec/support/bare_repo_operations.rb
@@ -0,0 +1,60 @@
+require 'zlib'
+
+class BareRepoOperations
+ # The ID of empty tree.
+ # See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
+ EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
+
+ include Gitlab::Popen
+
+ def initialize(path_to_repo)
+ @path_to_repo = path_to_repo
+ end
+
+ # Based on https://stackoverflow.com/a/25556917/1856239
+ def commit_file(file, dst_path, branch = 'master')
+ head_id = execute(['show', '--format=format:%H', '--no-patch', branch], allow_failure: true)[0] || EMPTY_TREE_ID
+
+ execute(['read-tree', '--empty'])
+ execute(['read-tree', head_id])
+
+ blob_id = execute(['hash-object', '--stdin', '-w']) do |stdin|
+ stdin.write(file.read)
+ end
+
+ execute(['update-index', '--add', '--cacheinfo', '100644', blob_id[0], dst_path])
+
+ tree_id = execute(['write-tree'])
+
+ commit_tree_args = ['commit-tree', tree_id[0], '-m', "Add #{dst_path}"]
+ commit_tree_args += ['-p', head_id] unless head_id == EMPTY_TREE_ID
+ commit_id = execute(commit_tree_args)
+
+ execute(['update-ref', "refs/heads/#{branch}", commit_id[0]])
+ end
+
+ private
+
+ def execute(args, allow_failure: false)
+ output, status = popen(base_args + args, nil) do |stdin|
+ yield stdin if block_given?
+ end
+
+ unless status.zero?
+ if allow_failure
+ return []
+ else
+ raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}"
+ end
+ end
+
+ output.split("\n")
+ end
+
+ def base_args
+ [
+ Gitlab.config.git.bin_path,
+ "--git-dir=#{@path_to_repo}"
+ ]
+ end
+end
diff --git a/spec/support/board_helpers.rb b/spec/support/board_helpers.rb
new file mode 100644
index 00000000000..507d0432d7f
--- /dev/null
+++ b/spec/support/board_helpers.rb
@@ -0,0 +1,16 @@
+module BoardHelpers
+ def click_card(card)
+ within card do
+ first('.card-number').click
+ end
+
+ wait_for_sidebar
+ end
+
+ def wait_for_sidebar
+ # loop until the CSS transition is complete
+ Timeout.timeout(0.5) do
+ loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
+ end
+ end
+end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index c45c4a4310d..9f672bc92fc 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -1,25 +1,25 @@
# rubocop:disable Style/GlobalVars
require 'capybara/rails'
require 'capybara/rspec'
-require 'capybara/poltergeist'
require 'capybara-screenshot/rspec'
+require 'selenium-webdriver'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
-Capybara.javascript_driver = :poltergeist
-Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(
- app,
- js_errors: true,
- timeout: timeout,
- window_size: [1366, 768],
- url_whitelist: %w[localhost 127.0.0.1],
- url_blacklist: %w[.mp4 .png .gif .avi .bmp .jpg .jpeg],
- phantomjs_options: [
- '--load-images=yes'
- ]
+Capybara.javascript_driver = :chrome
+Capybara.register_driver :chrome do |app|
+ extra_args = []
+ extra_args << 'headless' unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
+
+ capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
+ chromeOptions: {
+ 'args' => %w[no-sandbox disable-gpu --window-size=1240,1400] + extra_args
+ }
)
+
+ Capybara::Selenium::Driver
+ .new(app, browser: :chrome, desired_capabilities: capabilities)
end
Capybara.default_max_wait_time = timeout
@@ -27,6 +27,10 @@ Capybara.ignore_hidden_elements = true
# Keep only the screenshots generated from the last failing test suite
Capybara::Screenshot.prune_strategy = :keep_last_run
+# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326
+Capybara::Screenshot.register_driver(:chrome) do |driver, path|
+ driver.browser.save_screenshot(path)
+end
RSpec.configure do |config|
config.before(:context, :js) do
@@ -37,13 +41,23 @@ RSpec.configure do |config|
end
config.before(:example, :js) do
+ session = Capybara.current_session
+
allow(Gitlab::Application.routes).to receive(:default_url_options).and_return(
- host: Capybara.current_session.server.host,
- port: Capybara.current_session.server.port,
+ host: session.server.host,
+ port: session.server.port,
protocol: 'http')
+
+ # reset window size between tests
+ unless session.current_window.size == [1240, 1400]
+ session.current_window.resize_to(1240, 1400) rescue nil
+ end
end
config.after(:example, :js) do |example|
+ # prevent localstorage from introducing side effects based on test order
+ execute_script("localStorage.clear();")
+
# capybara/rspec already calls Capybara.reset_sessions! in an `after` hook,
# but `block_and_wait_for_requests_complete` is called before it so by
# calling it explicitely here, we prevent any new requests from being fired
diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb
index 3eb7bea3227..868233416bf 100644
--- a/spec/support/capybara_helpers.rb
+++ b/spec/support/capybara_helpers.rb
@@ -38,7 +38,7 @@ module CapybaraHelpers
# Simulate a browser restart by clearing the session cookie.
def clear_browser_session
- page.driver.remove_cookie('_gitlab_session')
+ page.driver.browser.manage.delete_cookie('_gitlab_session')
end
end
diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb
index b23d81a226a..a0839eefe6c 100644
--- a/spec/support/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -14,7 +14,7 @@ shared_examples 'a GitHub-ish import controller: POST personal_access_token' do
it "updates access token" do
token = 'asdfasdf9876'
- allow_any_instance_of(Gitlab::GithubImport::Client)
+ allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
.to receive(:user).and_return(true)
post :personal_access_token, personal_access_token: token
@@ -79,7 +79,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do
end
it "handles an invalid access token" do
- allow_any_instance_of(Gitlab::GithubImport::Client)
+ allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
.to receive(:repos).and_raise(Octokit::Unauthorized)
get :status
@@ -110,7 +110,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
context "when the repository owner is the provider user" do
context "when the provider user and GitLab user's usernames match" do
it "takes the current user's namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -122,7 +122,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
let(:provider_username) { "someone_else" }
it "takes the current user's namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -149,7 +149,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it "takes the existing namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -161,7 +161,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
it "creates a project using user's namespace" do
create(:user, username: other_username)
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -173,14 +173,14 @@ shared_examples 'a GitHub-ish import controller: POST create' do
context "when a namespace with the provider user's username doesn't exist" do
context "when current user can create namespaces" do
it "creates the namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).and_return(double(execute: true))
expect { post :create, target_namespace: provider_repo.name, format: :js }.to change(Namespace, :count).by(1)
end
it "takes the new namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider)
.and_return(double(execute: true))
@@ -194,14 +194,14 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it "doesn't create the namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).and_return(double(execute: true))
expect { post :create, format: :js }.not_to change(Namespace, :count)
end
it "takes the current user's namespace" do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -219,7 +219,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it 'takes the selected namespace and name' do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -227,7 +227,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it 'takes the selected name and default namespace' do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -245,7 +245,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it 'takes the selected namespace and name' do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider)
.and_return(double(execute: true))
@@ -257,7 +257,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
let(:test_name) { 'test_name' }
it 'takes the selected namespace and name' do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
.and_return(double(execute: true))
@@ -265,7 +265,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it 'creates the namespaces' do
- allow(Gitlab::GithubImport::ProjectCreator)
+ allow(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
.and_return(double(execute: true))
@@ -274,7 +274,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it 'new namespace has the right parent' do
- allow(Gitlab::GithubImport::ProjectCreator)
+ allow(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
.and_return(double(execute: true))
@@ -289,7 +289,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
it 'takes the selected namespace and name' do
- expect(Gitlab::GithubImport::ProjectCreator)
+ expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
.and_return(double(execute: true))
@@ -297,7 +297,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
it 'creates the namespaces' do
- allow(Gitlab::GithubImport::ProjectCreator)
+ allow(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
.and_return(double(execute: true))
diff --git a/spec/support/cookie_helper.rb b/spec/support/cookie_helper.rb
new file mode 100644
index 00000000000..224619c899c
--- /dev/null
+++ b/spec/support/cookie_helper.rb
@@ -0,0 +1,17 @@
+# Helper for setting cookies in Selenium/WebDriver
+#
+module CookieHelper
+ def set_cookie(name, value, options = {})
+ # Selenium driver will not set cookies for a given domain when the browser is at `about:blank`.
+ # It also doesn't appear to allow overriding the cookie path. loading `/` is the most inclusive.
+ visit options.fetch(:path, '/') unless on_a_page?
+ page.driver.browser.manage.add_cookie(name: name, value: value, **options)
+ end
+
+ private
+
+ def on_a_page?
+ current_url = Capybara.current_session.driver.browser.current_url
+ current_url && current_url != '' && current_url != 'about:blank' && current_url != 'data:,'
+ end
+end
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index 934b4557ba2..26fd271ce31 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -94,6 +94,7 @@ module CycleAnalyticsHelpers
ref: 'master',
tag: false,
name: 'dummy',
+ stage: 'dummy',
pipeline: dummy_pipeline,
protected: false)
end
diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb
index 3e979f2f470..b39052923dd 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/email_helpers.rb
@@ -1,6 +1,6 @@
module EmailHelpers
- def sent_to_user?(user, recipients = email_recipients)
- recipients.include?(user.notification_email)
+ def sent_to_user(user, recipients: email_recipients)
+ recipients.count { |to| to == user.notification_email }
end
def reset_delivered_emails!
@@ -10,17 +10,17 @@ module EmailHelpers
def should_only_email(*users, kind: :to)
recipients = email_recipients(kind: kind)
- users.each { |user| should_email(user, recipients) }
+ users.each { |user| should_email(user, recipients: recipients) }
expect(recipients.count).to eq(users.count)
end
- def should_email(user, recipients = email_recipients)
- expect(sent_to_user?(user, recipients)).to be_truthy
+ def should_email(user, times: 1, recipients: email_recipients)
+ expect(sent_to_user(user, recipients: recipients)).to eq(times)
end
- def should_not_email(user, recipients = email_recipients)
- expect(sent_to_user?(user, recipients)).to be_falsey
+ def should_not_email(user, recipients: email_recipients)
+ should_email(user, times: 0, recipients: recipients)
end
def should_not_email_anyone
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index 9f05cabf7ae..c24940393f9 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -71,26 +71,28 @@ shared_examples 'discussion comments' do |resource_name|
expect(page).not_to have_selector menu_selector
find(toggle_selector).click
- find('body').trigger 'click'
+ execute_script("document.querySelector('body').click()")
expect(page).not_to have_selector menu_selector
end
it 'clicking the ul padding or divider should not change the text' do
- find(menu_selector).trigger 'click'
+ execute_script("document.querySelector('#{menu_selector}').click()")
+ # on issues page, the menu closes when clicking anywhere, on other pages it will
+ # remain open if clicking divider or menu padding, but should not change button action
if resource_name == 'issue'
expect(find(dropdown_selector)).to have_content 'Comment'
find(toggle_selector).click
- find("#{menu_selector} .divider").trigger 'click'
+ execute_script("document.querySelector('#{menu_selector} .divider').click()")
else
- find(menu_selector).trigger 'click'
+ execute_script("document.querySelector('#{menu_selector}').click()")
expect(page).to have_selector menu_selector
expect(find(dropdown_selector)).to have_content 'Comment'
- find("#{menu_selector} .divider").trigger 'click'
+ execute_script("document.querySelector('#{menu_selector} .divider').click()")
expect(page).to have_selector menu_selector
end
@@ -105,7 +107,12 @@ shared_examples 'discussion comments' do |resource_name|
end
it 'updates the submit button text and closes the dropdown' do
- expect(find(dropdown_selector)).to have_content 'Start discussion'
+ # on issues page, the submit input is a <button>, on other pages it is <input>
+ if resource_name == 'issue'
+ expect(find(submit_selector)).to have_content 'Start discussion'
+ else
+ expect(find(submit_selector).value).to eq 'Start discussion'
+ end
expect(page).not_to have_selector menu_selector
end
@@ -121,14 +128,31 @@ shared_examples 'discussion comments' do |resource_name|
end
end
- it 'clicking "Start discussion" will post a discussion' do
- find(submit_selector).click
+ describe 'creating a discussion' do
+ before do
+ find(submit_selector).click
+ find(comments_selector, match: :first)
+ end
+
+ it 'clicking "Start discussion" will post a discussion' do
+ new_comment = all(comments_selector).last
+
+ expect(new_comment).to have_content 'a'
+ expect(new_comment).to have_selector '.discussion'
+ end
+
+ if resource_name == 'merge request'
+ it 'shows resolved discussion when toggled' do
+ click_button "Resolve discussion"
+
+ expect(page).to have_selector('.note-row-1', visible: true)
- find(comments_selector, match: :first)
- new_comment = all(comments_selector).last
+ refresh
+ click_button "Toggle discussion"
- expect(new_comment).to have_content 'a'
- expect(new_comment).to have_selector '.discussion'
+ expect(page).to have_selector('.note-row-1', visible: true)
+ end
+ end
end
if resource_name == 'issue'
@@ -170,7 +194,12 @@ shared_examples 'discussion comments' do |resource_name|
end
it 'updates the submit button text and closes the dropdown' do
- expect(find(dropdown_selector)).to have_content 'Comment'
+ # on issues page, the submit input is a <button>, on other pages it is <input>
+ if resource_name == 'issue'
+ expect(find(submit_selector)).to have_content 'Comment'
+ else
+ expect(find(submit_selector).value).to eq 'Comment'
+ end
expect(page).not_to have_selector menu_selector
end
@@ -209,6 +238,7 @@ shared_examples 'discussion comments' do |resource_name|
describe "on a closed #{resource_name}" do
before do
find("#{form_selector} .js-note-target-close").click
+ wait_for_requests
find("#{form_selector} .note-textarea").send_keys('a')
end
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 8282ba7e536..08e21ee2537 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -29,7 +29,7 @@ shared_examples 'issuable record that supports quick actions in its description
wait_for_requests
end
- describe "new #{issuable_type}", js: true do
+ describe "new #{issuable_type}", :js do
context 'with commands in the description' do
it "creates the #{issuable_type} and interpret commands accordingly" do
case issuable_type
@@ -53,7 +53,7 @@ shared_examples 'issuable record that supports quick actions in its description
end
end
- describe "note on #{issuable_type}", js: true do
+ describe "note on #{issuable_type}", :js do
before do
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
end
@@ -61,7 +61,7 @@ shared_examples 'issuable record that supports quick actions in its description
context 'with a note containing commands' do
it 'creates a note without the commands and interpret the commands accordingly' do
assignee = create(:user, username: 'bob')
- write_note("Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
+ write_note("Awesome!\n\n/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
expect(page).to have_content 'Awesome!'
expect(page).not_to have_content '/assign @bob'
@@ -82,7 +82,7 @@ shared_examples 'issuable record that supports quick actions in its description
context 'with a note containing only commands' do
it 'does not create a note but interpret the commands accordingly' do
assignee = create(:user, username: 'bob')
- write_note("/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
+ write_note("/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
expect(page).not_to have_content '/assign @bob'
expect(page).not_to have_content '/label ~bug'
@@ -290,7 +290,7 @@ shared_examples 'issuable record that supports quick actions in its description
end
end
- describe "preview of note on #{issuable_type}", js: true do
+ describe "preview of note on #{issuable_type}", :js do
it 'removes quick actions from note and explains them' do
create(:user, username: 'bob')
diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb
index 192a2fed0a8..836e5e7be23 100644
--- a/spec/support/features/reportable_note_shared_examples.rb
+++ b/spec/support/features/reportable_note_shared_examples.rb
@@ -39,7 +39,7 @@ shared_examples 'reportable note' do |type|
end
def open_dropdown(dropdown)
- dropdown.find('.more-actions-toggle').trigger('click')
+ dropdown.find('.more-actions-toggle').click
dropdown.find('.dropdown-menu li', match: :first)
end
end
diff --git a/spec/support/fixture_helpers.rb b/spec/support/fixture_helpers.rb
index 5515c355cea..128aaaf25fe 100644
--- a/spec/support/fixture_helpers.rb
+++ b/spec/support/fixture_helpers.rb
@@ -1,6 +1,7 @@
module FixtureHelpers
def fixture_file(filename)
return '' if filename.blank?
+
File.read(expand_fixture_path(filename))
end
diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb
index ef3c8e7087f..4ee33f9725b 100755
--- a/spec/support/generate-seed-repo-rb
+++ b/spec/support/generate-seed-repo-rb
@@ -33,6 +33,7 @@ end
def capture!(cmd, dir)
output = IO.popen(cmd, 'r', chdir: dir) { |io| io.read }
raise "command failed with #{$?}: #{cmd.join(' ')}" unless $?.success?
+
output.chomp
end
diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb
index 89fb362cf14..c7e8a39a617 100644
--- a/spec/support/gitaly.rb
+++ b/spec/support/gitaly.rb
@@ -1,6 +1,11 @@
RSpec.configure do |config|
config.before(:each) do |example|
- next if example.metadata[:skip_gitaly_mock]
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+ if example.metadata[:disable_gitaly]
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false)
+ else
+ next if example.metadata[:skip_gitaly_mock]
+
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+ end
end
end
diff --git a/spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535 b/spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535
new file mode 100644
index 00000000000..1c47f34b9a5
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535
Binary files differ
diff --git a/spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cf b/spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cf
new file mode 100644
index 00000000000..ca13c8df66a
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cf
Binary files differ
diff --git a/spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb16 b/spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb16
new file mode 100644
index 00000000000..3be244dbda4
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb16
@@ -0,0 +1,2 @@
+x¥ŽK
+Â0EgNIÒ|ADtè*^’ mZ qGîÄY×àð8—×ZK©ý®7"ÈFc’Ò%oH¢D²Ü9rZÛLÎs“MJ2Œ™=±ÑÒAå…CmeFg²·V¨xI9øH2†¯þXÜJ…ár»pÅ6‡Ï;NÔà8•zˆ??>ß+–ù×z¡¹WÆBÞ ÎÙf·Ç}«þßb¡N@K\SYîì •iSC \ No newline at end of file
diff --git a/spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31e b/spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31e
new file mode 100644
index 00000000000..2bf27fe5048
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31e
Binary files differ
diff --git a/spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3f b/spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3f
new file mode 100644
index 00000000000..8ab8606c6be
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3f
Binary files differ
diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json
index 688175369ae..658ff5871b0 100644
--- a/spec/support/gitlab_stubs/session.json
+++ b/spec/support/gitlab_stubs/session.json
@@ -14,7 +14,5 @@
"provider":null,
"is_admin":false,
"can_create_group":false,
- "can_create_project":false,
- "private_token":"Wvjy2Krpb7y8xi93owUz",
- "access_token":"Wvjy2Krpb7y8xi93owUz"
+ "can_create_project":false
}
diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json
index ce8dfe5ae75..658ff5871b0 100644
--- a/spec/support/gitlab_stubs/user.json
+++ b/spec/support/gitlab_stubs/user.json
@@ -14,7 +14,5 @@
"provider":null,
"is_admin":false,
"can_create_group":false,
- "can_create_project":false,
- "private_token":"Wvjy2Krpb7y8xi93owUz",
- "access_token":"Wvjy2Krpb7y8xi93owUz"
-} \ No newline at end of file
+ "can_create_project":false
+}
diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb
new file mode 100644
index 00000000000..dabf0db7666
--- /dev/null
+++ b/spec/support/google_api/cloud_platform_helpers.rb
@@ -0,0 +1,119 @@
+module GoogleApi
+ module CloudPlatformHelpers
+ def stub_google_api_validate_token
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_token] = 'token'
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = 1.hour.since.to_i.to_s
+ end
+
+ def stub_google_api_expired_token
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_token] = 'token'
+ request.session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = 1.hour.ago.to_i.to_s
+ end
+
+ def stub_cloud_platform_get_zone_cluster(project_id, zone, cluster_id, **options)
+ WebMock.stub_request(:get, cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id))
+ .to_return(cloud_platform_response(cloud_platform_cluster_body(options)))
+ end
+
+ def stub_cloud_platform_get_zone_cluster_error(project_id, zone, cluster_id)
+ WebMock.stub_request(:get, cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id))
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def stub_cloud_platform_create_cluster(project_id, zone, **options)
+ WebMock.stub_request(:post, cloud_platform_create_cluster_url(project_id, zone))
+ .to_return(cloud_platform_response(cloud_platform_operation_body(options)))
+ end
+
+ def stub_cloud_platform_create_cluster_error(project_id, zone)
+ WebMock.stub_request(:post, cloud_platform_create_cluster_url(project_id, zone))
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def stub_cloud_platform_get_zone_operation(project_id, zone, operation_id, **options)
+ WebMock.stub_request(:get, cloud_platform_get_zone_operation_url(project_id, zone, operation_id))
+ .to_return(cloud_platform_response(cloud_platform_operation_body(options)))
+ end
+
+ def stub_cloud_platform_get_zone_operation_error(project_id, zone, operation_id)
+ WebMock.stub_request(:get, cloud_platform_get_zone_operation_url(project_id, zone, operation_id))
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id)
+ "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/clusters/#{cluster_id}"
+ end
+
+ def cloud_platform_create_cluster_url(project_id, zone)
+ "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/clusters"
+ end
+
+ def cloud_platform_get_zone_operation_url(project_id, zone, operation_id)
+ "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/operations/#{operation_id}"
+ end
+
+ def cloud_platform_response(body)
+ { status: 200, headers: { 'Content-Type' => 'application/json' }, body: body.to_json }
+ end
+
+ def load_sample_cert
+ pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))
+ Base64.encode64(File.read(pem_file))
+ end
+
+ ##
+ # gcloud container clusters create
+ # https://cloud.google.com/container-engine/reference/rest/v1/projects.zones.clusters/create
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def cloud_platform_cluster_body(**options)
+ {
+ "name": options[:name] || 'string',
+ "description": options[:description] || 'string',
+ "initialNodeCount": options[:initialNodeCount] || 'number',
+ "masterAuth": {
+ "username": options[:username] || 'string',
+ "password": options[:password] || 'string',
+ "clusterCaCertificate": options[:clusterCaCertificate] || load_sample_cert,
+ "clientCertificate": options[:clientCertificate] || 'string',
+ "clientKey": options[:clientKey] || 'string'
+ },
+ "loggingService": options[:loggingService] || 'string',
+ "monitoringService": options[:monitoringService] || 'string',
+ "network": options[:network] || 'string',
+ "clusterIpv4Cidr": options[:clusterIpv4Cidr] || 'string',
+ "subnetwork": options[:subnetwork] || 'string',
+ "enableKubernetesAlpha": options[:enableKubernetesAlpha] || 'boolean',
+ "labelFingerprint": options[:labelFingerprint] || 'string',
+ "selfLink": options[:selfLink] || 'string',
+ "zone": options[:zone] || 'string',
+ "endpoint": options[:endpoint] || 'string',
+ "initialClusterVersion": options[:initialClusterVersion] || 'string',
+ "currentMasterVersion": options[:currentMasterVersion] || 'string',
+ "currentNodeVersion": options[:currentNodeVersion] || 'string',
+ "createTime": options[:createTime] || 'string',
+ "status": options[:status] || 'RUNNING',
+ "statusMessage": options[:statusMessage] || 'string',
+ "nodeIpv4CidrSize": options[:nodeIpv4CidrSize] || 'number',
+ "servicesIpv4Cidr": options[:servicesIpv4Cidr] || 'string',
+ "currentNodeCount": options[:currentNodeCount] || 'number',
+ "expireTime": options[:expireTime] || 'string'
+ }
+ end
+
+ def cloud_platform_operation_body(**options)
+ {
+ "name": options[:name] || 'operation-1234567891234-1234567',
+ "zone": options[:zone] || 'us-central1-a',
+ "operationType": options[:operationType] || 'CREATE_CLUSTER',
+ "status": options[:status] || 'PENDING',
+ "detail": options[:detail] || 'detail',
+ "statusMessage": options[:statusMessage] || '',
+ "selfLink": options[:selfLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/operations/operation-1234567891234-1234567',
+ "targetLink": options[:targetLink] || 'https://container.googleapis.com/v1/projects/123456789101/zones/us-central1-a/clusters/test-cluster',
+ "startTime": options[:startTime] || '2017-09-13T16:49:13.055601589Z',
+ "endTime": options[:endTime] || ''
+ }
+ end
+ end
+end
diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb
index fd22e384b1b..c98aa503ed1 100644
--- a/spec/support/helpers/merge_request_diff_helpers.rb
+++ b/spec/support/helpers/merge_request_diff_helpers.rb
@@ -2,7 +2,7 @@ module MergeRequestDiffHelpers
def click_diff_line(line_holder, diff_side = nil)
line = get_line_components(line_holder, diff_side)
line[:content].hover
- line[:num].find('.add-diff-note').trigger('click')
+ line[:num].find('.add-diff-note', visible: false).send_keys(:return)
end
def get_line_components(line_holder, diff_side = nil)
diff --git a/spec/support/helpers/note_interaction_helpers.rb b/spec/support/helpers/note_interaction_helpers.rb
index 86008698692..79a0aa174b1 100644
--- a/spec/support/helpers/note_interaction_helpers.rb
+++ b/spec/support/helpers/note_interaction_helpers.rb
@@ -2,7 +2,7 @@ module NoteInteractionHelpers
def open_more_actions_dropdown(note)
note_element = find("#note_#{note.id}")
- note_element.find('.more-actions-toggle').trigger('click')
+ note_element.find('.more-actions-toggle').click
note_element.find('.more-actions .dropdown-menu li', match: :first)
end
end
diff --git a/spec/support/input_helper.rb b/spec/support/input_helper.rb
new file mode 100644
index 00000000000..acbb42274ec
--- /dev/null
+++ b/spec/support/input_helper.rb
@@ -0,0 +1,7 @@
+# see app/assets/javascripts/test_utils/simulate_input.js
+
+module InputHelper
+ def simulate_input(selector, input = '')
+ evaluate_script("window.simulateInput(#{selector.to_json}, #{input.to_json});")
+ end
+end
diff --git a/spec/support/inspect_requests.rb b/spec/support/inspect_requests.rb
new file mode 100644
index 00000000000..88ddc5c7f6c
--- /dev/null
+++ b/spec/support/inspect_requests.rb
@@ -0,0 +1,17 @@
+require_relative './wait_for_requests'
+
+module InspectRequests
+ extend self
+ include WaitForRequests
+
+ def inspect_requests(inject_headers: {})
+ Gitlab::Testing::RequestInspectorMiddleware.log_requests!(inject_headers)
+
+ yield
+
+ wait_for_all_requests
+ Gitlab::Testing::RequestInspectorMiddleware.requests
+ ensure
+ Gitlab::Testing::RequestInspectorMiddleware.stop_logging!
+ end
+end
diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb
index 0b5f66597fd..88a7aeba461 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/jira_service_helper.rb
@@ -6,6 +6,8 @@ module JiraServiceHelper
properties = {
title: "JIRA tracker",
url: JIRA_URL,
+ username: 'jira-user',
+ password: 'my-secret-password',
project_key: "JIRA",
jira_issue_transition_id: '1'
}
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
index c92f78b324c..e46b61b6461 100644
--- a/spec/support/kubernetes_helpers.rb
+++ b/spec/support/kubernetes_helpers.rb
@@ -9,22 +9,51 @@ module KubernetesHelpers
kube_response(kube_pods_body)
end
- def stub_kubeclient_discover
- WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
+ def stub_kubeclient_discover(api_url)
+ WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
end
def stub_kubeclient_pods(response = nil)
- stub_kubeclient_discover
+ stub_kubeclient_discover(service.api_url)
pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods"
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
+ def stub_kubeclient_get_secrets(api_url, **options)
+ WebMock.stub_request(:get, api_url + '/api/v1/secrets')
+ .to_return(kube_response(kube_v1_secrets_body(options)))
+ end
+
+ def stub_kubeclient_get_secrets_error(api_url)
+ WebMock.stub_request(:get, api_url + '/api/v1/secrets')
+ .to_return(status: [404, "Internal Server Error"])
+ end
+
+ def kube_v1_secrets_body(**options)
+ {
+ "kind" => "SecretList",
+ "apiVersion": "v1",
+ "items" => [
+ {
+ "metadata": {
+ "name": options[:metadata_name] || "default-token-1",
+ "namespace": "kube-system"
+ },
+ "data": {
+ "token": options[:token] || Base64.encode64('token-sample-123')
+ }
+ }
+ ]
+ }
+ end
+
def kube_v1_discovery_body
{
"kind" => "APIResourceList",
"resources" => [
- { "name" => "pods", "namespaced" => true, "kind" => "Pod" }
+ { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
]
}
end
diff --git a/spec/support/ldap_helpers.rb b/spec/support/ldap_helpers.rb
index 079f244475c..28d39a32f02 100644
--- a/spec/support/ldap_helpers.rb
+++ b/spec/support/ldap_helpers.rb
@@ -15,10 +15,7 @@ module LdapHelpers
# admin_group: 'my-admin-group'
# )
def stub_ldap_config(messages)
- messages.each do |config, value|
- allow_any_instance_of(::Gitlab::LDAP::Config)
- .to receive(config.to_sym).and_return(value)
- end
+ allow_any_instance_of(::Gitlab::LDAP::Config).to receive_messages(messages)
end
# Stub an LDAP person search and provide the return entry. Specify `nil` for
diff --git a/spec/support/legacy_path_redirect_shared_examples.rb b/spec/support/legacy_path_redirect_shared_examples.rb
new file mode 100644
index 00000000000..f300bdd48b1
--- /dev/null
+++ b/spec/support/legacy_path_redirect_shared_examples.rb
@@ -0,0 +1,13 @@
+shared_examples 'redirecting a legacy path' do |source, target|
+ include RSpec::Rails::RequestExampleGroup
+
+ it "redirects #{source} to #{target} when the resource does not exist" do
+ expect(get(source)).to redirect_to(target)
+ end
+
+ it "does not redirect #{source} to #{target} when the resource exists" do
+ resource
+
+ expect(get(source)).not_to redirect_to(target)
+ end
+end
diff --git a/spec/support/live_debugger.rb b/spec/support/live_debugger.rb
new file mode 100644
index 00000000000..911eb48a8ca
--- /dev/null
+++ b/spec/support/live_debugger.rb
@@ -0,0 +1,17 @@
+require 'io/console'
+
+module LiveDebugger
+ def live_debug
+ puts
+ puts "Current example is paused for live debugging."
+ puts "Opening #{current_url} in your default browser..."
+ puts "The current user credentials are: #{@current_user.username} / #{@current_user.password}" if @current_user
+ puts "Press any key to resume the execution of the example!!"
+
+ `open #{current_url}`
+
+ loop until $stdin.getch
+
+ puts "Back to the example!"
+ end
+end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index 3e117530151..50702a0ac88 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -3,6 +3,21 @@ require_relative 'devise_helpers'
module LoginHelpers
include DeviseHelpers
+ # Overriding Devise::Test::IntegrationHelpers#sign_in to store @current_user
+ # since we may need it in LiveDebugger#live_debug.
+ def sign_in(resource, scope: nil)
+ super
+
+ @current_user = resource
+ end
+
+ # Overriding Devise::Test::IntegrationHelpers#sign_out to clear @current_user.
+ def sign_out(resource_or_scope)
+ super
+
+ @current_user = nil
+ end
+
# Internal: Log in as a specific user or a new user of a specific role
#
# user_or_role - User object, or a role to create (e.g., :admin, :user)
@@ -28,7 +43,7 @@ module LoginHelpers
gitlab_sign_in_with(user, **kwargs)
- user
+ @current_user = user
end
def gitlab_sign_in_via(provider, user, uid)
@@ -41,6 +56,7 @@ module LoginHelpers
def gitlab_sign_out
find(".header-user-dropdown-toggle").click
click_link "Sign out"
+ @current_user = nil
expect(page).to have_button('Sign in')
end
@@ -120,4 +136,16 @@ module LoginHelpers
allow_any_instance_of(Object).to receive(:user_saml_omniauth_authorize_path).and_return('/users/auth/saml')
allow_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml')
end
+
+ def stub_omniauth_config(messages)
+ allow(Gitlab.config.omniauth).to receive_messages(messages)
+ end
+
+ def stub_basic_saml_config
+ allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } })
+ end
+
+ def stub_saml_group_config(groups)
+ allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
+ end
end
diff --git a/spec/support/matchers/access_matchers_for_controller.rb b/spec/support/matchers/access_matchers_for_controller.rb
index bb6b7c63ee9..cdb62a5deee 100644
--- a/spec/support/matchers/access_matchers_for_controller.rb
+++ b/spec/support/matchers/access_matchers_for_controller.rb
@@ -5,7 +5,7 @@ module AccessMatchersForController
extend RSpec::Matchers::DSL
include Warden::Test::Helpers
- EXPECTED_STATUS_CODE_ALLOWED = [200, 201, 302].freeze
+ EXPECTED_STATUS_CODE_ALLOWED = [200, 201, 204, 302].freeze
EXPECTED_STATUS_CODE_DENIED = [401, 404].freeze
def emulate_user(role, membership = nil)
diff --git a/spec/support/matchers/be_a_binary_string.rb b/spec/support/matchers/be_a_binary_string.rb
new file mode 100644
index 00000000000..f041ae76167
--- /dev/null
+++ b/spec/support/matchers/be_a_binary_string.rb
@@ -0,0 +1,9 @@
+RSpec::Matchers.define :be_a_binary_string do |_|
+ match do |actual|
+ actual.is_a?(String) && actual.encoding == Encoding.find('ASCII-8BIT')
+ end
+
+ description do
+ "be a String with binary encoding"
+ end
+end
diff --git a/spec/support/matchers/have_gitlab_http_status.rb b/spec/support/matchers/have_gitlab_http_status.rb
index 3198f1b9edd..e7e418cdde4 100644
--- a/spec/support/matchers/have_gitlab_http_status.rb
+++ b/spec/support/matchers/have_gitlab_http_status.rb
@@ -8,7 +8,11 @@ RSpec::Matchers.define :have_gitlab_http_status do |expected|
end
failure_message do |actual|
+ # actual can be either an ActionDispatch::TestResponse (which uses #response_code)
+ # or a Capybara::Session (which uses #status_code)
+ response_code = actual.try(:response_code) || actual.try(:status_code)
+
"expected the response to have status code #{expected.inspect}" \
- " but it was #{actual.response_code}. The response was: #{actual.body}"
+ " but it was #{response_code}. The response was: #{actual.body}"
end
end
diff --git a/spec/support/matchers/security_header_matcher.rb b/spec/support/matchers/security_header_matcher.rb
new file mode 100644
index 00000000000..f8518d13ebb
--- /dev/null
+++ b/spec/support/matchers/security_header_matcher.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :include_security_headers do |expected|
+ match do |actual|
+ expect(actual.headers).to include('X-Content-Type-Options')
+ end
+end
diff --git a/spec/support/mobile_helpers.rb b/spec/support/mobile_helpers.rb
index 431f20a2a5c..3b9eb84e824 100644
--- a/spec/support/mobile_helpers.rb
+++ b/spec/support/mobile_helpers.rb
@@ -12,6 +12,6 @@ module MobileHelpers
end
def resize_window(width, height)
- page.driver.resize_window width, height
+ Capybara.current_session.current_window.resize_to(width, height)
end
end
diff --git a/spec/support/project_forks_helper.rb b/spec/support/project_forks_helper.rb
index 0d1c6792d13..d6680735aa1 100644
--- a/spec/support/project_forks_helper.rb
+++ b/spec/support/project_forks_helper.rb
@@ -52,7 +52,7 @@ module ProjectForksHelper
TestEnv.copy_repo(forked_project,
bare_repo: TestEnv.forked_repo_path_bare,
refs: TestEnv::FORKED_BRANCH_SHA)
-
+ forked_project.repository.after_import
forked_project
end
end
diff --git a/spec/support/project_hook_data_shared_example.rb b/spec/support/project_hook_data_shared_example.rb
deleted file mode 100644
index 1eb405d4be8..00000000000
--- a/spec/support/project_hook_data_shared_example.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-RSpec.shared_examples 'project hook data with deprecateds' do |project_key: :project|
- it 'contains project data' do
- expect(data[project_key][:name]).to eq(project.name)
- expect(data[project_key][:description]).to eq(project.description)
- expect(data[project_key][:web_url]).to eq(project.web_url)
- expect(data[project_key][:avatar_url]).to eq(project.avatar_url)
- expect(data[project_key][:git_http_url]).to eq(project.http_url_to_repo)
- expect(data[project_key][:git_ssh_url]).to eq(project.ssh_url_to_repo)
- expect(data[project_key][:namespace]).to eq(project.namespace.name)
- expect(data[project_key][:visibility_level]).to eq(project.visibility_level)
- expect(data[project_key][:path_with_namespace]).to eq(project.full_path)
- expect(data[project_key][:default_branch]).to eq(project.default_branch)
- expect(data[project_key][:homepage]).to eq(project.web_url)
- expect(data[project_key][:url]).to eq(project.url_to_repo)
- expect(data[project_key][:ssh_url]).to eq(project.ssh_url_to_repo)
- expect(data[project_key][:http_url]).to eq(project.http_url_to_repo)
- end
-end
-
-RSpec.shared_examples 'project hook data' do |project_key: :project|
- it 'contains project data' do
- expect(data[project_key][:name]).to eq(project.name)
- expect(data[project_key][:description]).to eq(project.description)
- expect(data[project_key][:web_url]).to eq(project.web_url)
- expect(data[project_key][:avatar_url]).to eq(project.avatar_url)
- expect(data[project_key][:git_http_url]).to eq(project.http_url_to_repo)
- expect(data[project_key][:git_ssh_url]).to eq(project.ssh_url_to_repo)
- expect(data[project_key][:namespace]).to eq(project.namespace.name)
- expect(data[project_key][:visibility_level]).to eq(project.visibility_level)
- expect(data[project_key][:path_with_namespace]).to eq(project.full_path)
- expect(data[project_key][:default_branch]).to eq(project.default_branch)
- end
-end
-
-RSpec.shared_examples 'deprecated repository hook data' do |project_key: :project|
- it 'contains deprecated repository data' do
- expect(data[:repository][:name]).to eq(project.name)
- expect(data[:repository][:description]).to eq(project.description)
- expect(data[:repository][:url]).to eq(project.url_to_repo)
- expect(data[:repository][:homepage]).to eq(project.web_url)
- end
-end
diff --git a/spec/support/prometheus/additional_metrics_shared_examples.rb b/spec/support/prometheus/additional_metrics_shared_examples.rb
index 620fa37d455..dbbd4ad4d40 100644
--- a/spec/support/prometheus/additional_metrics_shared_examples.rb
+++ b/spec/support/prometheus/additional_metrics_shared_examples.rb
@@ -41,16 +41,30 @@ RSpec.shared_examples 'additional metrics query' do
end
describe 'project has Kubernetes service' do
- let(:project) { create(:kubernetes_project) }
- let(:environment) { create(:environment, slug: 'environment-slug', project: project) }
- let(:kube_namespace) { project.kubernetes_service.actual_namespace }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ let(:environment) { create(:environment, slug: 'environment-slug', project: project) }
+ let(:kube_namespace) { project.deployment_platform.actual_namespace }
- it_behaves_like 'query context containing environment slug and filter'
+ it_behaves_like 'query context containing environment slug and filter'
- it 'query context contains kube_namespace' do
- expect(subject).to receive(:query_metrics).with(hash_including(kube_namespace: kube_namespace))
+ it 'query context contains kube_namespace' do
+ expect(subject).to receive(:query_metrics).with(hash_including(kube_namespace: kube_namespace))
- subject.query(*query_params)
+ subject.query(*query_params)
+ end
+ end
+
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/support/protected_tags/access_control_ce_shared_examples.rb b/spec/support/protected_tags/access_control_ce_shared_examples.rb
index 421a51fc336..71eec9f3217 100644
--- a/spec/support/protected_tags/access_control_ce_shared_examples.rb
+++ b/spec/support/protected_tags/access_control_ce_shared_examples.rb
@@ -1,5 +1,5 @@
RSpec.shared_examples "protected tags > access control > CE" do
- ProtectedTag::CreateAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+ ProtectedRefAccess::HUMAN_ACCESS_LEVELS.each do |(access_type_id, access_type_name)|
it "allows creating protected tags that #{access_type_name} can create" do
visit project_protected_tags_path(project)
@@ -9,7 +9,7 @@ RSpec.shared_examples "protected tags > access control > CE" do
allowed_to_create_button = find(".js-allowed-to-create")
unless allowed_to_create_button.text == access_type_name
- allowed_to_create_button.trigger('click')
+ allowed_to_create_button.click
find('.create_access_levels-container .dropdown-menu li', match: :first)
within('.create_access_levels-container .dropdown-menu') { click_on access_type_name }
end
diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb
index ba0b805caad..369775db462 100644
--- a/spec/support/query_recorder.rb
+++ b/spec/support/query_recorder.rb
@@ -8,7 +8,14 @@ module ActiveRecord
ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block)
end
+ def show_backtrace(values)
+ Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}")
+ caller.each { |line| Rails.logger.debug(" --> #{line}") }
+ end
+
def callback(name, start, finish, message_id, values)
+ show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG']
+
if values[:name]&.include?("CACHE")
@cached << values[:sql]
elsif !values[:name]&.include?("SCHEMA")
@@ -69,10 +76,17 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
@recorder.count
end
+ def count_queries(queries)
+ queries.each_with_object(Hash.new(0)) { |query, counts| counts[query] += 1 }
+ end
+
def log_message
if expected.is_a?(ActiveRecord::QueryRecorder)
- extra_queries = (expected.log - @recorder.log).join("\n\n")
- "Extra queries: \n\n #{extra_queries}"
+ counts = count_queries(expected.log)
+ extra_queries = @recorder.log.reject { |query| counts[query] -= 1 unless counts[query].zero? }
+ extra_queries_display = count_queries(extra_queries).map { |query, count| "[#{count}] #{query}" }
+
+ (['Extra queries:'] + extra_queries_display).join("\n\n")
else
@recorder.log_message
end
diff --git a/spec/support/quick_actions_helpers.rb b/spec/support/quick_actions_helpers.rb
index d2aaae7518f..361190aa352 100644
--- a/spec/support/quick_actions_helpers.rb
+++ b/spec/support/quick_actions_helpers.rb
@@ -3,7 +3,7 @@ module QuickActionsHelpers
Sidekiq::Testing.fake! do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: text
- find('.js-comment-submit-button').trigger('click')
+ find('.js-comment-submit-button').click
end
end
end
diff --git a/spec/support/redis_without_keys.rb b/spec/support/redis_without_keys.rb
new file mode 100644
index 00000000000..6220167dee6
--- /dev/null
+++ b/spec/support/redis_without_keys.rb
@@ -0,0 +1,8 @@
+class Redis
+ ForbiddenCommand = Class.new(StandardError)
+
+ def keys(*args)
+ raise ForbiddenCommand.new("Don't use `Redis#keys` as it iterates over all "\
+ "keys in redis. Use `Redis#scan_each` instead.")
+ end
+end
diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb
index 6b1853c2364..55da961e173 100644
--- a/spec/support/select2_helper.rb
+++ b/spec/support/select2_helper.rb
@@ -16,6 +16,7 @@ module Select2Helper
selector = options.fetch(:from)
+ first(selector, visible: false)
if options[:multiple]
execute_script("$('#{selector}').select2('val', ['#{value}']).trigger('change');")
else
diff --git a/spec/support/selection_helper.rb b/spec/support/selection_helper.rb
new file mode 100644
index 00000000000..b4725b137b2
--- /dev/null
+++ b/spec/support/selection_helper.rb
@@ -0,0 +1,6 @@
+module SelectionHelper
+ def select_element(selector)
+ find(selector)
+ execute_script("let range = document.createRange(); let sel = window.getSelection(); range.selectNodeContents(document.querySelector('#{selector}')); sel.addRange(range);")
+ end
+end
diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
index d5bc12f3bc5..17f319f49e9 100644
--- a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
+++ b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
@@ -1,5 +1,5 @@
shared_examples "protected branches > access control > CE" do
- ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+ ProtectedRefAccess::HUMAN_ACCESS_LEVELS.each do |(access_type_id, access_type_name)|
it "allows creating protected branches that #{access_type_name} can push to" do
visit project_protected_branches_path(project)
@@ -9,7 +9,7 @@ shared_examples "protected branches > access control > CE" do
allowed_to_push_button = find(".js-allowed-to-push")
unless allowed_to_push_button.text == access_type_name
- allowed_to_push_button.trigger('click')
+ allowed_to_push_button.click
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end
end
@@ -34,7 +34,7 @@ shared_examples "protected branches > access control > CE" do
within('.js-allowed-to-push-container') do
expect(first("li")).to have_content("Roles")
- click_on access_type_name
+ find(:link, access_type_name).click
end
end
@@ -44,7 +44,7 @@ shared_examples "protected branches > access control > CE" do
end
end
- ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+ ProtectedRefAccess::HUMAN_ACCESS_LEVELS.each do |(access_type_id, access_type_name)|
it "allows creating protected branches that #{access_type_name} can merge to" do
visit project_protected_branches_path(project)
@@ -79,7 +79,7 @@ shared_examples "protected branches > access control > CE" do
within('.js-allowed-to-merge-container') do
expect(first("li")).to have_content("Roles")
- click_on access_type_name
+ find(:link, access_type_name).click
end
end
diff --git a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
new file mode 100644
index 00000000000..a4762b68858
--- /dev/null
+++ b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
@@ -0,0 +1,57 @@
+# This shared example requires a `builder` and `user` variable
+shared_examples 'issuable hook data' do |kind|
+ let(:data) { builder.build(user: user) }
+
+ include_examples 'project hook data' do
+ let(:project) { builder.issuable.project }
+ end
+ include_examples 'deprecated repository hook data'
+
+ context "with a #{kind}" do
+ it 'contains issuable data' do
+ expect(data[:object_kind]).to eq(kind)
+ expect(data[:user]).to eq(user.hook_attrs)
+ expect(data[:project]).to eq(builder.issuable.project.hook_attrs)
+ expect(data[:object_attributes]).to eq(builder.issuable.hook_attrs)
+ expect(data[:changes]).to eq({})
+ expect(data[:repository]).to eq(builder.issuable.project.hook_attrs.slice(:name, :url, :description, :homepage))
+ end
+
+ it 'does not contain certain keys' do
+ expect(data).not_to have_key(:assignees)
+ expect(data).not_to have_key(:assignee)
+ end
+
+ describe 'changes are given' do
+ let(:changes) do
+ {
+ cached_markdown_version: %w[foo bar],
+ description: ['A description', 'A cool description'],
+ description_html: %w[foo bar],
+ in_progress_merge_commit_sha: %w[foo bar],
+ lock_version: %w[foo bar],
+ merge_jid: %w[foo bar],
+ title: ['A title', 'Hello World'],
+ title_html: %w[foo bar]
+ }
+ end
+ let(:data) { builder.build(user: user, changes: changes) }
+
+ it 'populates the :changes hash' do
+ expect(data[:changes]).to match(hash_including({
+ title: { previous: 'A title', current: 'Hello World' },
+ description: { previous: 'A description', current: 'A cool description' }
+ }))
+ end
+
+ it 'does not contain certain keys' do
+ expect(data[:changes]).not_to have_key('cached_markdown_version')
+ expect(data[:changes]).not_to have_key('description_html')
+ expect(data[:changes]).not_to have_key('lock_version')
+ expect(data[:changes]).not_to have_key('title_html')
+ expect(data[:changes]).not_to have_key('in_progress_merge_commit_sha')
+ expect(data[:changes]).not_to have_key('merge_jid')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/project_hook_data_shared_examples.rb b/spec/support/shared_examples/models/project_hook_data_shared_examples.rb
new file mode 100644
index 00000000000..f0264878811
--- /dev/null
+++ b/spec/support/shared_examples/models/project_hook_data_shared_examples.rb
@@ -0,0 +1,42 @@
+shared_examples 'project hook data with deprecateds' do |project_key: :project|
+ it 'contains project data' do
+ expect(data[project_key][:name]).to eq(project.name)
+ expect(data[project_key][:description]).to eq(project.description)
+ expect(data[project_key][:web_url]).to eq(project.web_url)
+ expect(data[project_key][:avatar_url]).to eq(project.avatar_url)
+ expect(data[project_key][:git_http_url]).to eq(project.http_url_to_repo)
+ expect(data[project_key][:git_ssh_url]).to eq(project.ssh_url_to_repo)
+ expect(data[project_key][:namespace]).to eq(project.namespace.name)
+ expect(data[project_key][:visibility_level]).to eq(project.visibility_level)
+ expect(data[project_key][:path_with_namespace]).to eq(project.full_path)
+ expect(data[project_key][:default_branch]).to eq(project.default_branch)
+ expect(data[project_key][:homepage]).to eq(project.web_url)
+ expect(data[project_key][:url]).to eq(project.url_to_repo)
+ expect(data[project_key][:ssh_url]).to eq(project.ssh_url_to_repo)
+ expect(data[project_key][:http_url]).to eq(project.http_url_to_repo)
+ end
+end
+
+shared_examples 'project hook data' do |project_key: :project|
+ it 'contains project data' do
+ expect(data[project_key][:name]).to eq(project.name)
+ expect(data[project_key][:description]).to eq(project.description)
+ expect(data[project_key][:web_url]).to eq(project.web_url)
+ expect(data[project_key][:avatar_url]).to eq(project.avatar_url)
+ expect(data[project_key][:git_http_url]).to eq(project.http_url_to_repo)
+ expect(data[project_key][:git_ssh_url]).to eq(project.ssh_url_to_repo)
+ expect(data[project_key][:namespace]).to eq(project.namespace.name)
+ expect(data[project_key][:visibility_level]).to eq(project.visibility_level)
+ expect(data[project_key][:path_with_namespace]).to eq(project.full_path)
+ expect(data[project_key][:default_branch]).to eq(project.default_branch)
+ end
+end
+
+shared_examples 'deprecated repository hook data' do
+ it 'contains deprecated repository data' do
+ expect(data[:repository][:name]).to eq(project.name)
+ expect(data[:repository][:description]).to eq(project.description)
+ expect(data[:repository][:url]).to eq(project.url_to_repo)
+ expect(data[:repository][:homepage]).to eq(project.web_url)
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
index c9302f7b750..4e18804b937 100644
--- a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
@@ -3,21 +3,24 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
let!(:custom_attribute2) { attributable.custom_attributes.create key: 'bar', value: 'bar' }
describe "GET /#{attributable_name} with custom attributes filter" do
- let!(:other_attributable) { create attributable.class.name.underscore }
+ before do
+ other_attributable
+ end
context 'with an unauthorized user' do
it 'does not filter by custom attributes' do
get api("/#{attributable_name}", user), custom_attributes: { foo: 'foo', bar: 'bar' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to be 2
+ expect(json_response.map { |r| r['id'] }).to contain_exactly attributable.id, other_attributable.id
end
end
it 'filters by custom attributes' do
get api("/#{attributable_name}", admin), custom_attributes: { foo: 'foo', bar: 'bar' }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to be 1
expect(json_response.first['id']).to eq attributable.id
end
@@ -33,7 +36,7 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
it 'returns all custom attributes' do
get api("/#{attributable_name}/#{attributable.id}/custom_attributes", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to contain_exactly(
{ 'key' => 'foo', 'value' => 'foo' },
{ 'key' => 'bar', 'value' => 'bar' }
@@ -51,7 +54,7 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
it 'returns a single custom attribute' do
get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq({ 'key' => 'foo', 'value' => 'foo' })
end
end
@@ -68,7 +71,7 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
put api("/#{attributable_name}/#{attributable.id}/custom_attributes/new", admin), value: 'new'
end.to change { attributable.custom_attributes.count }.by(1)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq({ 'key' => 'new', 'value' => 'new' })
expect(attributable.custom_attributes.find_by(key: 'new').value).to eq 'new'
end
@@ -78,7 +81,7 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin), value: 'new'
end.not_to change { attributable.custom_attributes.count }
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq({ 'key' => 'foo', 'value' => 'new' })
expect(custom_attribute1.reload.value).to eq 'new'
end
@@ -96,7 +99,7 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
end.to change { attributable.custom_attributes.count }.by(-1)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
expect(attributable.custom_attributes.find_by(key: 'foo')).to be_nil
end
end
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 7d7f66adeab..0ed917e448a 100644
--- a/spec/support/shared_examples/requests/api/status_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/status_shared_examples.rb
@@ -3,6 +3,8 @@
# Requires an API request:
# let(:request) { get api("/projects/#{project.id}/repository/branches", user) }
shared_examples_for '400 response' do
+ let(:message) { nil }
+
before do
# Fires the request
request
@@ -10,6 +12,10 @@ shared_examples_for '400 response' do
it 'returns 400' do
expect(response).to have_gitlab_http_status(400)
+
+ if message.present?
+ expect(json_response['message']).to eq(message)
+ end
end
end
@@ -26,6 +32,7 @@ end
shared_examples_for '404 response' do
let(:message) { nil }
+
before do
# Fires the request
request
diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb
index 6accf16bea4..17f3a861ba8 100644
--- a/spec/support/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/slack_mattermost_notifications_shared_examples.rb
@@ -76,8 +76,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
message: "user created page: Awesome wiki_page"
}
- wiki_page_service = WikiPages::CreateService.new(project, user, opts)
- @wiki_page = wiki_page_service.execute
+ @wiki_page = create(:wiki_page, wiki: project.wiki, attrs: opts)
@wiki_page_sample_data = Gitlab::DataBuilder::WikiPage.build(@wiki_page, user, 'create')
end
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index 2dfb4d4a07f..4ead78529c3 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -38,15 +38,15 @@ module StubConfiguration
allow(Gitlab.config.backup).to receive_messages(to_settings(messages))
end
+ def stub_lfs_setting(messages)
+ allow(Gitlab.config.lfs).to receive_messages(to_settings(messages))
+ end
+
def stub_storage_settings(messages)
# Default storage is always required
messages['default'] ||= Gitlab.config.repositories.storages.default
messages.each do |storage_name, storage_settings|
storage_settings['path'] = TestEnv.repos_path unless storage_settings.key?('path')
- storage_settings['failure_count_threshold'] ||= 10
- storage_settings['failure_wait_time'] ||= 30
- storage_settings['failure_reset_time'] ||= 1800
- storage_settings['storage_timeout'] ||= 5
end
allow(Gitlab.config.repositories).to receive(:storages).and_return(Settingslogic.new(messages))
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index a27bfdee3d2..fff120fcb88 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -182,6 +182,8 @@ module TestEnv
return unless @gitaly_pid
Process.kill('KILL', @gitaly_pid)
+ rescue Errno::ESRCH
+ # The process can already be gone if the test run was INTerrupted.
end
def setup_factory_repo
diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb
index 0fa74f911f6..909d4e2ee8d 100644
--- a/spec/support/time_tracking_shared_examples.rb
+++ b/spec/support/time_tracking_shared_examples.rb
@@ -80,6 +80,6 @@ end
def submit_time(quick_action)
fill_in 'note[note]', with: quick_action
- find('.js-comment-submit-button').trigger('click')
+ find('.js-comment-submit-button').click
wait_for_requests
end
diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb
index 2dfa5fbecea..3d9705c9c05 100644
--- a/spec/support/unique_ip_check_shared_examples.rb
+++ b/spec/support/unique_ip_check_shared_examples.rb
@@ -56,13 +56,13 @@ shared_examples 'user login request with unique ip limit' do |success_status = 2
end
it 'allows user authenticating from the same ip' do
- expect(request_from_ip('ip')).to have_http_status(success_status)
- expect(request_from_ip('ip')).to have_http_status(success_status)
+ expect(request_from_ip('ip')).to have_gitlab_http_status(success_status)
+ expect(request_from_ip('ip')).to have_gitlab_http_status(success_status)
end
it 'blocks user authenticating from two distinct ips' do
- expect(request_from_ip('ip')).to have_http_status(success_status)
- expect(request_from_ip('ip2')).to have_http_status(403)
+ expect(request_from_ip('ip')).to have_gitlab_http_status(success_status)
+ expect(request_from_ip('ip2')).to have_gitlab_http_status(403)
end
end
end
diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb
index 50a1d4a56e2..1490287681b 100644
--- a/spec/support/update_invalid_issuable.rb
+++ b/spec/support/update_invalid_issuable.rb
@@ -25,13 +25,11 @@ shared_examples 'update invalid issuable' do |klass|
.and_raise(ActiveRecord::StaleObjectError.new(issuable, :save))
end
- if klass == MergeRequest
- it 'renders edit when format is html' do
- put :update, params
+ it 'renders edit when format is html' do
+ put :update, params
- expect(response).to render_template(:edit)
- expect(assigns[:conflict]).to be_truthy
- end
+ expect(response).to render_template(:edit)
+ expect(assigns[:conflict]).to be_truthy
end
it 'renders json error message when format is json' do
@@ -44,17 +42,16 @@ shared_examples 'update invalid issuable' do |klass|
end
end
- if klass == MergeRequest
- context 'when updating an invalid issuable' do
- before do
- params[:merge_request][:title] = ""
- end
+ context 'when updating an invalid issuable' do
+ before do
+ key = klass == Issue ? :issue : :merge_request
+ params[key][:title] = ""
+ end
- it 'renders edit when merge request is invalid' do
- put :update, params
+ it 'renders edit when merge request is invalid' do
+ put :update, params
- expect(response).to render_template(:edit)
- end
+ expect(response).to render_template(:edit)
end
end
end
diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb
index b5c3c0f55b8..f4130d68271 100644
--- a/spec/support/wait_for_requests.rb
+++ b/spec/support/wait_for_requests.rb
@@ -1,25 +1,47 @@
-require_relative './wait_for_requests'
-
module WaitForRequests
extend self
# This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests
def block_and_wait_for_requests_complete
+ block_requests { wait_for_all_requests }
+ end
+
+ # Block all requests inside block with 503 response
+ def block_requests
Gitlab::Testing::RequestBlockerMiddleware.block_requests!
- wait_for('pending requests complete') do
- Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? && finished_all_requests?
- end
+ yield
ensure
Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
end
+ # Slow down requests inside block by injecting `sleep 0.2` before each response
+ def slow_requests
+ Gitlab::Testing::RequestBlockerMiddleware.slow_requests!
+ yield
+ ensure
+ Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
+ end
+
+ # Wait for client-side AJAX requests
def wait_for_requests
- wait_for('JS requests') { finished_all_requests? }
+ wait_for('JS requests complete') { finished_all_js_requests? }
+ end
+
+ # Wait for active Rack requests and client-side AJAX requests
+ def wait_for_all_requests
+ wait_for('pending requests complete') do
+ finished_all_rack_reqiests? &&
+ finished_all_js_requests?
+ end
end
private
- def finished_all_requests?
+ def finished_all_rack_reqiests?
+ Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero?
+ end
+
+ def finished_all_js_requests?
return true unless javascript_test?
finished_all_ajax_requests? &&
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 886052d7848..bf2e11bc360 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -4,7 +4,15 @@ require 'rake'
describe 'gitlab:app namespace rake task' do
let(:enable_registry) { true }
- before :all do
+ def tars_glob
+ Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar'))
+ end
+
+ def backup_tar
+ tars_glob.first
+ end
+
+ before(:all) do
Rake.application.rake_require 'tasks/gitlab/helpers'
Rake.application.rake_require 'tasks/gitlab/backup'
Rake.application.rake_require 'tasks/gitlab/shell'
@@ -19,9 +27,16 @@ describe 'gitlab:app namespace rake task' do
end
before do
+ stub_env('force', 'yes')
+ FileUtils.rm(tars_glob, force: true)
+ reenable_backup_sub_tasks
stub_container_registry_config(enabled: enable_registry)
end
+ after do
+ FileUtils.rm(tars_glob, force: true)
+ end
+
def run_rake_task(task_name)
Rake::Task[task_name].reenable
Rake.application.invoke_task task_name
@@ -34,22 +49,15 @@ describe 'gitlab:app namespace rake task' do
end
describe 'backup_restore' do
- before do
- # avoid writing task output to spec progress
- allow($stdout).to receive :write
- end
-
context 'gitlab version' do
before do
allow(Dir).to receive(:glob).and_return(['1_gitlab_backup.tar'])
- allow(Dir).to receive(:chdir)
allow(File).to receive(:exist?).and_return(true)
allow(Kernel).to receive(:system).and_return(true)
allow(FileUtils).to receive(:cp_r).and_return(true)
allow(FileUtils).to receive(:mv).and_return(true)
allow(Rake::Task["gitlab:shell:setup"])
.to receive(:invoke).and_return(true)
- ENV['force'] = 'yes'
end
let(:gitlab_version) { Gitlab::VERSION }
@@ -58,8 +66,9 @@ describe 'gitlab:app namespace rake task' do
allow(YAML).to receive(:load_file)
.and_return({ gitlab_version: "not #{gitlab_version}" })
- expect { run_rake_task('gitlab:backup:restore') }
- .to raise_error(SystemExit)
+ expect do
+ expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout
+ end.to raise_error(SystemExit)
end
it 'invokes restoration on match' do
@@ -75,44 +84,15 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
- expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
+ expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout
end
end
end # backup_restore task
describe 'backup' do
- before(:all) do
- ENV['force'] = 'yes'
- end
-
- def tars_glob
- Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar'))
- end
-
- def create_backup
- FileUtils.rm tars_glob
-
+ before do
# This reconnect makes our project fixture disappear, breaking the restore. Stub it out.
allow(ActiveRecord::Base.connection).to receive(:reconnect!)
-
- # Redirect STDOUT and run the rake task
- orig_stdout = $stdout
- $stdout = StringIO.new
- reenable_backup_sub_tasks
- run_rake_task('gitlab:backup:create')
- reenable_backup_sub_tasks
- $stdout = orig_stdout
-
- @backup_tar = tars_glob.first
- end
-
- def restore_backup
- orig_stdout = $stdout
- $stdout = StringIO.new
- reenable_backup_sub_tasks
- run_rake_task('gitlab:backup:restore')
- reenable_backup_sub_tasks
- $stdout = orig_stdout
end
describe 'backup creation and deletion using custom_hooks' do
@@ -120,27 +100,17 @@ describe 'gitlab:app namespace rake task' do
let(:user_backup_path) { "repositories/#{project.disk_path}" }
before do
- @origin_cd = Dir.pwd
-
- path = File.join(project.repository.path_to_repo, filename)
+ stub_env('SKIP', 'db')
+ path = File.join(project.repository.path_to_repo, 'custom_hooks')
FileUtils.mkdir_p(path)
FileUtils.touch(File.join(path, "dummy.txt"))
-
- ENV["SKIP"] = "db"
- create_backup
- end
-
- after do
- ENV["SKIP"] = ""
- FileUtils.rm(@backup_tar)
- Dir.chdir(@origin_cd)
end
context 'project uses custom_hooks and successfully creates backup' do
- let(:filename) { "custom_hooks" }
-
it 'creates custom_hooks.tar and project bundle' do
- tar_contents, exit_status = Gitlab::Popen.popen(%W{tar -tvf #{@backup_tar}})
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+
+ tar_contents, exit_status = Gitlab::Popen.popen(%W{tar -tvf #{backup_tar}})
expect(exit_status).to eq(0)
expect(tar_contents).to match(user_backup_path)
@@ -149,47 +119,43 @@ describe 'gitlab:app namespace rake task' do
end
it 'restores files correctly' do
- restore_backup
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout
- expect(Dir.entries(File.join(project.repository.path, "custom_hooks"))).to include("dummy.txt")
+ expect(Dir.entries(File.join(project.repository.path, 'custom_hooks'))).to include("dummy.txt")
end
end
end
context 'tar creation' do
- before do
- create_backup
- end
-
- after do
- FileUtils.rm(@backup_tar)
- end
-
context 'archive file permissions' do
it 'sets correct permissions on the tar file' do
- expect(File.exist?(@backup_tar)).to be_truthy
- expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600')
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+
+ expect(File.exist?(backup_tar)).to be_truthy
+ expect(File::Stat.new(backup_tar).mode.to_s(8)).to eq('100600')
end
context 'with custom archive_permissions' do
before do
allow(Gitlab.config.backup).to receive(:archive_permissions).and_return(0651)
- # We created a backup in a before(:all) so it got the default permissions.
- # We now need to do some work to create a _new_ backup file using our stub.
- FileUtils.rm(@backup_tar)
- create_backup
end
it 'uses the custom permissions' do
- expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100651')
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+
+ expect(File::Stat.new(backup_tar).mode.to_s(8)).to eq('100651')
end
end
end
it 'sets correct permissions on the tar contents' do
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
+ %W{tar -tvf #{backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
)
+
expect(exit_status).to eq(0)
expect(tar_contents).to match('db/')
expect(tar_contents).to match('uploads.tar.gz')
@@ -203,6 +169,8 @@ describe 'gitlab:app namespace rake task' do
end
it 'deletes temp directories' do
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+
temp_dirs = Dir.glob(
File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,pages,lfs,registry}')
)
@@ -214,9 +182,12 @@ describe 'gitlab:app namespace rake task' do
let(:enable_registry) { false }
it 'does not create registry.tar.gz' do
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar}}
+ %W{tar -tvf #{backup_tar}}
)
+
expect(exit_status).to eq(0)
expect(tar_contents).not_to match('registry.tar.gz')
end
@@ -232,37 +203,33 @@ describe 'gitlab:app namespace rake task' do
}
end
- let(:project_a) { create(:project, :repository, repository_storage: 'default') }
- let(:project_b) { create(:project, :repository, repository_storage: 'test_second_storage') }
-
before do
- FileUtils.mkdir('tmp/tests/default_storage')
- FileUtils.mkdir('tmp/tests/custom_storage')
+ # We only need a backup of the repositories for this test
+ stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,registry')
+ FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage'))
+ FileUtils.mkdir(Settings.absolute('tmp/tests/custom_storage'))
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- # Create the projects now, after mocking the settings but before doing the backup
- project_a
- project_b
-
# Avoid asking gitaly about the root ref (which will fail beacuse of the
# mocked storages)
allow_any_instance_of(Repository).to receive(:empty_repo?).and_return(false)
-
- # We only need a backup of the repositories for this test
- ENV["SKIP"] = "db,uploads,builds,artifacts,lfs,registry"
- create_backup
end
after do
- FileUtils.rm_rf('tmp/tests/default_storage')
- FileUtils.rm_rf('tmp/tests/custom_storage')
- FileUtils.rm(@backup_tar) if @backup_tar
+ FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage'))
+ FileUtils.rm_rf(Settings.absolute('tmp/tests/custom_storage'))
end
it 'includes repositories in all repository storages' do
+ project_a = create(:project, :repository, repository_storage: 'default')
+ project_b = create(:project, :repository, repository_storage: 'test_second_storage')
+
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} repositories}
+ %W{tar -tvf #{backup_tar} repositories}
)
+
expect(exit_status).to eq(0)
expect(tar_contents).to match("repositories/#{project_a.disk_path}.bundle")
expect(tar_contents).to match("repositories/#{project_b.disk_path}.bundle")
@@ -271,35 +238,15 @@ describe 'gitlab:app namespace rake task' do
end # backup_create task
describe "Skipping items" do
- def tars_glob
- Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar'))
- end
-
- before :all do
- @origin_cd = Dir.pwd
-
- reenable_backup_sub_tasks
-
- FileUtils.rm tars_glob
-
- # Redirect STDOUT and run the rake task
- orig_stdout = $stdout
- $stdout = StringIO.new
- ENV["SKIP"] = "repositories,uploads"
- run_rake_task('gitlab:backup:create')
- $stdout = orig_stdout
-
- @backup_tar = tars_glob.first
- end
-
- after :all do
- FileUtils.rm(@backup_tar)
- Dir.chdir @origin_cd
+ before do
+ stub_env('SKIP', 'repositories,uploads')
end
it "does not contain skipped item" do
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+
tar_contents, _exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
+ %W{tar -tvf #{backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
)
expect(tar_contents).to match('db/')
@@ -313,9 +260,10 @@ describe 'gitlab:app namespace rake task' do
end
it 'does not invoke repositories restore' do
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+
allow(Rake::Task['gitlab:shell:setup'])
.to receive(:invoke).and_return(true)
- allow($stdout).to receive :write
expect(Rake::Task['gitlab:db:drop_tables']).to receive :invoke
expect(Rake::Task['gitlab:backup:db:restore']).to receive :invoke
@@ -327,38 +275,15 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
- expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
+ expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout
end
end
describe "Human Readable Backup Name" do
- def tars_glob
- Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar'))
- end
-
- before :all do
- @origin_cd = Dir.pwd
-
- reenable_backup_sub_tasks
-
- FileUtils.rm tars_glob
-
- # Redirect STDOUT and run the rake task
- orig_stdout = $stdout
- $stdout = StringIO.new
- run_rake_task('gitlab:backup:create')
- $stdout = orig_stdout
-
- @backup_tar = tars_glob.first
- end
-
- after :all do
- FileUtils.rm(@backup_tar)
- Dir.chdir @origin_cd
- end
-
it 'name has human readable time' do
- expect(@backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_\d+\.\d+\.\d+.*_gitlab_backup.tar$/)
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+
+ expect(backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_\d+\.\d+\.\d+.*_gitlab_backup.tar$/)
end
end
end # gitlab:app namespace
diff --git a/spec/tasks/gitlab/cleanup_rake_spec.rb b/spec/tasks/gitlab/cleanup_rake_spec.rb
new file mode 100644
index 00000000000..9e746ceddd6
--- /dev/null
+++ b/spec/tasks/gitlab/cleanup_rake_spec.rb
@@ -0,0 +1,67 @@
+require 'rake_helper'
+
+describe 'gitlab:cleanup rake tasks' do
+ before do
+ Rake.application.rake_require 'tasks/gitlab/cleanup'
+ end
+
+ describe 'cleanup' do
+ let(:gitaly_address) { Gitlab.config.repositories.storages.default.gitaly_address }
+ let(:storages) do
+ {
+ 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address }
+ }
+ end
+
+ before do
+ FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage'))
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ end
+
+ after do
+ FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage'))
+ end
+
+ describe 'cleanup:repos' do
+ before do
+ FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/broken/project.git'))
+ FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))
+ end
+
+ it 'moves it to an orphaned path' do
+ run_rake_task('gitlab:cleanup:repos')
+ repo_list = Dir['tmp/tests/default_storage/broken/*']
+
+ expect(repo_list.first).to include('+orphaned+')
+ end
+
+ it 'ignores @hashed repos' do
+ run_rake_task('gitlab:cleanup:repos')
+
+ expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))).to be_truthy
+ end
+ end
+
+ describe 'cleanup:dirs' do
+ it 'removes missing namespaces' do
+ FileUtils.mkdir_p(Settings.absolute("tmp/tests/default_storage/namespace_1/project.git"))
+ FileUtils.mkdir_p(Settings.absolute("tmp/tests/default_storage/namespace_2/project.git"))
+ allow(Namespace).to receive(:pluck).and_return('namespace_1')
+
+ stub_env('REMOVE', 'true')
+ run_rake_task('gitlab:cleanup:dirs')
+
+ expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/namespace_1'))).to be_truthy
+ expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/namespace_2'))).to be_falsey
+ end
+
+ it 'ignores @hashed directory' do
+ FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))
+
+ run_rake_task('gitlab:cleanup:dirs')
+
+ expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index 1e9b20435ec..6aba86fdc3c 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -43,18 +43,11 @@ describe 'gitlab:gitaly namespace rake task' do
describe 'gmake/make' do
let(:command_preamble) { %w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE] }
- before(:all) do
- @old_env_ci = ENV.delete('CI')
- end
-
- after(:all) do
- ENV['CI'] = @old_env_ci if @old_env_ci
- end
-
before do
+ stub_env('CI', false)
FileUtils.mkdir_p(clone_path)
expect(Dir).to receive(:chdir).with(clone_path).and_call_original
- allow(Bundler).to receive(:bundle_path).and_return('/fake/bundle_path')
+ allow(Rails.env).to receive(:test?).and_return(false)
end
context 'gmake is available' do
@@ -64,7 +57,7 @@ describe 'gitlab:gitaly namespace rake task' do
it 'calls gmake in the gitaly directory' do
expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0])
- expect(main_object).to receive(:run_command!).with(command_preamble + %w[gmake BUNDLE_PATH=/fake/bundle_path]).and_return(true)
+ expect(main_object).to receive(:run_command!).with(command_preamble + %w[gmake]).and_return(true)
run_rake_task('gitlab:gitaly:install', clone_path)
end
@@ -77,18 +70,20 @@ describe 'gitlab:gitaly namespace rake task' do
end
it 'calls make in the gitaly directory' do
- expect(main_object).to receive(:run_command!).with(command_preamble + %w[make BUNDLE_PATH=/fake/bundle_path]).and_return(true)
+ expect(main_object).to receive(:run_command!).with(command_preamble + %w[make]).and_return(true)
run_rake_task('gitlab:gitaly:install', clone_path)
end
- context 'when Rails.env is not "test"' do
+ context 'when Rails.env is test' do
+ let(:command) { %w[make BUNDLE_FLAGS=--no-deployment] }
+
before do
- allow(Rails.env).to receive(:test?).and_return(false)
+ allow(Rails.env).to receive(:test?).and_return(true)
end
- it 'calls make in the gitaly directory without BUNDLE_PATH' do
- expect(main_object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true)
+ it 'calls make in the gitaly directory with --no-deployment flag for bundle' do
+ expect(main_object).to receive(:run_command!).with(command_preamble + command).and_return(true)
run_rake_task('gitlab:gitaly:install', clone_path)
end
@@ -117,6 +112,7 @@ describe 'gitlab:gitaly namespace rake task' do
expected_output = <<~TOML
# Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)}
# This is in TOML format suitable for use in Gitaly's config.toml file.
+ bin_dir = "tmp/tests/gitaly"
socket_path = "/path/to/my.socket"
[gitlab-shell]
dir = "#{Gitlab.config.gitlab_shell.path}"
diff --git a/spec/tasks/gitlab/ldap_rake_spec.rb b/spec/tasks/gitlab/ldap_rake_spec.rb
index 12d442b9820..279234f2887 100644
--- a/spec/tasks/gitlab/ldap_rake_spec.rb
+++ b/spec/tasks/gitlab/ldap_rake_spec.rb
@@ -4,7 +4,7 @@ describe 'gitlab:ldap:rename_provider rake task' do
it 'completes without error' do
Rake.application.rake_require 'tasks/gitlab/ldap'
stub_warn_user_is_not_gitlab
- ENV['force'] = 'yes'
+ stub_env('force', 'yes')
create(:identity) # Necessary to prevent `exit 1` from the task.
diff --git a/spec/tasks/gitlab/users_rake_spec.rb b/spec/tasks/gitlab/users_rake_spec.rb
deleted file mode 100644
index 972670e7f91..00000000000
--- a/spec/tasks/gitlab/users_rake_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-require 'spec_helper'
-require 'rake'
-
-describe 'gitlab:users namespace rake task' do
- let(:enable_registry) { true }
-
- before :all do
- Rake.application.rake_require 'tasks/gitlab/helpers'
- Rake.application.rake_require 'tasks/gitlab/users'
-
- # empty task as env is already loaded
- Rake::Task.define_task :environment
- end
-
- def run_rake_task(task_name)
- Rake::Task[task_name].reenable
- Rake.application.invoke_task task_name
- end
-
- describe 'clear_all_authentication_tokens' do
- before do
- # avoid writing task output to spec progress
- allow($stdout).to receive :write
- end
-
- context 'gitlab version' do
- it 'clears the authentication token for all users' do
- create_list(:user, 2)
-
- expect(User.pluck(:authentication_token)).to all(be_present)
-
- run_rake_task('gitlab:users:clear_all_authentication_tokens')
-
- expect(User.pluck(:authentication_token)).to all(be_nil)
- end
- end
- end
-end
diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb
index b84137eb365..51f7a536cbb 100644
--- a/spec/tasks/tokens_spec.rb
+++ b/spec/tasks/tokens_spec.rb
@@ -7,12 +7,6 @@ describe 'tokens rake tasks' do
Rake.application.rake_require 'tasks/tokens'
end
- describe 'reset_all task' do
- it 'invokes create_hooks task' do
- expect { run_rake_task('tokens:reset_all_auth') }.to change { user.reload.authentication_token }
- end
- end
-
describe 'reset_all_email task' do
it 'invokes create_hooks task' do
expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token }
diff --git a/spec/unicorn/unicorn_spec.rb b/spec/unicorn/unicorn_spec.rb
index 41de94d35c2..a4cf479a339 100644
--- a/spec/unicorn/unicorn_spec.rb
+++ b/spec/unicorn/unicorn_spec.rb
@@ -37,7 +37,22 @@ describe 'Unicorn' do
config_path = 'tmp/tests/unicorn.rb'
File.write(config_path, config_lines.join("\n") + "\n")
- cmd = %W[unicorn -E test -c #{config_path} #{Rails.root.join('config.ru')}]
+ rackup_path = 'tmp/tests/config.ru'
+ File.write(rackup_path, <<~EOS)
+ app =
+ proc do |env|
+ if env['REQUEST_METHOD'] == 'GET'
+ [200, {}, [Process.pid]]
+ else
+ Process.kill(env['QUERY_STRING'], Process.pid)
+ [200, {}, ['Bye!']]
+ end
+ end
+
+ run app
+ EOS
+
+ cmd = %W[unicorn -E test -c #{config_path} #{rackup_path}]
@unicorn_master_pid = spawn(*cmd)
wait_unicorn_boot!(@unicorn_master_pid, ready_file)
WebMock.allow_net_connect!
@@ -45,14 +60,14 @@ describe 'Unicorn' do
%w[SIGQUIT SIGTERM SIGKILL].each do |signal|
it "has a worker that self-terminates on signal #{signal}" do
- response = Excon.get('unix:///unicorn_test/pid', socket: @socket_path)
+ response = Excon.get('unix://', socket: @socket_path)
expect(response.status).to eq(200)
worker_pid = response.body.to_i
expect(worker_pid).to be > 0
begin
- Excon.post('unix:///unicorn_test/kill', socket: @socket_path, body: "signal=#{signal}")
+ Excon.post("unix://?#{signal}", socket: @socket_path)
rescue Excon::Error::Socket
# The connection may be closed abruptly
end
@@ -71,6 +86,7 @@ describe 'Unicorn' do
timeout = 5 * 60
timeout.times do
return if File.exist?(ready_file)
+
pid = Process.waitpid(master_pid, Process::WNOHANG)
raise "unicorn failed to boot: #{$?}" unless pid.nil?
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index 2492d56a5cf..fd195d6f9b8 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -3,25 +3,77 @@ require 'spec_helper'
describe FileUploader do
let(:uploader) { described_class.new(build_stubbed(:project)) }
- describe '.absolute_path' do
- it 'returns the correct absolute path by building it dynamically' do
- project = build_stubbed(:project)
- upload = double(model: project, path: 'secret/foo.jpg')
+ context 'legacy storage' do
+ let(:project) { build_stubbed(:project) }
- dynamic_segment = project.path_with_namespace
+ describe '.absolute_path' do
+ it 'returns the correct absolute path by building it dynamically' do
+ upload = double(model: project, path: 'secret/foo.jpg')
- expect(described_class.absolute_path(upload))
- .to end_with("#{dynamic_segment}/secret/foo.jpg")
+ dynamic_segment = project.full_path
+
+ expect(described_class.absolute_path(upload))
+ .to end_with("#{dynamic_segment}/secret/foo.jpg")
+ end
+ end
+
+ describe "#store_dir" do
+ it "stores in the namespace path" do
+ uploader = described_class.new(project)
+
+ expect(uploader.store_dir).to include(project.full_path)
+ expect(uploader.store_dir).not_to include("system")
+ end
end
end
- describe "#store_dir" do
- it "stores in the namespace path" do
- project = build_stubbed(:project)
- uploader = described_class.new(project)
+ context 'hashed storage' do
+ context 'when rolled out attachments' do
+ let(:project) { build_stubbed(:project, :hashed) }
+
+ describe '.absolute_path' do
+ it 'returns the correct absolute path by building it dynamically' do
+ upload = double(model: project, path: 'secret/foo.jpg')
+
+ dynamic_segment = project.disk_path
+
+ expect(described_class.absolute_path(upload))
+ .to end_with("#{dynamic_segment}/secret/foo.jpg")
+ end
+ end
+
+ describe "#store_dir" do
+ it "stores in the namespace path" do
+ uploader = described_class.new(project)
+
+ expect(uploader.store_dir).to include(project.disk_path)
+ expect(uploader.store_dir).not_to include("system")
+ end
+ end
+ end
+
+ context 'when only repositories are rolled out' do
+ let(:project) { build_stubbed(:project, storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) }
+
+ describe '.absolute_path' do
+ it 'returns the correct absolute path by building it dynamically' do
+ upload = double(model: project, path: 'secret/foo.jpg')
+
+ dynamic_segment = project.full_path
+
+ expect(described_class.absolute_path(upload))
+ .to end_with("#{dynamic_segment}/secret/foo.jpg")
+ end
+ end
+
+ describe "#store_dir" do
+ it "stores in the namespace path" do
+ uploader = described_class.new(project)
- expect(uploader.store_dir).to include(project.path_with_namespace)
- expect(uploader.store_dir).not_to include("system")
+ expect(uploader.store_dir).to include(project.full_path)
+ expect(uploader.store_dir).not_to include("system")
+ end
+ end
end
end
diff --git a/spec/validators/dynamic_path_validator_spec.rb b/spec/validators/dynamic_path_validator_spec.rb
deleted file mode 100644
index 08e1c5a728a..00000000000
--- a/spec/validators/dynamic_path_validator_spec.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-require 'spec_helper'
-
-describe DynamicPathValidator do
- let(:validator) { described_class.new(attributes: [:path]) }
-
- def expect_handles_invalid_utf8
- expect { yield('\255invalid') }.to be_falsey
- end
-
- describe '.valid_user_path' do
- it 'handles invalid utf8' do
- expect(described_class.valid_user_path?("a\0weird\255path")).to be_falsey
- end
- end
-
- describe '.valid_group_path' do
- it 'handles invalid utf8' do
- expect(described_class.valid_group_path?("a\0weird\255path")).to be_falsey
- end
- end
-
- describe '.valid_project_path' do
- it 'handles invalid utf8' do
- expect(described_class.valid_project_path?("a\0weird\255path")).to be_falsey
- end
- end
-
- describe '#path_valid_for_record?' do
- context 'for project' do
- it 'calls valid_project_path?' do
- project = build(:project, path: 'activity')
-
- expect(described_class).to receive(:valid_project_path?).with(project.full_path).and_call_original
-
- expect(validator.path_valid_for_record?(project, 'activity')).to be_truthy
- end
- end
-
- context 'for group' do
- it 'calls valid_group_path?' do
- group = build(:group, :nested, path: 'activity')
-
- expect(described_class).to receive(:valid_group_path?).with(group.full_path).and_call_original
-
- expect(validator.path_valid_for_record?(group, 'activity')).to be_falsey
- end
- end
-
- context 'for user' do
- it 'calls valid_user_path?' do
- user = build(:user, username: 'activity')
-
- expect(described_class).to receive(:valid_user_path?).with(user.full_path).and_call_original
-
- expect(validator.path_valid_for_record?(user, 'activity')).to be_truthy
- end
- end
-
- context 'for user namespace' do
- it 'calls valid_user_path?' do
- user = create(:user, username: 'activity')
- namespace = user.namespace
-
- expect(described_class).to receive(:valid_user_path?).with(namespace.full_path).and_call_original
-
- expect(validator.path_valid_for_record?(namespace, 'activity')).to be_truthy
- end
- end
- end
-
- describe '#validates_each' do
- it 'adds a message when the path is not in the correct format' do
- group = build(:group)
-
- validator.validate_each(group, :path, "Path with spaces, and comma's!")
-
- expect(group.errors[:path]).to include(Gitlab::PathRegex.namespace_format_message)
- end
-
- it 'adds a message when the path is not in the correct format' do
- group = build(:group, path: 'users')
-
- validator.validate_each(group, :path, 'users')
-
- expect(group.errors[:path]).to include('users is a reserved name')
- end
-
- it 'updating to an invalid path is not allowed' do
- project = create(:project)
- project.path = 'update'
-
- validator.validate_each(project, :path, 'update')
-
- expect(project.errors[:path]).to include('update is a reserved name')
- end
- end
-end
diff --git a/spec/validators/namespace_path_validator_spec.rb b/spec/validators/namespace_path_validator_spec.rb
new file mode 100644
index 00000000000..61e2845f35f
--- /dev/null
+++ b/spec/validators/namespace_path_validator_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe NamespacePathValidator do
+ let(:validator) { described_class.new(attributes: [:path]) }
+
+ describe '.valid_path?' do
+ it 'handles invalid utf8' do
+ expect(described_class.valid_path?("a\0weird\255path")).to be_falsey
+ end
+ end
+
+ describe '#validates_each' do
+ it 'adds a message when the path is not in the correct format' do
+ group = build(:group)
+
+ validator.validate_each(group, :path, "Path with spaces, and comma's!")
+
+ expect(group.errors[:path]).to include(Gitlab::PathRegex.namespace_format_message)
+ end
+
+ it 'adds a message when the path is reserved when creating' do
+ group = build(:group, path: 'help')
+
+ validator.validate_each(group, :path, 'help')
+
+ expect(group.errors[:path]).to include('help is a reserved name')
+ end
+
+ it 'adds a message when the path is reserved when updating' do
+ group = create(:group)
+ group.path = 'help'
+
+ validator.validate_each(group, :path, 'help')
+
+ expect(group.errors[:path]).to include('help is a reserved name')
+ end
+ end
+end
diff --git a/spec/validators/project_path_validator_spec.rb b/spec/validators/project_path_validator_spec.rb
new file mode 100644
index 00000000000..8bb5e72dc22
--- /dev/null
+++ b/spec/validators/project_path_validator_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe ProjectPathValidator do
+ let(:validator) { described_class.new(attributes: [:path]) }
+
+ describe '.valid_path?' do
+ it 'handles invalid utf8' do
+ expect(described_class.valid_path?("a\0weird\255path")).to be_falsey
+ end
+ end
+
+ describe '#validates_each' do
+ it 'adds a message when the path is not in the correct format' do
+ project = build(:project)
+
+ validator.validate_each(project, :path, "Path with spaces, and comma's!")
+
+ expect(project.errors[:path]).to include(Gitlab::PathRegex.project_path_format_message)
+ end
+
+ it 'adds a message when the path is reserved when creating' do
+ project = build(:project, path: 'blob')
+
+ validator.validate_each(project, :path, 'blob')
+
+ expect(project.errors[:path]).to include('blob is a reserved name')
+ end
+
+ it 'adds a message when the path is reserved when updating' do
+ project = create(:project)
+ project.path = 'blob'
+
+ validator.validate_each(project, :path, 'blob')
+
+ expect(project.errors[:path]).to include('blob is a reserved name')
+ end
+ end
+end
diff --git a/spec/validators/user_path_validator_spec.rb b/spec/validators/user_path_validator_spec.rb
new file mode 100644
index 00000000000..a46089cc24f
--- /dev/null
+++ b/spec/validators/user_path_validator_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe UserPathValidator do
+ let(:validator) { described_class.new(attributes: [:username]) }
+
+ describe '.valid_path?' do
+ it 'handles invalid utf8' do
+ expect(described_class.valid_path?("a\0weird\255path")).to be_falsey
+ end
+ end
+
+ describe '#validates_each' do
+ it 'adds a message when the path is not in the correct format' do
+ user = build(:user)
+
+ validator.validate_each(user, :username, "Path with spaces, and comma's!")
+
+ expect(user.errors[:username]).to include(Gitlab::PathRegex.namespace_format_message)
+ end
+
+ it 'adds a message when the path is reserved when creating' do
+ user = build(:user, username: 'help')
+
+ validator.validate_each(user, :username, 'help')
+
+ expect(user.errors[:username]).to include('help is a reserved name')
+ end
+
+ it 'adds a message when the path is reserved when updating' do
+ user = create(:user)
+ user.username = 'help'
+
+ validator.validate_each(user, :username, 'help')
+
+ expect(user.errors[:username]).to include('help is a reserved name')
+ end
+ end
+end
diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb
index c030129559e..0a78606171d 100644
--- a/spec/views/help/index.html.haml_spec.rb
+++ b/spec/views/help/index.html.haml_spec.rb
@@ -25,6 +25,14 @@ describe 'help/index' do
end
end
+ describe 'instance configuration link' do
+ it 'is visible to guests' do
+ render
+
+ expect(rendered).to have_link(nil, help_instance_configuration_url)
+ end
+ end
+
def stub_user(user = double)
allow(view).to receive(:user_signed_in?).and_return(user)
end
diff --git a/spec/views/help/instance_configuration.html.haml_spec.rb b/spec/views/help/instance_configuration.html.haml_spec.rb
new file mode 100644
index 00000000000..f30b5881fde
--- /dev/null
+++ b/spec/views/help/instance_configuration.html.haml_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+describe 'help/instance_configuration' do
+ describe 'General Sections:' do
+ let(:instance_configuration) { build(:instance_configuration)}
+ let(:settings) { instance_configuration.settings }
+ let(:ssh_settings) { settings[:ssh_algorithms_hashes] }
+
+ before do
+ assign(:instance_configuration, instance_configuration)
+ end
+
+ it 'has links to several sections' do
+ render
+
+ expect(rendered).to have_link(nil, '#ssh-host-keys-fingerprints') if ssh_settings.any?
+ expect(rendered).to have_link(nil, '#gitlab-pages')
+ expect(rendered).to have_link(nil, '#gitlab-ci')
+ end
+
+ it 'has several sections' do
+ render
+
+ expect(rendered).to have_css('h2#ssh-host-keys-fingerprints') if ssh_settings.any?
+ expect(rendered).to have_css('h2#gitlab-pages')
+ expect(rendered).to have_css('h2#gitlab-ci')
+ end
+ end
+end
diff --git a/spec/views/projects/commit/branches.html.haml_spec.rb b/spec/views/projects/commit/branches.html.haml_spec.rb
new file mode 100644
index 00000000000..b9d4dc80fe0
--- /dev/null
+++ b/spec/views/projects/commit/branches.html.haml_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+describe 'projects/commit/branches.html.haml' do
+ let(:project) { create(:project, :repository) }
+
+ before do
+ assign(:project, project)
+ end
+
+ context 'when branches and tags are available' do
+ before do
+ assign(:branches, ['master', 'test-branch'])
+ assign(:branches_limit_exceeded, false)
+ assign(:tags, ['tag1'])
+ assign(:tags_limit_exceeded, false)
+
+ render
+ end
+
+ it 'shows default branch' do
+ expect(rendered).to have_link('master')
+ end
+
+ it 'shows js expand link' do
+ expect(rendered).to have_selector('.js-details-expand')
+ end
+
+ it 'shows branch and tag links' do
+ expect(rendered).to have_link('test-branch')
+ expect(rendered).to have_link('tag1')
+ end
+ end
+
+ context 'when branches are available but no tags' do
+ before do
+ assign(:branches, ['master', 'test-branch'])
+ assign(:branches_limit_exceeded, false)
+ assign(:tags, [])
+ assign(:tags_limit_exceeded, true)
+
+ render
+ end
+
+ it 'shows branches' do
+ expect(rendered).to have_link('master')
+ expect(rendered).to have_link('test-branch')
+ end
+
+ it 'shows js expand link' do
+ expect(rendered).to have_selector('.js-details-expand')
+ end
+
+ it 'shows limit exceeded message for tags' do
+ expect(rendered).to have_text('Tags unavailable')
+ end
+ end
+
+ context 'when tags are available but no branches (just default)' do
+ before do
+ assign(:branches, ['master'])
+ assign(:branches_limit_exceeded, true)
+ assign(:tags, %w(tag1 tag2))
+ assign(:tags_limit_exceeded, false)
+
+ render
+ end
+
+ it 'shows default branch' do
+ expect(rendered).to have_text('master')
+ end
+
+ it 'shows js expand link' do
+ expect(rendered).to have_selector('.js-details-expand')
+ end
+
+ it 'shows tags' do
+ expect(rendered).to have_link('tag1')
+ expect(rendered).to have_link('tag2')
+ end
+
+ it 'shows limit exceeded for branches' do
+ expect(rendered).to have_text('Branches unavailable')
+ end
+ end
+
+ context 'when branches and tags are not available' do
+ before do
+ assign(:branches, ['master'])
+ assign(:branches_limit_exceeded, true)
+ assign(:tags, [])
+ assign(:tags_limit_exceeded, true)
+
+ render
+ end
+
+ it 'shows default branch' do
+ expect(rendered).to have_text('master')
+ end
+
+ it 'shows js expand link' do
+ expect(rendered).to have_selector('.js-details-expand')
+ end
+
+ it 'shows too many to search' do
+ expect(rendered).to have_text('Branches unavailable')
+ expect(rendered).to have_text('Tags unavailable')
+ end
+ end
+end
diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb
index d4279626e75..6139529013f 100644
--- a/spec/views/projects/jobs/show.html.haml_spec.rb
+++ b/spec/views/projects/jobs/show.html.haml_spec.rb
@@ -185,6 +185,31 @@ describe 'projects/jobs/show' do
end
end
+ context 'when incomplete trigger_request is used' do
+ before do
+ build.trigger_request = FactoryGirl.build(:ci_trigger_request, trigger: nil)
+ end
+
+ it 'test should not render token block' do
+ render
+
+ expect(rendered).not_to have_content('Token')
+ end
+ end
+
+ context 'when complete trigger_request is used' do
+ before do
+ build.trigger_request = FactoryGirl.build(:ci_trigger_request)
+ end
+
+ it 'should render token' do
+ render
+
+ expect(rendered).to have_content('Token')
+ expect(rendered).to have_content(build.trigger_request.trigger.short_token)
+ end
+ end
+
describe 'commit title in sidebar' do
let(:commit_title) { project.commit.title }
diff --git a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
index c757ccf02d3..95f0be49412 100644
--- a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
@@ -35,7 +35,7 @@ describe 'projects/pipelines_settings/_show' do
context 'when kubernetes is active' do
before do
- project.build_kubernetes_service(active: true)
+ create(:kubernetes_service, project: project)
end
context 'when auto devops domain is not defined' do
diff --git a/spec/views/shared/issuable/_participants.html.haml.rb b/spec/views/shared/issuable/_participants.html.haml.rb
deleted file mode 100644
index 51059d4c0d7..00000000000
--- a/spec/views/shared/issuable/_participants.html.haml.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'spec_helper'
-require 'nokogiri'
-
-describe 'shared/issuable/_participants.html.haml' do
- let(:project) { create(:project) }
- let(:participants) { create_list(:user, 100) }
-
- before do
- allow(view).to receive_messages(project: project,
- participants: participants)
- end
-
- it 'renders lazy loaded avatars' do
- render 'shared/issuable/participants'
-
- html = Nokogiri::HTML(rendered)
-
- avatars = html.css('.participants-author img')
-
- avatars.each do |avatar|
- expect(avatar[:class]).to include('lazy')
- expect(avatar[:src]).to eql(LazyImageTagHelper.placeholder_image)
- expect(avatar[:"data-src"]).to match('http://www.gravatar.com/avatar/')
- end
- end
-end
diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb
index 11f208289db..8054ec11a48 100644
--- a/spec/workers/cluster_provision_worker_spec.rb
+++ b/spec/workers/cluster_provision_worker_spec.rb
@@ -2,11 +2,22 @@ require 'spec_helper'
describe ClusterProvisionWorker do
describe '#perform' do
- context 'when cluster exists' do
- let(:cluster) { create(:gcp_cluster) }
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
+ let(:provider) { create(:cluster_provider_gcp, :scheduled) }
it 'provision a cluster' do
- expect_any_instance_of(Ci::ProvisionClusterService).to receive(:execute)
+ expect_any_instance_of(Clusters::Gcp::ProvisionService).to receive(:execute)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
+
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute)
described_class.new.perform(cluster.id)
end
@@ -14,7 +25,7 @@ describe ClusterProvisionWorker do
context 'when cluster does not exist' do
it 'does not provision a cluster' do
- expect_any_instance_of(Ci::ProvisionClusterService).not_to receive(:execute)
+ expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute)
described_class.new.perform(123)
end
diff --git a/spec/workers/concerns/gitlab/github_import/notify_upon_death_spec.rb b/spec/workers/concerns/gitlab/github_import/notify_upon_death_spec.rb
new file mode 100644
index 00000000000..4b9aa9a7ef8
--- /dev/null
+++ b/spec/workers/concerns/gitlab/github_import/notify_upon_death_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::NotifyUponDeath do
+ let(:worker_class) do
+ Class.new do
+ include Sidekiq::Worker
+ include Gitlab::GithubImport::NotifyUponDeath
+ end
+ end
+
+ describe '.sidekiq_retries_exhausted' do
+ it 'notifies the JobWaiter when 3 arguments are given and the last is a String' do
+ job = { 'args' => [12, {}, '123abc'], 'jid' => '123' }
+
+ expect(Gitlab::JobWaiter)
+ .to receive(:notify)
+ .with('123abc', '123')
+
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+ end
+
+ it 'does not notify the JobWaiter when only 2 arguments are given' do
+ job = { 'args' => [12, {}], 'jid' => '123' }
+
+ expect(Gitlab::JobWaiter)
+ .not_to receive(:notify)
+
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+ end
+
+ it 'does not notify the JobWaiter when only 1 argument is given' do
+ job = { 'args' => [12], 'jid' => '123' }
+
+ expect(Gitlab::JobWaiter)
+ .not_to receive(:notify)
+
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+ end
+
+ it 'does not notify the JobWaiter when the last argument is not a String' do
+ job = { 'args' => [12, {}, 40], 'jid' => '123' }
+
+ expect(Gitlab::JobWaiter)
+ .not_to receive(:notify)
+
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+ end
+ end
+end
diff --git a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
new file mode 100644
index 00000000000..3ccf06f2d7d
--- /dev/null
+++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ObjectImporter do
+ let(:worker) do
+ Class.new do
+ include(Gitlab::GithubImport::ObjectImporter)
+
+ def counter_name
+ :dummy_counter
+ end
+
+ def counter_description
+ 'This is a counter'
+ end
+ end.new
+ end
+
+ describe '#import' do
+ it 'imports the object' do
+ representation_class = double(:representation_class)
+ importer_class = double(:importer_class)
+ importer_instance = double(:importer_instance)
+ representation = double(:representation)
+ project = double(:project, path_with_namespace: 'foo/bar')
+ client = double(:client)
+
+ expect(worker)
+ .to receive(:representation_class)
+ .and_return(representation_class)
+
+ expect(worker)
+ .to receive(:importer_class)
+ .and_return(importer_class)
+
+ expect(representation_class)
+ .to receive(:from_json_hash)
+ .with(an_instance_of(Hash))
+ .and_return(representation)
+
+ expect(importer_class)
+ .to receive(:new)
+ .with(representation, project, client)
+ .and_return(importer_instance)
+
+ expect(importer_instance)
+ .to receive(:execute)
+
+ expect(worker.counter)
+ .to receive(:increment)
+ .with(project: 'foo/bar')
+ .and_call_original
+
+ worker.import(project, client, { 'number' => 10 })
+ end
+ end
+
+ describe '#counter' do
+ it 'returns a Prometheus counter' do
+ expect(worker)
+ .to receive(:counter_name)
+ .and_call_original
+
+ expect(worker)
+ .to receive(:counter_description)
+ .and_call_original
+
+ worker.counter
+ end
+ end
+end
diff --git a/spec/workers/concerns/gitlab/github_import/queue_spec.rb b/spec/workers/concerns/gitlab/github_import/queue_spec.rb
new file mode 100644
index 00000000000..321ae3fe978
--- /dev/null
+++ b/spec/workers/concerns/gitlab/github_import/queue_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Queue do
+ it 'sets the Sidekiq options for the worker' do
+ worker = Class.new do
+ include Sidekiq::Worker
+ include Gitlab::GithubImport::Queue
+ end
+
+ expect(worker.sidekiq_options['queue']).to eq('github_importer')
+ end
+end
diff --git a/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb
new file mode 100644
index 00000000000..8de4059c4ae
--- /dev/null
+++ b/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ReschedulingMethods do
+ let(:worker) do
+ Class.new { include(Gitlab::GithubImport::ReschedulingMethods) }.new
+ end
+
+ describe '#perform' do
+ context 'with a non-existing project' do
+ it 'does not perform any work' do
+ expect(worker)
+ .not_to receive(:try_import)
+
+ worker.perform(-1, {})
+ end
+
+ it 'notifies any waiters so they do not wait forever' do
+ expect(worker)
+ .to receive(:notify_waiter)
+ .with('123')
+
+ worker.perform(-1, {}, '123')
+ end
+ end
+
+ context 'with an existing project' do
+ let(:project) { create(:project) }
+
+ it 'notifies any waiters upon successfully importing the data' do
+ expect(worker)
+ .to receive(:try_import)
+ .with(
+ an_instance_of(Project),
+ an_instance_of(Gitlab::GithubImport::Client),
+ { 'number' => 2 }
+ )
+ .and_return(true)
+
+ expect(worker)
+ .to receive(:notify_waiter).with('123')
+
+ worker.perform(project.id, { 'number' => 2 }, '123')
+ end
+
+ it 'reschedules itself if the data could not be imported' do
+ expect(worker)
+ .to receive(:try_import)
+ .with(
+ an_instance_of(Project),
+ an_instance_of(Gitlab::GithubImport::Client),
+ { 'number' => 2 }
+ )
+ .and_return(false)
+
+ expect(worker)
+ .not_to receive(:notify_waiter)
+
+ expect_any_instance_of(Gitlab::GithubImport::Client)
+ .to receive(:rate_limit_resets_in)
+ .and_return(14)
+
+ expect(worker.class)
+ .to receive(:perform_in)
+ .with(14, project.id, { 'number' => 2 }, '123')
+
+ worker.perform(project.id, { 'number' => 2 }, '123')
+ end
+ end
+ end
+
+ describe '#try_import' do
+ it 'returns true when the import succeeds' do
+ expect(worker)
+ .to receive(:import)
+ .with(10, 20)
+
+ expect(worker.try_import(10, 20)).to eq(true)
+ end
+
+ it 'returns false when the import fails due to hitting the GitHub API rate limit' do
+ expect(worker)
+ .to receive(:import)
+ .with(10, 20)
+ .and_raise(Gitlab::GithubImport::RateLimitError)
+
+ expect(worker.try_import(10, 20)).to eq(false)
+ end
+ end
+
+ describe '#notify_waiter' do
+ it 'notifies the waiter if a waiter key is specified' do
+ expect(worker)
+ .to receive(:jid)
+ .and_return('abc123')
+
+ expect(Gitlab::JobWaiter)
+ .to receive(:notify)
+ .with('123', 'abc123')
+
+ worker.notify_waiter('123')
+ end
+
+ it 'does not notify any waiters if no waiter key is specified' do
+ expect(Gitlab::JobWaiter)
+ .not_to receive(:notify)
+
+ worker.notify_waiter(nil)
+ end
+ end
+end
diff --git a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
new file mode 100644
index 00000000000..241e8a2b6d3
--- /dev/null
+++ b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::StageMethods do
+ let(:project) { create(:project) }
+ let(:worker) do
+ Class.new { include(Gitlab::GithubImport::StageMethods) }.new
+ end
+
+ describe '#perform' do
+ it 'returns if no project could be found' do
+ expect(worker).not_to receive(:try_import)
+
+ worker.perform(-1)
+ end
+
+ it 'imports the data when the project exists' do
+ allow(worker)
+ .to receive(:find_project)
+ .with(project.id)
+ .and_return(project)
+
+ expect(worker)
+ .to receive(:try_import)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Client),
+ an_instance_of(Project)
+ )
+
+ worker.perform(project.id)
+ end
+ end
+
+ describe '#try_import' do
+ it 'imports the project' do
+ client = double(:client)
+
+ expect(worker)
+ .to receive(:import)
+ .with(client, project)
+
+ worker.try_import(client, project)
+ end
+
+ it 'reschedules the worker if RateLimitError was raised' do
+ client = double(:client, rate_limit_resets_in: 10)
+
+ expect(worker)
+ .to receive(:import)
+ .with(client, project)
+ .and_raise(Gitlab::GithubImport::RateLimitError)
+
+ expect(worker.class)
+ .to receive(:perform_in)
+ .with(10, project.id)
+
+ worker.try_import(client, project)
+ end
+ end
+
+ describe '#find_project' do
+ it 'returns a Project for an existing ID' do
+ project.update_column(:import_status, 'started')
+
+ expect(worker.find_project(project.id)).to eq(project)
+ end
+
+ it 'returns nil for a project that failed importing' do
+ project.update_column(:import_status, 'failed')
+
+ expect(worker.find_project(project.id)).to be_nil
+ end
+
+ it 'returns nil for a non-existing project ID' do
+ expect(worker.find_project(-1)).to be_nil
+ end
+ end
+end
diff --git a/spec/workers/create_pipeline_worker_spec.rb b/spec/workers/create_pipeline_worker_spec.rb
new file mode 100644
index 00000000000..02cb0f46cb4
--- /dev/null
+++ b/spec/workers/create_pipeline_worker_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe CreatePipelineWorker do
+ describe '#perform' do
+ let(:worker) { described_class.new }
+
+ context 'when a project not found' do
+ it 'does not call the Service' do
+ expect(Ci::CreatePipelineService).not_to receive(:new)
+ expect { worker.perform(99, create(:user).id, 'master', :web) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'when a user not found' do
+ let(:project) { create(:project) }
+
+ it 'does not call the Service' do
+ expect(Ci::CreatePipelineService).not_to receive(:new)
+ expect { worker.perform(project.id, 99, project.default_branch, :web) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'when everything is ok' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:create_pipeline_service) { instance_double(Ci::CreatePipelineService) }
+
+ it 'calls the Service' do
+ expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: project.default_branch).and_return(create_pipeline_service)
+ expect(create_pipeline_service).to receive(:execute).with(:web, any_args)
+
+ worker.perform(project.id, user.id, project.default_branch, :web)
+ end
+ end
+ end
+end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index ab656d619f4..47297de738b 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -104,7 +104,7 @@ describe GitGarbageCollectWorker do
it_should_behave_like 'flushing ref caches', true
end
- context "with Gitaly turned off", skip_gitaly_mock: true do
+ context "with Gitaly turned off", :skip_gitaly_mock do
it_should_behave_like 'flushing ref caches', false
end
diff --git a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
new file mode 100644
index 00000000000..3be49a0dee8
--- /dev/null
+++ b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
@@ -0,0 +1,115 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_state do
+ let(:project) { create(:project, import_jid: '123') }
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ context 'when the project no longer exists' do
+ it 'does not perform any work' do
+ expect(worker).not_to receive(:wait_for_jobs)
+
+ worker.perform(-1, { '123' => 2 }, :finish)
+ end
+ end
+
+ context 'when there are remaining jobs' do
+ before do
+ allow(worker)
+ .to receive(:find_project)
+ .and_return(project)
+ end
+
+ it 'reschedules itself' do
+ expect(worker)
+ .to receive(:wait_for_jobs)
+ .with({ '123' => 2 })
+ .and_return({ '123' => 1 })
+
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(described_class::INTERVAL, project.id, { '123' => 1 }, :finish)
+
+ worker.perform(project.id, { '123' => 2 }, :finish)
+ end
+ end
+
+ context 'when there are no remaining jobs' do
+ before do
+ allow(worker)
+ .to receive(:find_project)
+ .and_return(project)
+
+ allow(worker)
+ .to receive(:wait_for_jobs)
+ .with({ '123' => 2 })
+ .and_return({})
+ end
+
+ it 'schedules the next stage' do
+ expect(project)
+ .to receive(:refresh_import_jid_expiration)
+
+ expect(Gitlab::GithubImport::Stage::FinishImportWorker)
+ .to receive(:perform_async)
+ .with(project.id)
+
+ worker.perform(project.id, { '123' => 2 }, :finish)
+ end
+
+ it 'raises KeyError when the stage name is invalid' do
+ expect { worker.perform(project.id, { '123' => 2 }, :kittens) }
+ .to raise_error(KeyError)
+ end
+ end
+ end
+
+ describe '#wait_for_jobs' do
+ it 'waits for jobs to complete and returns a new pair of keys to wait for' do
+ waiter1 = double(:waiter1, jobs_remaining: 1, key: '123')
+ waiter2 = double(:waiter2, jobs_remaining: 0, key: '456')
+
+ expect(Gitlab::JobWaiter)
+ .to receive(:new)
+ .ordered
+ .with(2, '123')
+ .and_return(waiter1)
+
+ expect(Gitlab::JobWaiter)
+ .to receive(:new)
+ .ordered
+ .with(1, '456')
+ .and_return(waiter2)
+
+ expect(waiter1)
+ .to receive(:wait)
+ .with(described_class::BLOCKING_WAIT_TIME)
+
+ expect(waiter2)
+ .to receive(:wait)
+ .with(described_class::BLOCKING_WAIT_TIME)
+
+ new_waiters = worker.wait_for_jobs({ '123' => 2, '456' => 1 })
+
+ expect(new_waiters).to eq({ '123' => 1 })
+ end
+ end
+
+ describe '#find_project' do
+ it 'returns a Project' do
+ project.update_column(:import_status, 'started')
+
+ found = worker.find_project(project.id)
+
+ expect(found).to be_an_instance_of(Project)
+
+ # This test is there to make sure we only select the columns we care
+ # about.
+ expect(found.attributes).to eq({ 'id' => nil, 'import_jid' => '123' })
+ end
+
+ it 'returns nil if the project import is not running' do
+ expect(worker.find_project(project.id)).to be_nil
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
new file mode 100644
index 00000000000..7c8c665a9b3
--- /dev/null
+++ b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ImportDiffNoteWorker do
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports a diff note' do
+ project = double(:project, path_with_namespace: 'foo/bar')
+ client = double(:client)
+ importer = double(:importer)
+ hash = {
+ 'noteable_id' => 42,
+ 'path' => 'README.md',
+ 'commit_id' => '123abc',
+ 'diff_hunk' => "@@ -1 +1 @@\n-Hello\n+Hello world",
+ 'user' => { 'id' => 4, 'login' => 'alice' },
+ 'note' => 'Hello world',
+ 'created_at' => Time.zone.now.to_s,
+ 'updated_at' => Time.zone.now.to_s
+ }
+
+ expect(Gitlab::GithubImport::Importer::DiffNoteImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::DiffNote),
+ project,
+ client
+ )
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+
+ expect(worker.counter)
+ .to receive(:increment)
+ .with(project: 'foo/bar')
+ .and_call_original
+
+ worker.import(project, client, hash)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
new file mode 100644
index 00000000000..4116380ff4d
--- /dev/null
+++ b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ImportIssueWorker do
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports an issue' do
+ project = double(:project, path_with_namespace: 'foo/bar')
+ client = double(:client)
+ importer = double(:importer)
+ hash = {
+ 'iid' => 42,
+ 'title' => 'My Issue',
+ 'description' => 'This is my issue',
+ 'milestone_number' => 4,
+ 'state' => 'opened',
+ 'assignees' => [{ 'id' => 4, 'login' => 'alice' }],
+ 'label_names' => %w[bug],
+ 'user' => { 'id' => 4, 'login' => 'alice' },
+ 'created_at' => Time.zone.now.to_s,
+ 'updated_at' => Time.zone.now.to_s,
+ 'pull_request' => false
+ }
+
+ expect(Gitlab::GithubImport::Importer::IssueAndLabelLinksImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::Issue),
+ project,
+ client
+ )
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+
+ expect(worker.counter)
+ .to receive(:increment)
+ .with(project: 'foo/bar')
+ .and_call_original
+
+ worker.import(project, client, hash)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/import_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
new file mode 100644
index 00000000000..0ca825a722b
--- /dev/null
+++ b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ImportNoteWorker do
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports a note' do
+ project = double(:project, path_with_namespace: 'foo/bar')
+ client = double(:client)
+ importer = double(:importer)
+ hash = {
+ 'noteable_id' => 42,
+ 'noteable_type' => 'issues',
+ 'user' => { 'id' => 4, 'login' => 'alice' },
+ 'note' => 'Hello world',
+ 'created_at' => Time.zone.now.to_s,
+ 'updated_at' => Time.zone.now.to_s
+ }
+
+ expect(Gitlab::GithubImport::Importer::NoteImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::Note),
+ project,
+ client
+ )
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+
+ expect(worker.counter)
+ .to receive(:increment)
+ .with(project: 'foo/bar')
+ .and_call_original
+
+ worker.import(project, client, hash)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
new file mode 100644
index 00000000000..d49f560af42
--- /dev/null
+++ b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::ImportPullRequestWorker do
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports a pull request' do
+ project = double(:project, path_with_namespace: 'foo/bar')
+ client = double(:client)
+ importer = double(:importer)
+ hash = {
+ 'iid' => 42,
+ 'title' => 'My Pull Request',
+ 'description' => 'This is my pull request',
+ 'source_branch' => 'my-feature',
+ 'source_branch_sha' => '123abc',
+ 'target_branch' => 'master',
+ 'target_branch_sha' => '456def',
+ 'source_repository_id' => 400,
+ 'target_repository_id' => 200,
+ 'source_repository_owner' => 'alice',
+ 'state' => 'closed',
+ 'milestone_number' => 4,
+ 'user' => { 'id' => 4, 'login' => 'alice' },
+ 'assignee' => { 'id' => 4, 'login' => 'alice' },
+ 'created_at' => Time.zone.now.to_s,
+ 'updated_at' => Time.zone.now.to_s,
+ 'merged_at' => Time.zone.now.to_s
+ }
+
+ expect(Gitlab::GithubImport::Importer::PullRequestImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::PullRequest),
+ project,
+ client
+ )
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+
+ expect(worker.counter)
+ .to receive(:increment)
+ .with(project: 'foo/bar')
+ .and_call_original
+
+ worker.import(project, client, hash)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
new file mode 100644
index 00000000000..073c6d7a2f5
--- /dev/null
+++ b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
@@ -0,0 +1,95 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::RefreshImportJidWorker do
+ let(:worker) { described_class.new }
+
+ describe '.perform_in_the_future' do
+ it 'schedules a job in the future' do
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(1.minute.to_i, 10, '123')
+
+ described_class.perform_in_the_future(10, '123')
+ end
+ end
+
+ describe '#perform' do
+ let(:project) { create(:project, import_jid: '123abc') }
+
+ context 'when the project does not exist' do
+ it 'does nothing' do
+ expect(Gitlab::SidekiqStatus)
+ .not_to receive(:running?)
+
+ worker.perform(-1, '123')
+ end
+ end
+
+ context 'when the job is running' do
+ it 'refreshes the import JID and reschedules itself' do
+ allow(worker)
+ .to receive(:find_project)
+ .with(project.id)
+ .and_return(project)
+
+ expect(Gitlab::SidekiqStatus)
+ .to receive(:running?)
+ .with('123')
+ .and_return(true)
+
+ expect(project)
+ .to receive(:refresh_import_jid_expiration)
+
+ expect(worker.class)
+ .to receive(:perform_in_the_future)
+ .with(project.id, '123')
+
+ worker.perform(project.id, '123')
+ end
+ end
+
+ context 'when the job is no longer running' do
+ it 'returns' do
+ allow(worker)
+ .to receive(:find_project)
+ .with(project.id)
+ .and_return(project)
+
+ expect(Gitlab::SidekiqStatus)
+ .to receive(:running?)
+ .with('123')
+ .and_return(false)
+
+ expect(project)
+ .not_to receive(:refresh_import_jid_expiration)
+
+ worker.perform(project.id, '123')
+ end
+ end
+ end
+
+ describe '#find_project' do
+ it 'returns a Project' do
+ project = create(:project, import_status: 'started')
+
+ expect(worker.find_project(project.id)).to be_an_instance_of(Project)
+ end
+
+ it 'only selects the import JID field' do
+ project = create(:project, import_status: 'started', import_jid: '123abc')
+
+ expect(worker.find_project(project.id).attributes)
+ .to eq({ 'id' => nil, 'import_jid' => '123abc' })
+ end
+
+ it 'returns nil for a project for which the import process failed' do
+ project = create(:project, import_status: 'failed')
+
+ expect(worker.find_project(project.id)).to be_nil
+ end
+
+ it 'returns nil for a non-existing project' do
+ expect(worker.find_project(-1)).to be_nil
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb b/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb
new file mode 100644
index 00000000000..91e0cddb5d8
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Stage::FinishImportWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ it 'marks the import as finished' do
+ expect(project).to receive(:after_import)
+ expect(worker).to receive(:report_import_time).with(project)
+
+ worker.import(double(:client), project)
+ end
+ end
+
+ describe '#report_import_time' do
+ it 'reports the total import time' do
+ expect(worker.histogram)
+ .to receive(:observe)
+ .with({ project: project.path_with_namespace }, a_kind_of(Numeric))
+ .and_call_original
+
+ expect(worker.counter)
+ .to receive(:increment)
+ .and_call_original
+
+ expect(worker.logger).to receive(:info).with(an_instance_of(String))
+
+ worker.report_import_time(project)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
new file mode 100644
index 00000000000..8c80d660287
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Stage::ImportBaseDataWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports the base data of a project' do
+ importer = double(:importer)
+ client = double(:client)
+
+ described_class::IMPORTERS.each do |klass|
+ expect(klass)
+ .to receive(:new)
+ .with(project, client)
+ .and_return(importer)
+
+ expect(importer).to receive(:execute)
+ end
+
+ expect(project).to receive(:refresh_import_jid_expiration)
+
+ expect(Gitlab::GithubImport::Stage::ImportPullRequestsWorker)
+ .to receive(:perform_async)
+ .with(project.id)
+
+ worker.import(client, project)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
new file mode 100644
index 00000000000..ab347f5b75b
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Stage::ImportIssuesAndDiffNotesWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports the issues and diff notes' do
+ client = double(:client)
+
+ described_class::IMPORTERS.each do |klass|
+ importer = double(:importer)
+ waiter = Gitlab::JobWaiter.new(2, '123')
+
+ expect(klass)
+ .to receive(:new)
+ .with(project, client)
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+ .and_return(waiter)
+ end
+
+ expect(Gitlab::GithubImport::AdvanceStageWorker)
+ .to receive(:perform_async)
+ .with(project.id, { '123' => 2 }, :notes)
+
+ worker.import(client, project)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
new file mode 100644
index 00000000000..098d2d55386
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Stage::ImportNotesWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports all the notes' do
+ importer = double(:importer)
+ client = double(:client)
+ waiter = Gitlab::JobWaiter.new(2, '123')
+
+ expect(Gitlab::GithubImport::Importer::NotesImporter)
+ .to receive(:new)
+ .with(project, client)
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+ .and_return(waiter)
+
+ expect(Gitlab::GithubImport::AdvanceStageWorker)
+ .to receive(:perform_async)
+ .with(project.id, { '123' => 2 }, :finish)
+
+ worker.import(client, project)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
new file mode 100644
index 00000000000..2fc91a3e80a
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ it 'imports all the pull requests' do
+ importer = double(:importer)
+ client = double(:client)
+ waiter = Gitlab::JobWaiter.new(2, '123')
+
+ expect(Gitlab::GithubImport::Importer::PullRequestsImporter)
+ .to receive(:new)
+ .with(project, client)
+ .and_return(importer)
+
+ expect(importer)
+ .to receive(:execute)
+ .and_return(waiter)
+
+ expect(project)
+ .to receive(:refresh_import_jid_expiration)
+
+ expect(Gitlab::GithubImport::AdvanceStageWorker)
+ .to receive(:perform_async)
+ .with(project.id, { '123' => 2 }, :issues_and_diff_notes)
+
+ worker.import(client, project)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb
new file mode 100644
index 00000000000..adab535ac05
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Stage::ImportRepositoryWorker do
+ let(:project) { double(:project, id: 4) }
+ let(:worker) { described_class.new }
+
+ describe '#import' do
+ before do
+ expect(Gitlab::GithubImport::RefreshImportJidWorker)
+ .to receive(:perform_in_the_future)
+ .with(project.id, '123')
+
+ expect(worker)
+ .to receive(:jid)
+ .and_return('123')
+ end
+
+ context 'when the import succeeds' do
+ it 'schedules the importing of the base data' do
+ client = double(:client)
+
+ expect_any_instance_of(Gitlab::GithubImport::Importer::RepositoryImporter)
+ .to receive(:execute)
+ .and_return(true)
+
+ expect(Gitlab::GithubImport::Stage::ImportBaseDataWorker)
+ .to receive(:perform_async)
+ .with(project.id)
+
+ worker.import(client, project)
+ end
+ end
+
+ context 'when the import fails' do
+ it 'does not schedule the importing of the base data' do
+ client = double(:client)
+
+ expect_any_instance_of(Gitlab::GithubImport::Importer::RepositoryImporter)
+ .to receive(:execute)
+ .and_return(false)
+
+ expect(Gitlab::GithubImport::Stage::ImportBaseDataWorker)
+ .not_to receive(:perform_async)
+
+ worker.import(client, project)
+ end
+ end
+ end
+end
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index 75197039f5a..e7a4ac0f3d6 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -22,25 +22,32 @@ describe PipelineScheduleWorker do
end
context 'when there is a scheduled pipeline within next_run_at' do
- it 'creates a new pipeline' do
- expect { subject }.to change { project.pipelines.count }.by(1)
- expect(Ci::Pipeline.last).to be_schedule
+ shared_examples 'successful scheduling' do
+ it 'creates a new pipeline' do
+ expect { subject }.to change { project.pipelines.count }.by(1)
+ expect(Ci::Pipeline.last).to be_schedule
+
+ pipeline_schedule.reload
+ expect(pipeline_schedule.next_run_at).to be > Time.now
+ expect(pipeline_schedule).to eq(project.pipelines.last.pipeline_schedule)
+ expect(pipeline_schedule).to be_active
+ end
end
- it 'updates the next_run_at field' do
- subject
+ it_behaves_like 'successful scheduling'
- expect(pipeline_schedule.reload.next_run_at).to be > Time.now
- end
-
- it 'sets the schedule on the pipeline' do
- subject
+ context 'when the latest commit contains [ci skip]' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:git_commit_message)
+ .and_return('some commit [ci skip]')
+ end
- expect(project.pipelines.last.pipeline_schedule).to eq(pipeline_schedule)
+ it_behaves_like 'successful scheduling'
end
end
- context 'inactive schedule' do
+ context 'when the schedule is deactivated' do
before do
pipeline_schedule.deactivate!
end
diff --git a/spec/workers/project_migrate_hashed_storage_worker_spec.rb b/spec/workers/project_migrate_hashed_storage_worker_spec.rb
index f5226dee0ad..2e3951e7afc 100644
--- a/spec/workers/project_migrate_hashed_storage_worker_spec.rb
+++ b/spec/workers/project_migrate_hashed_storage_worker_spec.rb
@@ -1,29 +1,53 @@
require 'spec_helper'
-describe ProjectMigrateHashedStorageWorker do
+describe ProjectMigrateHashedStorageWorker, :clean_gitlab_redis_shared_state do
describe '#perform' do
let(:project) { create(:project, :empty_repo) }
let(:pending_delete_project) { create(:project, :empty_repo, pending_delete: true) }
- it 'skips when project no longer exists' do
- nonexistent_id = 999999999999
+ context 'when have exclusive lease' do
+ before do
+ lease = subject.lease_for(project.id)
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
- subject.perform(nonexistent_id)
- end
+ allow(Gitlab::ExclusiveLease).to receive(:new).and_return(lease)
+ allow(lease).to receive(:try_obtain).and_return(true)
+ end
+
+ it 'skips when project no longer exists' do
+ nonexistent_id = 999999999999
+
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+ subject.perform(nonexistent_id)
+ end
+
+ it 'skips when project is pending delete' do
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
- it 'skips when project is pending delete' do
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+ subject.perform(pending_delete_project.id)
+ end
- subject.perform(pending_delete_project.id)
+ it 'delegates removal to service class' do
+ service = double('service')
+ expect(::Projects::HashedStorageMigrationService).to receive(:new).with(project, subject.logger).and_return(service)
+ expect(service).to receive(:execute)
+
+ subject.perform(project.id)
+ end
end
- it 'delegates removal to service class' do
- service = double('service')
- expect(::Projects::HashedStorageMigrationService).to receive(:new).with(project, subject.logger).and_return(service)
- expect(service).to receive(:execute)
+ context 'when dont have exclusive lease' do
+ before do
+ lease = subject.lease_for(project.id)
+
+ allow(Gitlab::ExclusiveLease).to receive(:new).and_return(lease)
+ allow(lease).to receive(:try_obtain).and_return(false)
+ end
+
+ it 'skips when dont have lease' do
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
- subject.perform(project.id)
+ subject.perform(project.id)
+ end
end
end
end
diff --git a/spec/workers/reactive_caching_worker_spec.rb b/spec/workers/reactive_caching_worker_spec.rb
index 5f4453c15d6..3da851de067 100644
--- a/spec/workers/reactive_caching_worker_spec.rb
+++ b/spec/workers/reactive_caching_worker_spec.rb
@@ -1,15 +1,28 @@
require 'spec_helper'
describe ReactiveCachingWorker do
- let(:project) { create(:kubernetes_project) }
- let(:service) { project.deployment_service }
- subject { described_class.new.perform("KubernetesService", service.id) }
+ let(:service) { project.deployment_platform }
describe '#perform' do
- it 'calls #exclusively_update_reactive_cache!' do
- expect_any_instance_of(KubernetesService).to receive(:exclusively_update_reactive_cache!)
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
- subject
+ it 'calls #exclusively_update_reactive_cache!' do
+ expect_any_instance_of(KubernetesService).to receive(:exclusively_update_reactive_cache!)
+
+ described_class.new.perform("KubernetesService", service.id)
+ end
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it 'calls #exclusively_update_reactive_cache!' do
+ expect_any_instance_of(Clusters::Platforms::Kubernetes).to receive(:exclusively_update_reactive_cache!)
+
+ described_class.new.perform("Clusters::Platforms::Kubernetes", service.id)
+ end
end
end
end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index d9e9409840f..e881ec37ae5 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -12,6 +12,28 @@ describe RepositoryForkWorker do
end
describe "#perform" do
+ describe 'when a worker was reset without cleanup' do
+ let(:jid) { '12345678' }
+ let(:started_project) { create(:project, :repository, :import_started) }
+
+ it 'creates a new repository from a fork' do
+ allow(subject).to receive(:jid).and_return(jid)
+
+ expect(shell).to receive(:fork_repository).with(
+ '/test/path',
+ project.full_path,
+ project.repository_storage_path,
+ fork_project.namespace.full_path
+ ).and_return(true)
+
+ subject.perform(
+ project.id,
+ '/test/path',
+ project.full_path,
+ fork_project.namespace.full_path)
+ end
+ end
+
it "creates a new repository from a fork" do
expect(shell).to receive(:fork_repository).with(
'/test/path',
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index 100dfc32bbe..0af537647ad 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -6,6 +6,23 @@ describe RepositoryImportWorker do
subject { described_class.new }
describe '#perform' do
+ context 'when worker was reset without cleanup' do
+ let(:jid) { '12345678' }
+ let(:started_project) { create(:project, :import_started, import_jid: jid) }
+
+ it 'imports the project successfully' do
+ allow(subject).to receive(:jid).and_return(jid)
+
+ expect_any_instance_of(Projects::ImportService).to receive(:execute)
+ .and_return({ status: :ok })
+
+ expect_any_instance_of(Repository).to receive(:expire_emptiness_caches)
+ expect_any_instance_of(Project).to receive(:import_finish)
+
+ subject.perform(project.id)
+ end
+ end
+
context 'when the import was successful' do
it 'imports a project' do
expect_any_instance_of(Projects::ImportService).to receive(:execute)
@@ -42,5 +59,28 @@ describe RepositoryImportWorker do
expect(project.reload.import_status).to eq('failed')
end
end
+
+ context 'when using an asynchronous importer' do
+ it 'does not mark the import process as finished' do
+ service = double(:service)
+
+ allow(Projects::ImportService)
+ .to receive(:new)
+ .and_return(service)
+
+ allow(service)
+ .to receive(:execute)
+ .and_return(true)
+
+ allow(service)
+ .to receive(:async?)
+ .and_return(true)
+
+ expect_any_instance_of(Project)
+ .not_to receive(:import_finish)
+
+ subject.perform(project.id)
+ end
+ end
end
end
diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb
index ac6f4fefb4e..bdc64c6785b 100644
--- a/spec/workers/stuck_ci_jobs_worker_spec.rb
+++ b/spec/workers/stuck_ci_jobs_worker_spec.rb
@@ -105,8 +105,8 @@ describe StuckCiJobsWorker do
job.project.update(pending_delete: true)
end
- it 'does not drop job' do
- expect_any_instance_of(Ci::Build).not_to receive(:drop)
+ it 'does drop job' do
+ expect_any_instance_of(Ci::Build).to receive(:drop).and_call_original
worker.perform
end
end
@@ -117,7 +117,7 @@ describe StuckCiJobsWorker do
let(:worker2) { described_class.new }
it 'is guard by exclusive lease when executed concurrently' do
- expect(worker).to receive(:drop).at_least(:once)
+ expect(worker).to receive(:drop).at_least(:once).and_call_original
expect(worker2).not_to receive(:drop)
worker.perform
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(false)
@@ -125,8 +125,8 @@ describe StuckCiJobsWorker do
end
it 'can be executed in sequence' do
- expect(worker).to receive(:drop).at_least(:once)
- expect(worker2).to receive(:drop).at_least(:once)
+ expect(worker).to receive(:drop).at_least(:once).and_call_original
+ expect(worker2).to receive(:drop).at_least(:once).and_call_original
worker.perform
worker2.perform
end
diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb
index a5ad78393c9..f8b55e873df 100644
--- a/spec/workers/stuck_merge_jobs_worker_spec.rb
+++ b/spec/workers/stuck_merge_jobs_worker_spec.rb
@@ -12,8 +12,13 @@ describe StuckMergeJobsWorker do
worker.perform
- expect(mr_with_sha.reload).to be_merged
- expect(mr_without_sha.reload).to be_opened
+ mr_with_sha.reload
+ mr_without_sha.reload
+
+ expect(mr_with_sha).to be_merged
+ expect(mr_without_sha).to be_opened
+ expect(mr_with_sha.merge_jid).to be_present
+ expect(mr_without_sha.merge_jid).to be_nil
end
it 'updates merge request to opened when locked but has not been merged' do
diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb
index 558ff9109ec..0fa19ac84bb 100644
--- a/spec/workers/update_merge_requests_worker_spec.rb
+++ b/spec/workers/update_merge_requests_worker_spec.rb
@@ -23,5 +23,17 @@ describe UpdateMergeRequestsWorker do
perform
end
+
+ context 'when slow' do
+ before do
+ stub_const("UpdateMergeRequestsWorker::LOG_TIME_THRESHOLD", -1)
+ end
+
+ it 'logs debug info' do
+ expect(Rails.logger).to receive(:info).with(a_string_matching(/\AUpdateMergeRequestsWorker#perform.*project_id=#{project.id},user_id=#{user.id},oldrev=#{oldrev},newrev=#{newrev},ref=#{ref}/))
+
+ perform
+ end
+ end
end
end
diff --git a/spec/workers/wait_for_cluster_creation_worker_spec.rb b/spec/workers/wait_for_cluster_creation_worker_spec.rb
index dcd4a3b9aec..0e92b298178 100644
--- a/spec/workers/wait_for_cluster_creation_worker_spec.rb
+++ b/spec/workers/wait_for_cluster_creation_worker_spec.rb
@@ -2,65 +2,32 @@ require 'spec_helper'
describe WaitForClusterCreationWorker do
describe '#perform' do
- context 'when cluster exists' do
- let(:cluster) { create(:gcp_cluster) }
- let(:operation) { double }
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
+ let(:provider) { create(:cluster_provider_gcp, :creating) }
- before do
- allow(operation).to receive(:status).and_return(status)
- allow(operation).to receive(:start_time).and_return(1.minute.ago)
- allow(operation).to receive(:status_message).and_return('error')
- allow_any_instance_of(Ci::FetchGcpOperationService).to receive(:execute).and_yield(operation)
- end
-
- context 'when operation status is RUNNING' do
- let(:status) { 'RUNNING' }
-
- it 'reschedules worker' do
- expect(described_class).to receive(:perform_in)
-
- described_class.new.perform(cluster.id)
- end
-
- context 'when operation timeout' do
- before do
- allow(operation).to receive(:start_time).and_return(30.minutes.ago.utc)
- end
-
- it 'sets an error message on cluster' do
- described_class.new.perform(cluster.id)
+ it 'provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).to receive(:execute)
- expect(cluster.reload).to be_errored
- end
- end
- end
-
- context 'when operation status is DONE' do
- let(:status) { 'DONE' }
-
- it 'finalizes cluster creation' do
- expect_any_instance_of(Ci::FinalizeClusterCreationService).to receive(:execute)
-
- described_class.new.perform(cluster.id)
- end
+ described_class.new.perform(cluster.id)
end
+ end
- context 'when operation status is others' do
- let(:status) { 'others' }
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, provider_type: :user) }
- it 'sets an error message on cluster' do
- described_class.new.perform(cluster.id)
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).not_to receive(:execute)
- expect(cluster.reload).to be_errored
- end
+ described_class.new.perform(cluster.id)
end
end
context 'when cluster does not exist' do
it 'does not provision a cluster' do
- expect_any_instance_of(Ci::FetchGcpOperationService).not_to receive(:execute)
+ expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).not_to receive(:execute)
- described_class.new.perform(1234)
+ described_class.new.perform(123)
end
end
end
diff --git a/vendor/assets/javascripts/autosize.js b/vendor/assets/javascripts/autosize.js
deleted file mode 100644
index cfa49e72c50..00000000000
--- a/vendor/assets/javascripts/autosize.js
+++ /dev/null
@@ -1,243 +0,0 @@
-/*!
- Autosize 3.0.14
- license: MIT
- http://www.jacklmoore.com/autosize
-*/
-(function (global, factory) {
- if (typeof define === 'function' && define.amd) {
- define(['exports', 'module'], factory);
- } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') {
- factory(exports, module);
- } else {
- var mod = {
- exports: {}
- };
- factory(mod.exports, mod);
- global.autosize = mod.exports;
- }
-})(this, function (exports, module) {
- 'use strict';
-
- var set = typeof Set === 'function' ? new Set() : (function () {
- var list = [];
-
- return {
- has: function has(key) {
- return Boolean(list.indexOf(key) > -1);
- },
- add: function add(key) {
- list.push(key);
- },
- 'delete': function _delete(key) {
- list.splice(list.indexOf(key), 1);
- } };
- })();
-
- function assign(ta) {
- var _ref = arguments[1] === undefined ? {} : arguments[1];
-
- var _ref$setOverflowX = _ref.setOverflowX;
- var setOverflowX = _ref$setOverflowX === undefined ? true : _ref$setOverflowX;
- var _ref$setOverflowY = _ref.setOverflowY;
- var setOverflowY = _ref$setOverflowY === undefined ? true : _ref$setOverflowY;
-
- if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has(ta)) return;
-
- var heightOffset = null;
- var overflowY = null;
- var clientWidth = ta.clientWidth;
-
- function init() {
- var style = window.getComputedStyle(ta, null);
-
- overflowY = style.overflowY;
-
- if (style.resize === 'vertical') {
- ta.style.resize = 'none';
- } else if (style.resize === 'both') {
- ta.style.resize = 'horizontal';
- }
-
- if (style.boxSizing === 'content-box') {
- heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom));
- } else {
- heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
- }
- // Fix when a textarea is not on document body and heightOffset is Not a Number
- if (isNaN(heightOffset)) {
- heightOffset = 0;
- }
-
- update();
- }
-
- function changeOverflow(value) {
- {
- // Chrome/Safari-specific fix:
- // When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
- // made available by removing the scrollbar. The following forces the necessary text reflow.
- var width = ta.style.width;
- ta.style.width = '0px';
- // Force reflow:
- /* jshint ignore:start */
- ta.offsetWidth;
- /* jshint ignore:end */
- ta.style.width = width;
- }
-
- overflowY = value;
-
- if (setOverflowY) {
- ta.style.overflowY = value;
- }
-
- resize();
- }
-
- function resize() {
- var htmlTop = window.pageYOffset;
- var bodyTop = document.body.scrollTop;
- var originalHeight = ta.style.height;
-
- ta.style.height = 'auto';
-
- var endHeight = ta.scrollHeight + heightOffset;
-
- if (ta.scrollHeight === 0) {
- // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
- ta.style.height = originalHeight;
- return;
- }
-
- ta.style.height = endHeight + 'px';
-
- // used to check if an update is actually necessary on window.resize
- clientWidth = ta.clientWidth;
-
- // prevents scroll-position jumping
- document.documentElement.scrollTop = htmlTop;
- document.body.scrollTop = bodyTop;
- }
-
- function update() {
- var startHeight = ta.style.height;
-
- resize();
-
- var style = window.getComputedStyle(ta, null);
-
- if (style.height !== ta.style.height) {
- if (overflowY !== 'visible') {
- changeOverflow('visible');
- }
- } else {
- if (overflowY !== 'hidden') {
- changeOverflow('hidden');
- }
- }
-
- if (startHeight !== ta.style.height) {
- var evt = document.createEvent('Event');
- evt.initEvent('autosize:resized', true, false);
- ta.dispatchEvent(evt);
- }
- }
-
- var pageResize = function pageResize() {
- if (ta.clientWidth !== clientWidth) {
- update();
- }
- };
-
- var destroy = (function (style) {
- window.removeEventListener('resize', pageResize, false);
- ta.removeEventListener('input', update, false);
- ta.removeEventListener('keyup', update, false);
- ta.removeEventListener('autosize:destroy', destroy, false);
- ta.removeEventListener('autosize:update', update, false);
- set['delete'](ta);
-
- Object.keys(style).forEach(function (key) {
- ta.style[key] = style[key];
- });
- }).bind(ta, {
- height: ta.style.height,
- resize: ta.style.resize,
- overflowY: ta.style.overflowY,
- overflowX: ta.style.overflowX,
- wordWrap: ta.style.wordWrap });
-
- ta.addEventListener('autosize:destroy', destroy, false);
-
- // IE9 does not fire onpropertychange or oninput for deletions,
- // so binding to onkeyup to catch most of those events.
- // There is no way that I know of to detect something like 'cut' in IE9.
- if ('onpropertychange' in ta && 'oninput' in ta) {
- ta.addEventListener('keyup', update, false);
- }
-
- window.addEventListener('resize', pageResize, false);
- ta.addEventListener('input', update, false);
- ta.addEventListener('autosize:update', update, false);
- set.add(ta);
-
- if (setOverflowX) {
- ta.style.overflowX = 'hidden';
- ta.style.wordWrap = 'break-word';
- }
-
- init();
- }
-
- function destroy(ta) {
- if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
- var evt = document.createEvent('Event');
- evt.initEvent('autosize:destroy', true, false);
- ta.dispatchEvent(evt);
- }
-
- function update(ta) {
- if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
- var evt = document.createEvent('Event');
- evt.initEvent('autosize:update', true, false);
- ta.dispatchEvent(evt);
- }
-
- var autosize = null;
-
- // Do nothing in Node.js environment and IE8 (or lower)
- if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
- autosize = function (el) {
- return el;
- };
- autosize.destroy = function (el) {
- return el;
- };
- autosize.update = function (el) {
- return el;
- };
- } else {
- autosize = function (el, options) {
- if (el) {
- Array.prototype.forEach.call(el.length ? el : [el], function (x) {
- return assign(x, options);
- });
- }
- return el;
- };
- autosize.destroy = function (el) {
- if (el) {
- Array.prototype.forEach.call(el.length ? el : [el], destroy);
- }
- return el;
- };
- autosize.update = function (el) {
- if (el) {
- Array.prototype.forEach.call(el.length ? el : [el], update);
- }
- return el;
- };
- }
-
- module.exports = autosize;
-}); \ No newline at end of file
diff --git a/vendor/assets/javascripts/clipboard.js b/vendor/assets/javascripts/clipboard.js
deleted file mode 100644
index 39d7d2306f8..00000000000
--- a/vendor/assets/javascripts/clipboard.js
+++ /dev/null
@@ -1,621 +0,0 @@
-/*!
- * clipboard.js v1.4.2
- * https://zenorocha.github.io/clipboard.js
- *
- * Licensed MIT © Zeno Rocha
- */
-(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-/**
- * Module dependencies.
- */
-
-var closest = require('closest')
- , event = require('component-event');
-
-/**
- * Delegate event `type` to `selector`
- * and invoke `fn(e)`. A callback function
- * is returned which may be passed to `.unbind()`.
- *
- * @param {Element} el
- * @param {String} selector
- * @param {String} type
- * @param {Function} fn
- * @param {Boolean} capture
- * @return {Function}
- * @api public
- */
-
-// Some events don't bubble, so we want to bind to the capture phase instead
-// when delegating.
-var forceCaptureEvents = ['focus', 'blur'];
-
-exports.bind = function(el, selector, type, fn, capture){
- if (forceCaptureEvents.indexOf(type) !== -1) capture = true;
-
- return event.bind(el, type, function(e){
- var target = e.target || e.srcElement;
- e.delegateTarget = closest(target, selector, true, el);
- if (e.delegateTarget) fn.call(el, e);
- }, capture);
-};
-
-/**
- * Unbind event `type`'s callback `fn`.
- *
- * @param {Element} el
- * @param {String} type
- * @param {Function} fn
- * @param {Boolean} capture
- * @api public
- */
-
-exports.unbind = function(el, type, fn, capture){
- if (forceCaptureEvents.indexOf(type) !== -1) capture = true;
-
- event.unbind(el, type, fn, capture);
-};
-
-},{"closest":2,"component-event":4}],2:[function(require,module,exports){
-var matches = require('matches-selector')
-
-module.exports = function (element, selector, checkYoSelf) {
- var parent = checkYoSelf ? element : element.parentNode
-
- while (parent && parent !== document) {
- if (matches(parent, selector)) return parent;
- parent = parent.parentNode
- }
-}
-
-},{"matches-selector":3}],3:[function(require,module,exports){
-
-/**
- * Element prototype.
- */
-
-var proto = Element.prototype;
-
-/**
- * Vendor function.
- */
-
-var vendor = proto.matchesSelector
- || proto.webkitMatchesSelector
- || proto.mozMatchesSelector
- || proto.msMatchesSelector
- || proto.oMatchesSelector;
-
-/**
- * Expose `match()`.
- */
-
-module.exports = match;
-
-/**
- * Match `el` to `selector`.
- *
- * @param {Element} el
- * @param {String} selector
- * @return {Boolean}
- * @api public
- */
-
-function match(el, selector) {
- if (vendor) return vendor.call(el, selector);
- var nodes = el.parentNode.querySelectorAll(selector);
- for (var i = 0; i < nodes.length; ++i) {
- if (nodes[i] == el) return true;
- }
- return false;
-}
-},{}],4:[function(require,module,exports){
-var bind = window.addEventListener ? 'addEventListener' : 'attachEvent',
- unbind = window.removeEventListener ? 'removeEventListener' : 'detachEvent',
- prefix = bind !== 'addEventListener' ? 'on' : '';
-
-/**
- * Bind `el` event `type` to `fn`.
- *
- * @param {Element} el
- * @param {String} type
- * @param {Function} fn
- * @param {Boolean} capture
- * @return {Function}
- * @api public
- */
-
-exports.bind = function(el, type, fn, capture){
- el[bind](prefix + type, fn, capture || false);
- return fn;
-};
-
-/**
- * Unbind `el` event `type`'s callback `fn`.
- *
- * @param {Element} el
- * @param {String} type
- * @param {Function} fn
- * @param {Boolean} capture
- * @return {Function}
- * @api public
- */
-
-exports.unbind = function(el, type, fn, capture){
- el[unbind](prefix + type, fn, capture || false);
- return fn;
-};
-},{}],5:[function(require,module,exports){
-function E () {
- // Keep this empty so it's easier to inherit from
- // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
-}
-
-E.prototype = {
- on: function (name, callback, ctx) {
- var e = this.e || (this.e = {});
-
- (e[name] || (e[name] = [])).push({
- fn: callback,
- ctx: ctx
- });
-
- return this;
- },
-
- once: function (name, callback, ctx) {
- var self = this;
- var fn = function () {
- self.off(name, fn);
- callback.apply(ctx, arguments);
- };
-
- return this.on(name, fn, ctx);
- },
-
- emit: function (name) {
- var data = [].slice.call(arguments, 1);
- var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
- var i = 0;
- var len = evtArr.length;
-
- for (i; i < len; i++) {
- evtArr[i].fn.apply(evtArr[i].ctx, data);
- }
-
- return this;
- },
-
- off: function (name, callback) {
- var e = this.e || (this.e = {});
- var evts = e[name];
- var liveEvents = [];
-
- if (evts && callback) {
- for (var i = 0, len = evts.length; i < len; i++) {
- if (evts[i].fn !== callback) liveEvents.push(evts[i]);
- }
- }
-
- // Remove event from queue to prevent memory leak
- // Suggested by https://github.com/lazd
- // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
-
- (liveEvents.length)
- ? e[name] = liveEvents
- : delete e[name];
-
- return this;
- }
-};
-
-module.exports = E;
-
-},{}],6:[function(require,module,exports){
-/**
- * Inner class which performs selection from either `text` or `target`
- * properties and then executes copy or cut operations.
- */
-'use strict';
-
-exports.__esModule = true;
-
-var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
-
-function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
-
-var ClipboardAction = (function () {
- /**
- * @param {Object} options
- */
-
- function ClipboardAction(options) {
- _classCallCheck(this, ClipboardAction);
-
- this.resolveOptions(options);
- this.initSelection();
- }
-
- /**
- * Defines base properties passed from constructor.
- * @param {Object} options
- */
-
- ClipboardAction.prototype.resolveOptions = function resolveOptions() {
- var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
-
- this.action = options.action;
- this.emitter = options.emitter;
- this.target = options.target;
- this.text = options.text;
- this.trigger = options.trigger;
-
- this.selectedText = '';
- };
-
- /**
- * Decides which selection strategy is going to be applied based
- * on the existence of `text` and `target` properties.
- */
-
- ClipboardAction.prototype.initSelection = function initSelection() {
- if (this.text && this.target) {
- throw new Error('Multiple attributes declared, use either "target" or "text"');
- } else if (this.text) {
- this.selectFake();
- } else if (this.target) {
- this.selectTarget();
- } else {
- throw new Error('Missing required attributes, use either "target" or "text"');
- }
- };
-
- /**
- * Creates a fake textarea element, sets its value from `text` property,
- * and makes a selection on it.
- */
-
- ClipboardAction.prototype.selectFake = function selectFake() {
- var _this = this;
-
- this.removeFake();
-
- this.fakeHandler = document.body.addEventListener('click', function () {
- return _this.removeFake();
- });
-
- this.fakeElem = document.createElement('textarea');
- this.fakeElem.style.position = 'absolute';
- this.fakeElem.style.left = '-9999px';
- this.fakeElem.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
- this.fakeElem.setAttribute('readonly', '');
- this.fakeElem.value = this.text;
- this.selectedText = this.text;
-
- document.body.appendChild(this.fakeElem);
-
- this.fakeElem.select();
- this.copyText();
- };
-
- /**
- * Only removes the fake element after another click event, that way
- * a user can hit `Ctrl+C` to copy because selection still exists.
- */
-
- ClipboardAction.prototype.removeFake = function removeFake() {
- if (this.fakeHandler) {
- document.body.removeEventListener('click');
- this.fakeHandler = null;
- }
-
- if (this.fakeElem) {
- document.body.removeChild(this.fakeElem);
- this.fakeElem = null;
- }
- };
-
- /**
- * Selects the content from element passed on `target` property.
- */
-
- ClipboardAction.prototype.selectTarget = function selectTarget() {
- if (this.target.nodeName === 'INPUT' || this.target.nodeName === 'TEXTAREA') {
- this.target.select();
- this.selectedText = this.target.value;
- } else {
- var range = document.createRange();
- var selection = window.getSelection();
-
- selection.removeAllRanges();
- range.selectNodeContents(this.target);
- selection.addRange(range);
- this.selectedText = selection.toString();
- }
-
- this.copyText();
- };
-
- /**
- * Executes the copy operation based on the current selection.
- */
-
- ClipboardAction.prototype.copyText = function copyText() {
- var succeeded = undefined;
-
- try {
- succeeded = document.execCommand(this.action);
- } catch (err) {
- succeeded = false;
- }
-
- this.handleResult(succeeded);
- };
-
- /**
- * Fires an event based on the copy operation result.
- * @param {Boolean} succeeded
- */
-
- ClipboardAction.prototype.handleResult = function handleResult(succeeded) {
- if (succeeded) {
- this.emitter.emit('success', {
- action: this.action,
- text: this.selectedText,
- trigger: this.trigger,
- clearSelection: this.clearSelection.bind(this)
- });
- } else {
- this.emitter.emit('error', {
- action: this.action,
- trigger: this.trigger,
- clearSelection: this.clearSelection.bind(this)
- });
- }
- };
-
- /**
- * Removes current selection and focus from `target` element.
- */
-
- ClipboardAction.prototype.clearSelection = function clearSelection() {
- if (this.target) {
- this.target.blur();
- }
-
- window.getSelection().removeAllRanges();
- };
-
- /**
- * Sets the `action` to be performed which can be either 'copy' or 'cut'.
- * @param {String} action
- */
-
- /**
- * Destroy lifecycle.
- */
-
- ClipboardAction.prototype.destroy = function destroy() {
- this.removeFake();
- };
-
- _createClass(ClipboardAction, [{
- key: 'action',
- set: function set() {
- var action = arguments.length <= 0 || arguments[0] === undefined ? 'copy' : arguments[0];
-
- this._action = action;
-
- if (this._action !== 'copy' && this._action !== 'cut') {
- throw new Error('Invalid "action" value, use either "copy" or "cut"');
- }
- },
-
- /**
- * Gets the `action` property.
- * @return {String}
- */
- get: function get() {
- return this._action;
- }
-
- /**
- * Sets the `target` property using an element
- * that will be have its content copied.
- * @param {Element} target
- */
- }, {
- key: 'target',
- set: function set(target) {
- if (target !== undefined) {
- if (target && typeof target === 'object' && target.nodeType === 1) {
- this._target = target;
- } else {
- throw new Error('Invalid "target" value, use a valid Element');
- }
- }
- },
-
- /**
- * Gets the `target` property.
- * @return {String|HTMLElement}
- */
- get: function get() {
- return this._target;
- }
- }]);
-
- return ClipboardAction;
-})();
-
-exports['default'] = ClipboardAction;
-module.exports = exports['default'];
-
-},{}],7:[function(require,module,exports){
-'use strict';
-
-exports.__esModule = true;
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
-
-function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
-
-function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
-
-var _clipboardAction = require('./clipboard-action');
-
-var _clipboardAction2 = _interopRequireDefault(_clipboardAction);
-
-var _delegateEvents = require('delegate-events');
-
-var _delegateEvents2 = _interopRequireDefault(_delegateEvents);
-
-var _tinyEmitter = require('tiny-emitter');
-
-var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter);
-
-/**
- * Base class which takes a selector, delegates a click event to it,
- * and instantiates a new `ClipboardAction` on each click.
- */
-
-var Clipboard = (function (_Emitter) {
- _inherits(Clipboard, _Emitter);
-
- /**
- * @param {String} selector
- * @param {Object} options
- */
-
- function Clipboard(selector, options) {
- _classCallCheck(this, Clipboard);
-
- _Emitter.call(this);
-
- this.resolveOptions(options);
- this.delegateClick(selector);
- }
-
- /**
- * Helper function to retrieve attribute value.
- * @param {String} suffix
- * @param {Element} element
- */
-
- /**
- * Defines if attributes would be resolved using internal setter functions
- * or custom functions that were passed in the constructor.
- * @param {Object} options
- */
-
- Clipboard.prototype.resolveOptions = function resolveOptions() {
- var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
-
- this.action = typeof options.action === 'function' ? options.action : this.defaultAction;
- this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;
- this.text = typeof options.text === 'function' ? options.text : this.defaultText;
- };
-
- /**
- * Delegates a click event on the passed selector.
- * @param {String} selector
- */
-
- Clipboard.prototype.delegateClick = function delegateClick(selector) {
- var _this = this;
-
- this.binding = _delegateEvents2['default'].bind(document.body, selector, 'click', function (e) {
- return _this.onClick(e);
- });
- };
-
- /**
- * Undelegates a click event on body.
- * @param {String} selector
- */
-
- Clipboard.prototype.undelegateClick = function undelegateClick() {
- _delegateEvents2['default'].unbind(document.body, 'click', this.binding);
- };
-
- /**
- * Defines a new `ClipboardAction` on each click event.
- * @param {Event} e
- */
-
- Clipboard.prototype.onClick = function onClick(e) {
- if (this.clipboardAction) {
- this.clipboardAction = null;
- }
-
- this.clipboardAction = new _clipboardAction2['default']({
- action: this.action(e.delegateTarget),
- target: this.target(e.delegateTarget),
- text: this.text(e.delegateTarget),
- trigger: e.delegateTarget,
- emitter: this
- });
- };
-
- /**
- * Default `action` lookup function.
- * @param {Element} trigger
- */
-
- Clipboard.prototype.defaultAction = function defaultAction(trigger) {
- return getAttributeValue('action', trigger);
- };
-
- /**
- * Default `target` lookup function.
- * @param {Element} trigger
- */
-
- Clipboard.prototype.defaultTarget = function defaultTarget(trigger) {
- var selector = getAttributeValue('target', trigger);
-
- if (selector) {
- return document.querySelector(selector);
- }
- };
-
- /**
- * Default `text` lookup function.
- * @param {Element} trigger
- */
-
- Clipboard.prototype.defaultText = function defaultText(trigger) {
- return getAttributeValue('text', trigger);
- };
-
- /**
- * Destroy lifecycle.
- */
-
- Clipboard.prototype.destroy = function destroy() {
- this.undelegateClick();
-
- if (this.clipboardAction) {
- this.clipboardAction.destroy();
- this.clipboardAction = null;
- }
- };
-
- return Clipboard;
-})(_tinyEmitter2['default']);
-
-function getAttributeValue(suffix, element) {
- var attribute = 'data-clipboard-' + suffix;
-
- if (!element.hasAttribute(attribute)) {
- return;
- }
-
- return element.getAttribute(attribute);
-}
-
-exports['default'] = Clipboard;
-module.exports = exports['default'];
-
-},{"./clipboard-action":6,"delegate-events":1,"tiny-emitter":5}]},{},[7])(7)
-});
diff --git a/vendor/assets/javascripts/fuzzaldrin-plus.js b/vendor/assets/javascripts/fuzzaldrin-plus.js
deleted file mode 100644
index 1985e3f8f6c..00000000000
--- a/vendor/assets/javascripts/fuzzaldrin-plus.js
+++ /dev/null
@@ -1,1161 +0,0 @@
-/*!
- * fuzzaldrin-plus.js - 0.3.1
- * https://github.com/jeancroy/fuzzaldrin-plus
- *
- * Copyright 2016 - Jean Christophe Roy
- * Released under the MIT license
- * https://github.com/jeancroy/fuzzaldrin-plus/raw/master/LICENSE.md
- */
-(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-fuzzaldrinPlus = require('fuzzaldrin-plus');
-
-},{"fuzzaldrin-plus":3}],2:[function(require,module,exports){
-(function() {
- var PathSeparator, legacy_scorer, pluckCandidates, scorer, sortCandidates;
-
- scorer = require('./scorer');
-
- legacy_scorer = require('./legacy');
-
- pluckCandidates = function(a) {
- return a.candidate;
- };
-
- sortCandidates = function(a, b) {
- return b.score - a.score;
- };
-
- PathSeparator = require('path').sep;
-
- module.exports = function(candidates, query, _arg) {
- var allowErrors, bAllowErrors, bKey, candidate, coreQuery, key, legacy, maxInners, maxResults, prepQuery, queryHasSlashes, score, scoredCandidates, spotLeft, string, _i, _j, _len, _len1, _ref;
- _ref = _arg != null ? _arg : {}, key = _ref.key, maxResults = _ref.maxResults, maxInners = _ref.maxInners, allowErrors = _ref.allowErrors, legacy = _ref.legacy;
- scoredCandidates = [];
- spotLeft = (maxInners != null) && maxInners > 0 ? maxInners : candidates.length;
- bAllowErrors = !!allowErrors;
- bKey = key != null;
- prepQuery = scorer.prepQuery(query);
- if (!legacy) {
- for (_i = 0, _len = candidates.length; _i < _len; _i++) {
- candidate = candidates[_i];
- string = bKey ? candidate[key] : candidate;
- if (!string) {
- continue;
- }
- score = scorer.score(string, query, prepQuery, bAllowErrors);
- if (score > 0) {
- scoredCandidates.push({
- candidate: candidate,
- score: score
- });
- if (!--spotLeft) {
- break;
- }
- }
- }
- } else {
- queryHasSlashes = prepQuery.depth > 0;
- coreQuery = prepQuery.core;
- for (_j = 0, _len1 = candidates.length; _j < _len1; _j++) {
- candidate = candidates[_j];
- string = key != null ? candidate[key] : candidate;
- if (!string) {
- continue;
- }
- score = legacy_scorer.score(string, coreQuery, queryHasSlashes);
- if (!queryHasSlashes) {
- score = legacy_scorer.basenameScore(string, coreQuery, score);
- }
- if (score > 0) {
- scoredCandidates.push({
- candidate: candidate,
- score: score
- });
- }
- }
- }
- scoredCandidates.sort(sortCandidates);
- candidates = scoredCandidates.map(pluckCandidates);
- if (maxResults != null) {
- candidates = candidates.slice(0, maxResults);
- }
- return candidates;
- };
-
-}).call(this);
-
-},{"./legacy":4,"./scorer":6,"path":7}],3:[function(require,module,exports){
-(function() {
- var PathSeparator, filter, legacy_scorer, matcher, prepQueryCache, scorer;
-
- scorer = require('./scorer');
-
- legacy_scorer = require('./legacy');
-
- filter = require('./filter');
-
- matcher = require('./matcher');
-
- PathSeparator = require('path').sep;
-
- prepQueryCache = null;
-
- module.exports = {
- filter: function(candidates, query, options) {
- if (!((query != null ? query.length : void 0) && (candidates != null ? candidates.length : void 0))) {
- return [];
- }
- return filter(candidates, query, options);
- },
- prepQuery: function(query) {
- return scorer.prepQuery(query);
- },
- score: function(string, query, prepQuery, _arg) {
- var allowErrors, coreQuery, legacy, queryHasSlashes, score, _ref;
- _ref = _arg != null ? _arg : {}, allowErrors = _ref.allowErrors, legacy = _ref.legacy;
- if (!((string != null ? string.length : void 0) && (query != null ? query.length : void 0))) {
- return 0;
- }
- if (prepQuery == null) {
- prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query));
- }
- if (!legacy) {
- score = scorer.score(string, query, prepQuery, !!allowErrors);
- } else {
- queryHasSlashes = prepQuery.depth > 0;
- coreQuery = prepQuery.core;
- score = legacy_scorer.score(string, coreQuery, queryHasSlashes);
- if (!queryHasSlashes) {
- score = legacy_scorer.basenameScore(string, coreQuery, score);
- }
- }
- return score;
- },
- match: function(string, query, prepQuery, _arg) {
- var allowErrors, baseMatches, matches, query_lw, string_lw, _i, _ref, _results;
- allowErrors = (_arg != null ? _arg : {}).allowErrors;
- if (!string) {
- return [];
- }
- if (!query) {
- return [];
- }
- if (string === query) {
- return (function() {
- _results = [];
- for (var _i = 0, _ref = string.length; 0 <= _ref ? _i < _ref : _i > _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); }
- return _results;
- }).apply(this);
- }
- if (prepQuery == null) {
- prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query));
- }
- if (!(allowErrors || scorer.isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
- return [];
- }
- string_lw = string.toLowerCase();
- query_lw = prepQuery.query_lw;
- matches = matcher.match(string, string_lw, prepQuery);
- if (matches.length === 0) {
- return matches;
- }
- if (string.indexOf(PathSeparator) > -1) {
- baseMatches = matcher.basenameMatch(string, string_lw, prepQuery);
- matches = matcher.mergeMatches(matches, baseMatches);
- }
- return matches;
- }
- };
-
-}).call(this);
-
-},{"./filter":2,"./legacy":4,"./matcher":5,"./scorer":6,"path":7}],4:[function(require,module,exports){
-(function() {
- var PathSeparator, queryIsLastPathSegment;
-
- PathSeparator = require('path').sep;
-
- exports.basenameScore = function(string, query, score) {
- var base, depth, index, lastCharacter, segmentCount, slashCount;
- index = string.length - 1;
- while (string[index] === PathSeparator) {
- index--;
- }
- slashCount = 0;
- lastCharacter = index;
- base = null;
- while (index >= 0) {
- if (string[index] === PathSeparator) {
- slashCount++;
- if (base == null) {
- base = string.substring(index + 1, lastCharacter + 1);
- }
- } else if (index === 0) {
- if (lastCharacter < string.length - 1) {
- if (base == null) {
- base = string.substring(0, lastCharacter + 1);
- }
- } else {
- if (base == null) {
- base = string;
- }
- }
- }
- index--;
- }
- if (base === string) {
- score *= 2;
- } else if (base) {
- score += exports.score(base, query);
- }
- segmentCount = slashCount + 1;
- depth = Math.max(1, 10 - segmentCount);
- score *= depth * 0.01;
- return score;
- };
-
- exports.score = function(string, query) {
- var character, characterScore, indexInQuery, indexInString, lowerCaseIndex, minIndex, queryLength, queryScore, stringLength, totalCharacterScore, upperCaseIndex, _ref;
- if (string === query) {
- return 1;
- }
- if (queryIsLastPathSegment(string, query)) {
- return 1;
- }
- totalCharacterScore = 0;
- queryLength = query.length;
- stringLength = string.length;
- indexInQuery = 0;
- indexInString = 0;
- while (indexInQuery < queryLength) {
- character = query[indexInQuery++];
- lowerCaseIndex = string.indexOf(character.toLowerCase());
- upperCaseIndex = string.indexOf(character.toUpperCase());
- minIndex = Math.min(lowerCaseIndex, upperCaseIndex);
- if (minIndex === -1) {
- minIndex = Math.max(lowerCaseIndex, upperCaseIndex);
- }
- indexInString = minIndex;
- if (indexInString === -1) {
- return 0;
- }
- characterScore = 0.1;
- if (string[indexInString] === character) {
- characterScore += 0.1;
- }
- if (indexInString === 0 || string[indexInString - 1] === PathSeparator) {
- characterScore += 0.8;
- } else if ((_ref = string[indexInString - 1]) === '-' || _ref === '_' || _ref === ' ') {
- characterScore += 0.7;
- }
- string = string.substring(indexInString + 1, stringLength);
- totalCharacterScore += characterScore;
- }
- queryScore = totalCharacterScore / queryLength;
- return ((queryScore * (queryLength / stringLength)) + queryScore) / 2;
- };
-
- queryIsLastPathSegment = function(string, query) {
- if (string[string.length - query.length - 1] === PathSeparator) {
- return string.lastIndexOf(query) === string.length - query.length;
- }
- };
-
- exports.match = function(string, query, stringOffset) {
- var character, indexInQuery, indexInString, lowerCaseIndex, matches, minIndex, queryLength, stringLength, upperCaseIndex, _i, _ref, _results;
- if (stringOffset == null) {
- stringOffset = 0;
- }
- if (string === query) {
- return (function() {
- _results = [];
- for (var _i = stringOffset, _ref = stringOffset + string.length; stringOffset <= _ref ? _i < _ref : _i > _ref; stringOffset <= _ref ? _i++ : _i--){ _results.push(_i); }
- return _results;
- }).apply(this);
- }
- queryLength = query.length;
- stringLength = string.length;
- indexInQuery = 0;
- indexInString = 0;
- matches = [];
- while (indexInQuery < queryLength) {
- character = query[indexInQuery++];
- lowerCaseIndex = string.indexOf(character.toLowerCase());
- upperCaseIndex = string.indexOf(character.toUpperCase());
- minIndex = Math.min(lowerCaseIndex, upperCaseIndex);
- if (minIndex === -1) {
- minIndex = Math.max(lowerCaseIndex, upperCaseIndex);
- }
- indexInString = minIndex;
- if (indexInString === -1) {
- return [];
- }
- matches.push(stringOffset + indexInString);
- stringOffset += indexInString + 1;
- string = string.substring(indexInString + 1, stringLength);
- }
- return matches;
- };
-
-}).call(this);
-
-},{"path":7}],5:[function(require,module,exports){
-(function() {
- var PathSeparator, scorer;
-
- PathSeparator = require('path').sep;
-
- scorer = require('./scorer');
-
- exports.basenameMatch = function(subject, subject_lw, prepQuery) {
- var basePos, depth, end;
- end = subject.length - 1;
- while (subject[end] === PathSeparator) {
- end--;
- }
- basePos = subject.lastIndexOf(PathSeparator, end);
- if (basePos === -1) {
- return [];
- }
- depth = prepQuery.depth;
- while (depth-- > 0) {
- basePos = subject.lastIndexOf(PathSeparator, basePos - 1);
- if (basePos === -1) {
- return [];
- }
- }
- basePos++;
- end++;
- return exports.match(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery, basePos);
- };
-
- exports.mergeMatches = function(a, b) {
- var ai, bj, i, j, m, n, out;
- m = a.length;
- n = b.length;
- if (n === 0) {
- return a.slice();
- }
- if (m === 0) {
- return b.slice();
- }
- i = -1;
- j = 0;
- bj = b[j];
- out = [];
- while (++i < m) {
- ai = a[i];
- while (bj <= ai && ++j < n) {
- if (bj < ai) {
- out.push(bj);
- }
- bj = b[j];
- }
- out.push(ai);
- }
- while (j < n) {
- out.push(b[j++]);
- }
- return out;
- };
-
- exports.match = function(subject, subject_lw, prepQuery, offset) {
- var DIAGONAL, LEFT, STOP, UP, acro_score, align, backtrack, csc_diag, csc_row, csc_score, i, j, m, matches, move, n, pos, query, query_lw, score, score_diag, score_row, score_up, si_lw, start, trace;
- if (offset == null) {
- offset = 0;
- }
- query = prepQuery.query;
- query_lw = prepQuery.query_lw;
- m = subject.length;
- n = query.length;
- acro_score = scorer.scoreAcronyms(subject, subject_lw, query, query_lw).score;
- score_row = new Array(n);
- csc_row = new Array(n);
- STOP = 0;
- UP = 1;
- LEFT = 2;
- DIAGONAL = 3;
- trace = new Array(m * n);
- pos = -1;
- j = -1;
- while (++j < n) {
- score_row[j] = 0;
- csc_row[j] = 0;
- }
- i = -1;
- while (++i < m) {
- score = 0;
- score_up = 0;
- csc_diag = 0;
- si_lw = subject_lw[i];
- j = -1;
- while (++j < n) {
- csc_score = 0;
- align = 0;
- score_diag = score_up;
- if (query_lw[j] === si_lw) {
- start = scorer.isWordStart(i, subject, subject_lw);
- csc_score = csc_diag > 0 ? csc_diag : scorer.scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
- align = score_diag + scorer.scoreCharacter(i, j, start, acro_score, csc_score);
- }
- score_up = score_row[j];
- csc_diag = csc_row[j];
- if (score > score_up) {
- move = LEFT;
- } else {
- score = score_up;
- move = UP;
- }
- if (align > score) {
- score = align;
- move = DIAGONAL;
- } else {
- csc_score = 0;
- }
- score_row[j] = score;
- csc_row[j] = csc_score;
- trace[++pos] = score > 0 ? move : STOP;
- }
- }
- i = m - 1;
- j = n - 1;
- pos = i * n + j;
- backtrack = true;
- matches = [];
- while (backtrack && i >= 0 && j >= 0) {
- switch (trace[pos]) {
- case UP:
- i--;
- pos -= n;
- break;
- case LEFT:
- j--;
- pos--;
- break;
- case DIAGONAL:
- matches.push(i + offset);
- j--;
- i--;
- pos -= n + 1;
- break;
- default:
- backtrack = false;
- }
- }
- matches.reverse();
- return matches;
- };
-
-}).call(this);
-
-},{"./scorer":6,"path":7}],6:[function(require,module,exports){
-(function() {
- var AcronymResult, PathSeparator, Query, basenameScore, coreChars, countDir, doScore, emptyAcronymResult, file_coeff, isMatch, isSeparator, isWordEnd, isWordStart, miss_coeff, opt_char_re, pos_bonus, scoreAcronyms, scoreCharacter, scoreConsecutives, scoreExact, scoreExactMatch, scorePattern, scorePosition, scoreSize, tau_depth, tau_size, truncatedUpperCase, wm;
-
- PathSeparator = require('path').sep;
-
- wm = 150;
-
- pos_bonus = 20;
-
- tau_depth = 13;
-
- tau_size = 85;
-
- file_coeff = 1.2;
-
- miss_coeff = 0.75;
-
- opt_char_re = /[ _\-:\/\\]/g;
-
- exports.coreChars = coreChars = function(query) {
- return query.replace(opt_char_re, '');
- };
-
- exports.score = function(string, query, prepQuery, allowErrors) {
- var score, string_lw;
- if (prepQuery == null) {
- prepQuery = new Query(query);
- }
- if (allowErrors == null) {
- allowErrors = false;
- }
- if (!(allowErrors || isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
- return 0;
- }
- string_lw = string.toLowerCase();
- score = doScore(string, string_lw, prepQuery);
- return Math.ceil(basenameScore(string, string_lw, prepQuery, score));
- };
-
- Query = (function() {
- function Query(query) {
- if (!(query != null ? query.length : void 0)) {
- return null;
- }
- this.query = query;
- this.query_lw = query.toLowerCase();
- this.core = coreChars(query);
- this.core_lw = this.core.toLowerCase();
- this.core_up = truncatedUpperCase(this.core);
- this.depth = countDir(query, query.length);
- }
-
- return Query;
-
- })();
-
- exports.prepQuery = function(query) {
- return new Query(query);
- };
-
- exports.isMatch = isMatch = function(subject, query_lw, query_up) {
- var i, j, m, n, qj_lw, qj_up, si;
- m = subject.length;
- n = query_lw.length;
- if (!m || n > m) {
- return false;
- }
- i = -1;
- j = -1;
- while (++j < n) {
- qj_lw = query_lw[j];
- qj_up = query_up[j];
- while (++i < m) {
- si = subject[i];
- if (si === qj_lw || si === qj_up) {
- break;
- }
- }
- if (i === m) {
- return false;
- }
- }
- return true;
- };
-
- doScore = function(subject, subject_lw, prepQuery) {
- var acro, acro_score, align, csc_diag, csc_row, csc_score, i, j, m, miss_budget, miss_left, mm, n, pos, query, query_lw, record_miss, score, score_diag, score_row, score_up, si_lw, start, sz;
- query = prepQuery.query;
- query_lw = prepQuery.query_lw;
- m = subject.length;
- n = query.length;
- acro = scoreAcronyms(subject, subject_lw, query, query_lw);
- acro_score = acro.score;
- if (acro.count === n) {
- return scoreExact(n, m, acro_score, acro.pos);
- }
- pos = subject_lw.indexOf(query_lw);
- if (pos > -1) {
- return scoreExactMatch(subject, subject_lw, query, query_lw, pos, n, m);
- }
- score_row = new Array(n);
- csc_row = new Array(n);
- sz = scoreSize(n, m);
- miss_budget = Math.ceil(miss_coeff * n) + 5;
- miss_left = miss_budget;
- j = -1;
- while (++j < n) {
- score_row[j] = 0;
- csc_row[j] = 0;
- }
- i = subject_lw.indexOf(query_lw[0]);
- if (i > -1) {
- i--;
- }
- mm = subject_lw.lastIndexOf(query_lw[n - 1], m);
- if (mm > i) {
- m = mm + 1;
- }
- while (++i < m) {
- score = 0;
- score_diag = 0;
- csc_diag = 0;
- si_lw = subject_lw[i];
- record_miss = true;
- j = -1;
- while (++j < n) {
- score_up = score_row[j];
- if (score_up > score) {
- score = score_up;
- }
- csc_score = 0;
- if (query_lw[j] === si_lw) {
- start = isWordStart(i, subject, subject_lw);
- csc_score = csc_diag > 0 ? csc_diag : scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
- align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score);
- if (align > score) {
- score = align;
- miss_left = miss_budget;
- } else {
- if (record_miss && --miss_left <= 0) {
- return score_row[n - 1] * sz;
- }
- record_miss = false;
- }
- }
- score_diag = score_up;
- csc_diag = csc_row[j];
- csc_row[j] = csc_score;
- score_row[j] = score;
- }
- }
- return score * sz;
- };
-
- exports.isWordStart = isWordStart = function(pos, subject, subject_lw) {
- var curr_s, prev_s;
- if (pos === 0) {
- return true;
- }
- curr_s = subject[pos];
- prev_s = subject[pos - 1];
- return isSeparator(curr_s) || isSeparator(prev_s) || (curr_s !== subject_lw[pos] && prev_s === subject_lw[pos - 1]);
- };
-
- exports.isWordEnd = isWordEnd = function(pos, subject, subject_lw, len) {
- var curr_s, next_s;
- if (pos === len - 1) {
- return true;
- }
- curr_s = subject[pos];
- next_s = subject[pos + 1];
- return isSeparator(curr_s) || isSeparator(next_s) || (curr_s === subject_lw[pos] && next_s !== subject_lw[pos + 1]);
- };
-
- isSeparator = function(c) {
- return c === ' ' || c === '.' || c === '-' || c === '_' || c === '/' || c === '\\';
- };
-
- scorePosition = function(pos) {
- var sc;
- if (pos < pos_bonus) {
- sc = pos_bonus - pos;
- return 100 + sc * sc;
- } else {
- return Math.max(100 + pos_bonus - pos, 0);
- }
- };
-
- scoreSize = function(n, m) {
- return tau_size / (tau_size + Math.abs(m - n));
- };
-
- scoreExact = function(n, m, quality, pos) {
- return 2 * n * (wm * quality + scorePosition(pos)) * scoreSize(n, m);
- };
-
- exports.scorePattern = scorePattern = function(count, len, sameCase, start, end) {
- var bonus, sz;
- sz = count;
- bonus = 6;
- if (sameCase === count) {
- bonus += 2;
- }
- if (start) {
- bonus += 3;
- }
- if (end) {
- bonus += 1;
- }
- if (count === len) {
- if (start) {
- if (sameCase === len) {
- sz += 2;
- } else {
- sz += 1;
- }
- }
- if (end) {
- bonus += 1;
- }
- }
- return sameCase + sz * (sz + bonus);
- };
-
- exports.scoreCharacter = scoreCharacter = function(i, j, start, acro_score, csc_score) {
- var posBonus;
- posBonus = scorePosition(i);
- if (start) {
- return posBonus + wm * ((acro_score > csc_score ? acro_score : csc_score) + 10);
- }
- return posBonus + wm * csc_score;
- };
-
- exports.scoreConsecutives = scoreConsecutives = function(subject, subject_lw, query, query_lw, i, j, start) {
- var k, m, mi, n, nj, sameCase, startPos, sz;
- m = subject.length;
- n = query.length;
- mi = m - i;
- nj = n - j;
- k = mi < nj ? mi : nj;
- startPos = i;
- sameCase = 0;
- sz = 0;
- if (query[j] === subject[i]) {
- sameCase++;
- }
- while (++sz < k && query_lw[++j] === subject_lw[++i]) {
- if (query[j] === subject[i]) {
- sameCase++;
- }
- }
- if (sz === 1) {
- return 1 + 2 * sameCase;
- }
- return scorePattern(sz, n, sameCase, start, isWordEnd(i, subject, subject_lw, m));
- };
-
- exports.scoreExactMatch = scoreExactMatch = function(subject, subject_lw, query, query_lw, pos, n, m) {
- var end, i, pos2, sameCase, start;
- start = isWordStart(pos, subject, subject_lw);
- if (!start) {
- pos2 = subject_lw.indexOf(query_lw, pos + 1);
- if (pos2 > -1) {
- start = isWordStart(pos2, subject, subject_lw);
- if (start) {
- pos = pos2;
- }
- }
- }
- i = -1;
- sameCase = 0;
- while (++i < n) {
- if (query[pos + i] === subject[i]) {
- sameCase++;
- }
- }
- end = isWordEnd(pos + n - 1, subject, subject_lw, m);
- return scoreExact(n, m, scorePattern(n, n, sameCase, start, end), pos);
- };
-
- AcronymResult = (function() {
- function AcronymResult(score, pos, count) {
- this.score = score;
- this.pos = pos;
- this.count = count;
- }
-
- return AcronymResult;
-
- })();
-
- emptyAcronymResult = new AcronymResult(0, 0.1, 0);
-
- exports.scoreAcronyms = scoreAcronyms = function(subject, subject_lw, query, query_lw) {
- var count, i, j, m, n, pos, qj_lw, sameCase, score;
- m = subject.length;
- n = query.length;
- if (!(m > 1 && n > 1)) {
- return emptyAcronymResult;
- }
- count = 0;
- pos = 0;
- sameCase = 0;
- i = -1;
- j = -1;
- while (++j < n) {
- qj_lw = query_lw[j];
- while (++i < m) {
- if (qj_lw === subject_lw[i] && isWordStart(i, subject, subject_lw)) {
- if (query[j] === subject[i]) {
- sameCase++;
- }
- pos += i;
- count++;
- break;
- }
- }
- if (i === m) {
- break;
- }
- }
- if (count < 2) {
- return emptyAcronymResult;
- }
- score = scorePattern(count, n, sameCase, true, false);
- return new AcronymResult(score, pos / count, count);
- };
-
- basenameScore = function(subject, subject_lw, prepQuery, fullPathScore) {
- var alpha, basePathScore, basePos, depth, end;
- if (fullPathScore === 0) {
- return 0;
- }
- end = subject.length - 1;
- while (subject[end] === PathSeparator) {
- end--;
- }
- basePos = subject.lastIndexOf(PathSeparator, end);
- if (basePos === -1) {
- return fullPathScore;
- }
- depth = prepQuery.depth;
- while (depth-- > 0) {
- basePos = subject.lastIndexOf(PathSeparator, basePos - 1);
- if (basePos === -1) {
- return fullPathScore;
- }
- }
- basePos++;
- end++;
- basePathScore = doScore(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery);
- alpha = 0.5 * tau_depth / (tau_depth + countDir(subject, end + 1));
- return alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, file_coeff * (end - basePos));
- };
-
- exports.countDir = countDir = function(path, end) {
- var count, i;
- if (end < 1) {
- return 0;
- }
- count = 0;
- i = -1;
- while (++i < end && path[i] === PathSeparator) {
- continue;
- }
- while (++i < end) {
- if (path[i] === PathSeparator) {
- count++;
- while (++i < end && path[i] === PathSeparator) {
- continue;
- }
- }
- }
- return count;
- };
-
- truncatedUpperCase = function(str) {
- var char, upper, _i, _len;
- upper = "";
- for (_i = 0, _len = str.length; _i < _len; _i++) {
- char = str[_i];
- upper += char.toUpperCase()[0];
- }
- return upper;
- };
-
-}).call(this);
-
-},{"path":7}],7:[function(require,module,exports){
-(function (process){
-// Copyright Joyent, Inc. and other Node contributors.
-//
-// Permission is hereby granted, free of charge, to any person obtaining a
-// copy of this software and associated documentation files (the
-// "Software"), to deal in the Software without restriction, including
-// without limitation the rights to use, copy, modify, merge, publish,
-// distribute, sublicense, and/or sell copies of the Software, and to permit
-// persons to whom the Software is furnished to do so, subject to the
-// following conditions:
-//
-// The above copyright notice and this permission notice shall be included
-// in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
-// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
-// USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-// resolves . and .. elements in a path array with directory names there
-// must be no slashes, empty elements, or device names (c:\) in the array
-// (so also no leading and trailing slashes - it does not distinguish
-// relative and absolute paths)
-function normalizeArray(parts, allowAboveRoot) {
- // if the path tries to go above the root, `up` ends up > 0
- var up = 0;
- for (var i = parts.length - 1; i >= 0; i--) {
- var last = parts[i];
- if (last === '.') {
- parts.splice(i, 1);
- } else if (last === '..') {
- parts.splice(i, 1);
- up++;
- } else if (up) {
- parts.splice(i, 1);
- up--;
- }
- }
-
- // if the path is allowed to go above the root, restore leading ..s
- if (allowAboveRoot) {
- for (; up--; up) {
- parts.unshift('..');
- }
- }
-
- return parts;
-}
-
-// Split a filename into [root, dir, basename, ext], unix version
-// 'root' is just a slash, or nothing.
-var splitPathRe =
- /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
-var splitPath = function(filename) {
- return splitPathRe.exec(filename).slice(1);
-};
-
-// path.resolve([from ...], to)
-// posix version
-exports.resolve = function() {
- var resolvedPath = '',
- resolvedAbsolute = false;
-
- for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
- var path = (i >= 0) ? arguments[i] : process.cwd();
-
- // Skip empty and invalid entries
- if (typeof path !== 'string') {
- throw new TypeError('Arguments to path.resolve must be strings');
- } else if (!path) {
- continue;
- }
-
- resolvedPath = path + '/' + resolvedPath;
- resolvedAbsolute = path.charAt(0) === '/';
- }
-
- // At this point the path should be resolved to a full absolute path, but
- // handle relative paths to be safe (might happen when process.cwd() fails)
-
- // Normalize the path
- resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) {
- return !!p;
- }), !resolvedAbsolute).join('/');
-
- return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
-};
-
-// path.normalize(path)
-// posix version
-exports.normalize = function(path) {
- var isAbsolute = exports.isAbsolute(path),
- trailingSlash = substr(path, -1) === '/';
-
- // Normalize the path
- path = normalizeArray(filter(path.split('/'), function(p) {
- return !!p;
- }), !isAbsolute).join('/');
-
- if (!path && !isAbsolute) {
- path = '.';
- }
- if (path && trailingSlash) {
- path += '/';
- }
-
- return (isAbsolute ? '/' : '') + path;
-};
-
-// posix version
-exports.isAbsolute = function(path) {
- return path.charAt(0) === '/';
-};
-
-// posix version
-exports.join = function() {
- var paths = Array.prototype.slice.call(arguments, 0);
- return exports.normalize(filter(paths, function(p, index) {
- if (typeof p !== 'string') {
- throw new TypeError('Arguments to path.join must be strings');
- }
- return p;
- }).join('/'));
-};
-
-
-// path.relative(from, to)
-// posix version
-exports.relative = function(from, to) {
- from = exports.resolve(from).substr(1);
- to = exports.resolve(to).substr(1);
-
- function trim(arr) {
- var start = 0;
- for (; start < arr.length; start++) {
- if (arr[start] !== '') break;
- }
-
- var end = arr.length - 1;
- for (; end >= 0; end--) {
- if (arr[end] !== '') break;
- }
-
- if (start > end) return [];
- return arr.slice(start, end - start + 1);
- }
-
- var fromParts = trim(from.split('/'));
- var toParts = trim(to.split('/'));
-
- var length = Math.min(fromParts.length, toParts.length);
- var samePartsLength = length;
- for (var i = 0; i < length; i++) {
- if (fromParts[i] !== toParts[i]) {
- samePartsLength = i;
- break;
- }
- }
-
- var outputParts = [];
- for (var i = samePartsLength; i < fromParts.length; i++) {
- outputParts.push('..');
- }
-
- outputParts = outputParts.concat(toParts.slice(samePartsLength));
-
- return outputParts.join('/');
-};
-
-exports.sep = '/';
-exports.delimiter = ':';
-
-exports.dirname = function(path) {
- var result = splitPath(path),
- root = result[0],
- dir = result[1];
-
- if (!root && !dir) {
- // No dirname whatsoever
- return '.';
- }
-
- if (dir) {
- // It has a dirname, strip trailing slash
- dir = dir.substr(0, dir.length - 1);
- }
-
- return root + dir;
-};
-
-
-exports.basename = function(path, ext) {
- var f = splitPath(path)[2];
- // TODO: make this comparison case-insensitive on windows?
- if (ext && f.substr(-1 * ext.length) === ext) {
- f = f.substr(0, f.length - ext.length);
- }
- return f;
-};
-
-
-exports.extname = function(path) {
- return splitPath(path)[3];
-};
-
-function filter (xs, f) {
- if (xs.filter) return xs.filter(f);
- var res = [];
- for (var i = 0; i < xs.length; i++) {
- if (f(xs[i], i, xs)) res.push(xs[i]);
- }
- return res;
-}
-
-// String.prototype.substr - negative index don't work in IE8
-var substr = 'ab'.substr(-1) === 'b'
- ? function (str, start, len) { return str.substr(start, len) }
- : function (str, start, len) {
- if (start < 0) start = str.length + start;
- return str.substr(start, len);
- }
-;
-
-}).call(this,require('_process'))
-},{"_process":8}],8:[function(require,module,exports){
-// shim for using process in browser
-
-var process = module.exports = {};
-var queue = [];
-var draining = false;
-var currentQueue;
-var queueIndex = -1;
-
-function cleanUpNextTick() {
- draining = false;
- if (currentQueue.length) {
- queue = currentQueue.concat(queue);
- } else {
- queueIndex = -1;
- }
- if (queue.length) {
- drainQueue();
- }
-}
-
-function drainQueue() {
- if (draining) {
- return;
- }
- var timeout = setTimeout(cleanUpNextTick);
- draining = true;
-
- var len = queue.length;
- while(len) {
- currentQueue = queue;
- queue = [];
- while (++queueIndex < len) {
- if (currentQueue) {
- currentQueue[queueIndex].run();
- }
- }
- queueIndex = -1;
- len = queue.length;
- }
- currentQueue = null;
- draining = false;
- clearTimeout(timeout);
-}
-
-process.nextTick = function (fun) {
- var args = new Array(arguments.length - 1);
- if (arguments.length > 1) {
- for (var i = 1; i < arguments.length; i++) {
- args[i - 1] = arguments[i];
- }
- }
- queue.push(new Item(fun, args));
- if (queue.length === 1 && !draining) {
- setTimeout(drainQueue, 0);
- }
-};
-
-// v8 likes predictible objects
-function Item(fun, array) {
- this.fun = fun;
- this.array = array;
-}
-Item.prototype.run = function () {
- this.fun.apply(null, this.array);
-};
-process.title = 'browser';
-process.browser = true;
-process.env = {};
-process.argv = [];
-process.version = ''; // empty string to avoid regexp issues
-process.versions = {};
-
-function noop() {}
-
-process.on = noop;
-process.addListener = noop;
-process.once = noop;
-process.off = noop;
-process.removeListener = noop;
-process.removeAllListeners = noop;
-process.emit = noop;
-
-process.binding = function (name) {
- throw new Error('process.binding is not supported');
-};
-
-process.cwd = function () { return '/' };
-process.chdir = function (dir) {
- throw new Error('process.chdir is not supported');
-};
-process.umask = function() { return 0; };
-
-},{}]},{},[1]);
diff --git a/vendor/assets/javascripts/latinise.js b/vendor/assets/javascripts/latinise.js
deleted file mode 100644
index da37966b28a..00000000000
--- a/vendor/assets/javascripts/latinise.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// Converting text to basic latin (aka removing accents)
-//
-// Based on: http://semplicewebsites.com/removing-accents-javascript
-//
-var Latinise = {
- map: {"Ã":"A","Ä‚":"A","Ắ":"A","Ặ":"A","Ằ":"A","Ẳ":"A","Ẵ":"A","Ç":"A","Â":"A","Ấ":"A","Ậ":"A","Ầ":"A","Ẩ":"A","Ẫ":"A","Ä":"A","Çž":"A","Ȧ":"A","Ç ":"A","Ạ":"A","È€":"A","À":"A","Ả":"A","È‚":"A","Ä€":"A","Ä„":"A","Ã…":"A","Ǻ":"A","Ḁ":"A","Ⱥ":"A","Ã":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ç¢":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ḃ":"B","Ḅ":"B","Æ":"B","Ḇ":"B","Ƀ":"B","Æ‚":"B","Ć":"C","ÄŒ":"C","Ç":"C","Ḉ":"C","Ĉ":"C","ÄŠ":"C","Ƈ":"C","È»":"C","ÄŽ":"D","á¸":"D","Ḓ":"D","Ḋ":"D","Ḍ":"D","ÆŠ":"D","Ḏ":"D","Dz":"D","Ç…":"D","Ä":"D","Æ‹":"D","DZ":"DZ","Ç„":"DZ","É":"E","Ä”":"E","Äš":"E","Ȩ":"E","Ḝ":"E","Ê":"E","Ế":"E","Ệ":"E","Ề":"E","Ể":"E","Ễ":"E","Ḙ":"E","Ë":"E","Ä–":"E","Ẹ":"E","È„":"E","È":"E","Ẻ":"E","Ȇ":"E","Ä’":"E","Ḗ":"E","Ḕ":"E","Ę":"E","Ɇ":"E","Ẽ":"E","Ḛ":"E","êª":"ET","Ḟ":"F","Æ‘":"F","Ç´":"G","Äž":"G","Ǧ":"G","Ä¢":"G","Äœ":"G","Ä ":"G","Æ“":"G","Ḡ":"G","Ǥ":"G","Ḫ":"H","Èž":"H","Ḩ":"H","Ĥ":"H","Ⱨ":"H","Ḧ":"H","Ḣ":"H","Ḥ":"H","Ħ":"H","Ã":"I","Ĭ":"I","Ç":"I","ÃŽ":"I","Ã":"I","Ḯ":"I","Ä°":"I","Ị":"I","Ȉ":"I","ÃŒ":"I","Ỉ":"I","ÈŠ":"I","Ī":"I","Ä®":"I","Æ—":"I","Ĩ":"I","Ḭ":"I","ê¹":"D","ê»":"F","ê½":"G","êž‚":"R","êž„":"S","Ꞇ":"T","ê¬":"IS","Ä´":"J","Ɉ":"J","Ḱ":"K","Ǩ":"K","Ķ":"K","Ⱪ":"K","ê‚":"K","Ḳ":"K","Ƙ":"K","Ḵ":"K","ê€":"K","ê„":"K","Ĺ":"L","Ƚ":"L","Ľ":"L","Ä»":"L","Ḽ":"L","Ḷ":"L","Ḹ":"L","â± ":"L","êˆ":"L","Ḻ":"L","Ä¿":"L","â±¢":"L","Lj":"L","Å":"L","LJ":"LJ","Ḿ":"M","á¹€":"M","Ṃ":"M","â±®":"M","Ń":"N","Ň":"N","Å…":"N","Ṋ":"N","Ṅ":"N","Ṇ":"N","Ǹ":"N","Æ":"N","Ṉ":"N","È ":"N","Ç‹":"N","Ñ":"N","ÇŠ":"NJ","Ó":"O","ÅŽ":"O","Ç‘":"O","Ô":"O","á»":"O","Ộ":"O","á»’":"O","á»”":"O","á»–":"O","Ö":"O","Ȫ":"O","È®":"O","È°":"O","Ọ":"O","Å":"O","ÈŒ":"O","Ã’":"O","Ỏ":"O","Æ ":"O","Ớ":"O","Ợ":"O","Ờ":"O","Ở":"O","á» ":"O","ÈŽ":"O","êŠ":"O","êŒ":"O","ÅŒ":"O","á¹’":"O","á¹":"O","ÆŸ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Õ":"O","Ṍ":"O","Ṏ":"O","Ȭ":"O","Æ¢":"OI","êŽ":"OO","Æ":"E","Ɔ":"O","È¢":"OU","á¹”":"P","á¹–":"P","ê’":"P","Ƥ":"P","ê”":"P","â±£":"P","ê":"P","ê˜":"Q","ê–":"Q","Å”":"R","Ř":"R","Å–":"R","Ṙ":"R","Ṛ":"R","Ṝ":"R","È":"R","È’":"R","Ṟ":"R","ÉŒ":"R","Ɽ":"R","Ꜿ":"C","ÆŽ":"E","Åš":"S","Ṥ":"S","Å ":"S","Ṧ":"S","Åž":"S","Åœ":"S","Ș":"S","á¹ ":"S","á¹¢":"S","Ṩ":"S","ẞ":"SS","Ť":"T","Å¢":"T","á¹°":"T","Èš":"T","Ⱦ":"T","Ṫ":"T","Ṭ":"T","Ƭ":"T","á¹®":"T","Æ®":"T","Ŧ":"T","Ɐ":"A","Ꞁ":"L","Æœ":"M","É…":"V","Ꜩ":"TZ","Ú":"U","Ŭ":"U","Ç“":"U","Û":"U","Ṷ":"U","Ãœ":"U","Ç—":"U","Ç™":"U","Ç›":"U","Ç•":"U","á¹²":"U","Ụ":"U","Å°":"U","È”":"U","Ù":"U","Ủ":"U","Ư":"U","Ứ":"U","á»°":"U","Ừ":"U","Ử":"U","á»®":"U","È–":"U","Ū":"U","Ṻ":"U","Ų":"U","Å®":"U","Ũ":"U","Ṹ":"U","á¹´":"U","êž":"V","á¹¾":"V","Ʋ":"V","á¹¼":"V","ê ":"VY","Ẃ":"W","Å´":"W","Ẅ":"W","Ẇ":"W","Ẉ":"W","Ẁ":"W","â±²":"W","Ẍ":"X","Ẋ":"X","Ã":"Y","Ŷ":"Y","Ÿ":"Y","Ẏ":"Y","á»´":"Y","Ỳ":"Y","Ƴ":"Y","Ỷ":"Y","Ỿ":"Y","Ȳ":"Y","ÉŽ":"Y","Ỹ":"Y","Ź":"Z","Ž":"Z","áº":"Z","Ⱬ":"Z","Å»":"Z","Ẓ":"Z","Ȥ":"Z","Ẕ":"Z","Ƶ":"Z","IJ":"IJ","Å’":"OE","á´€":"A","á´":"AE","Ê™":"B","á´ƒ":"B","á´„":"C","á´…":"D","á´‡":"E","ꜰ":"F","É¢":"G","Ê›":"G","Êœ":"H","ɪ":"I","Ê":"R","á´Š":"J","á´‹":"K","ÊŸ":"L","á´Œ":"L","á´":"M","É´":"N","á´":"O","ɶ":"OE","á´":"O","á´•":"OU","á´˜":"P","Ê€":"R","á´Ž":"N","á´™":"R","ꜱ":"S","á´›":"T","â±»":"E","á´š":"R","á´œ":"U","á´ ":"V","á´¡":"W","Ê":"Y","á´¢":"Z","á":"a","ă":"a","ắ":"a","ặ":"a","ằ":"a","ẳ":"a","ẵ":"a","ÇŽ":"a","â":"a","ấ":"a","ậ":"a","ầ":"a","ẩ":"a","ẫ":"a","ä":"a","ÇŸ":"a","ȧ":"a","Ç¡":"a","ạ":"a","È":"a","à":"a","ả":"a","ȃ":"a","Ä":"a","Ä…":"a","á¶":"a","ẚ":"a","Ã¥":"a","Ç»":"a","á¸":"a","â±¥":"a","ã":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","Ç£":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ḃ":"b","ḅ":"b","É“":"b","ḇ":"b","ᵬ":"b","ᶀ":"b","Æ€":"b","ƃ":"b","ɵ":"o","ć":"c","Ä":"c","ç":"c","ḉ":"c","ĉ":"c","É•":"c","Ä‹":"c","ƈ":"c","ȼ":"c","Ä":"d","ḑ":"d","ḓ":"d","È¡":"d","ḋ":"d","á¸":"d","É—":"d","ᶑ":"d","á¸":"d","áµ­":"d","á¶":"d","Ä‘":"d","É–":"d","ÆŒ":"d","ı":"i","È·":"j","ÉŸ":"j","Ê„":"j","dz":"dz","dž":"dz","é":"e","Ä•":"e","Ä›":"e","È©":"e","á¸":"e","ê":"e","ế":"e","ệ":"e","á»":"e","ể":"e","á»…":"e","ḙ":"e","ë":"e","Ä—":"e","ẹ":"e","È…":"e","è":"e","ẻ":"e","ȇ":"e","Ä“":"e","ḗ":"e","ḕ":"e","ⱸ":"e","Ä™":"e","ᶒ":"e","ɇ":"e","ẽ":"e","ḛ":"e","ê«":"et","ḟ":"f","Æ’":"f","áµ®":"f","ᶂ":"f","ǵ":"g","ÄŸ":"g","ǧ":"g","Ä£":"g","Ä":"g","Ä¡":"g","É ":"g","ḡ":"g","ᶃ":"g","Ç¥":"g","ḫ":"h","ÈŸ":"h","ḩ":"h","Ä¥":"h","ⱨ":"h","ḧ":"h","ḣ":"h","ḥ":"h","ɦ":"h","ẖ":"h","ħ":"h","Æ•":"hv","í":"i","Ä­":"i","Ç":"i","î":"i","ï":"i","ḯ":"i","ị":"i","ȉ":"i","ì":"i","ỉ":"i","È‹":"i","Ä«":"i","į":"i","ᶖ":"i","ɨ":"i","Ä©":"i","ḭ":"i","êº":"d","ê¼":"f","áµ¹":"g","ꞃ":"r","êž…":"s","ꞇ":"t","ê­":"is","Ç°":"j","ĵ":"j","Ê":"j","ɉ":"j","ḱ":"k","Ç©":"k","Ä·":"k","ⱪ":"k","êƒ":"k","ḳ":"k","Æ™":"k","ḵ":"k","ᶄ":"k","ê":"k","ê…":"k","ĺ":"l","Æš":"l","ɬ":"l","ľ":"l","ļ":"l","ḽ":"l","È´":"l","ḷ":"l","ḹ":"l","ⱡ":"l","ê‰":"l","ḻ":"l","Å€":"l","É«":"l","ᶅ":"l","É­":"l","Å‚":"l","lj":"lj","Å¿":"s","ẜ":"s","ẛ":"s","áº":"s","ḿ":"m","á¹":"m","ṃ":"m","ɱ":"m","ᵯ":"m","ᶆ":"m","Å„":"n","ň":"n","ņ":"n","ṋ":"n","ȵ":"n","á¹…":"n","ṇ":"n","ǹ":"n","ɲ":"n","ṉ":"n","Æž":"n","áµ°":"n","ᶇ":"n","ɳ":"n","ñ":"n","ÇŒ":"nj","ó":"o","Å":"o","Ç’":"o","ô":"o","ố":"o","á»™":"o","ồ":"o","ổ":"o","á»—":"o","ö":"o","È«":"o","ȯ":"o","ȱ":"o","á»":"o","Å‘":"o","È":"o","ò":"o","á»":"o","Æ¡":"o","á»›":"o","ợ":"o","á»":"o","ở":"o","ỡ":"o","È":"o","ê‹":"o","ê":"o","ⱺ":"o","Å":"o","ṓ":"o","ṑ":"o","Ç«":"o","Ç­":"o","ø":"o","Ç¿":"o","õ":"o","á¹":"o","á¹":"o","È­":"o","Æ£":"oi","ê":"oo","É›":"e","ᶓ":"e","É”":"o","ᶗ":"o","È£":"ou","ṕ":"p","á¹—":"p","ê“":"p","Æ¥":"p","áµ±":"p","ᶈ":"p","ê•":"p","áµ½":"p","ê‘":"p","ê™":"q","Ê ":"q","É‹":"q","ê—":"q","Å•":"r","Å™":"r","Å—":"r","á¹™":"r","á¹›":"r","á¹":"r","È‘":"r","ɾ":"r","áµ³":"r","È“":"r","ṟ":"r","ɼ":"r","áµ²":"r","ᶉ":"r","É":"r","ɽ":"r","ↄ":"c","ꜿ":"c","ɘ":"e","É¿":"r","Å›":"s","á¹¥":"s","Å¡":"s","ṧ":"s","ÅŸ":"s","Å":"s","È™":"s","ṡ":"s","á¹£":"s","ṩ":"s","Ê‚":"s","áµ´":"s","ᶊ":"s","È¿":"s","É¡":"g","ß":"ss","á´‘":"o","á´“":"o","á´":"u","Å¥":"t","Å£":"t","á¹±":"t","È›":"t","ȶ":"t","ẗ":"t","ⱦ":"t","ṫ":"t","á¹­":"t","Æ­":"t","ṯ":"t","áµµ":"t","Æ«":"t","ʈ":"t","ŧ":"t","ᵺ":"th","É":"a","á´‚":"ae","Ç":"e","áµ·":"g","É¥":"h","Ê®":"h","ʯ":"h","á´‰":"i","Êž":"k","êž":"l","ɯ":"m","É°":"m","á´”":"oe","ɹ":"r","É»":"r","ɺ":"r","â±¹":"r","ʇ":"t","ÊŒ":"v","Ê":"w","ÊŽ":"y","ꜩ":"tz","ú":"u","Å­":"u","Ç”":"u","û":"u","á¹·":"u","ü":"u","ǘ":"u","Çš":"u","Çœ":"u","Ç–":"u","á¹³":"u","ụ":"u","ű":"u","È•":"u","ù":"u","ủ":"u","Æ°":"u","ứ":"u","á»±":"u","ừ":"u","á»­":"u","ữ":"u","È—":"u","Å«":"u","á¹»":"u","ų":"u","ᶙ":"u","ů":"u","Å©":"u","á¹¹":"u","á¹µ":"u","ᵫ":"ue","ê¸":"um","â±´":"v","êŸ":"v","ṿ":"v","Ê‹":"v","ᶌ":"v","â±±":"v","á¹½":"v","ê¡":"vy","ẃ":"w","ŵ":"w","ẅ":"w","ẇ":"w","ẉ":"w","áº":"w","â±³":"w","ẘ":"w","áº":"x","ẋ":"x","á¶":"x","ý":"y","Å·":"y","ÿ":"y","áº":"y","ỵ":"y","ỳ":"y","Æ´":"y","á»·":"y","ỿ":"y","ȳ":"y","ẙ":"y","É":"y","ỹ":"y","ź":"z","ž":"z","ẑ":"z","Ê‘":"z","ⱬ":"z","ż":"z","ẓ":"z","È¥":"z","ẕ":"z","ᵶ":"z","ᶎ":"z","Ê":"z","ƶ":"z","É€":"z","ff":"ff","ffi":"ffi","ffl":"ffl","ï¬":"fi","fl":"fl","ij":"ij","Å“":"oe","st":"st","â‚":"a","â‚‘":"e","áµ¢":"i","â±¼":"j","â‚’":"o","áµ£":"r","ᵤ":"u","áµ¥":"v","â‚“":"x"}
-};
-
-String.prototype.latinise = function() {
- return this.replace(/[^A-Za-z0-9]/g, function(x) { return Latinise.map[x] || x; });
-};
diff --git a/vendor/assets/javascripts/peek.js b/vendor/assets/javascripts/peek.js
index f7e77de34ff..6a341a3f0fe 100644
--- a/vendor/assets/javascripts/peek.js
+++ b/vendor/assets/javascripts/peek.js
@@ -1,5 +1,14 @@
+/*
+ * This is a modified version of https://github.com/peek/peek/blob/master/app/assets/javascripts/peek.js
+ *
+ * - Removed the dependency on jquery.tipsy
+ * - Removed the initializeTipsy and toggleBar functions
+ * - Customized updatePerformanceBar to handle SQL queries report specificities
+ * - Changed /peek/results to /-/peek/results
+ * - Removed the keypress, pjax:end, page:change, and turbolinks:load handlers
+ */
(function($) {
- var fetchRequestResults, getRequestId, peekEnabled, toggleBar, updatePerformanceBar;
+ var fetchRequestResults, getRequestId, peekEnabled, updatePerformanceBar;
getRequestId = function() {
return $('#peek').data('request-id');
};
@@ -41,22 +50,6 @@
});
return $(document).trigger('peek:render', [getRequestId(), results]);
};
- toggleBar = function(event) {
- var wrapper;
- if ($(event.target).is(':input')) {
- return;
- }
- if (event.which === 96 && !event.metaKey) {
- wrapper = $('#peek');
- if (wrapper.hasClass('disabled')) {
- wrapper.removeClass('disabled');
- return document.cookie = "peek=true; path=/";
- } else {
- wrapper.addClass('disabled');
- return document.cookie = "peek=false; path=/";
- }
- }
- };
fetchRequestResults = function() {
return $.ajax('/-/peek/results', {
data: {
@@ -68,7 +61,6 @@
error: function(xhr, textStatus, error) {}
});
};
- $(document).on('keypress', toggleBar);
$(document).on('peek:update', fetchRequestResults);
return $(function() {
if (peekEnabled()) {
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index 520a86352f7..addf405e4f5 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -32,7 +32,7 @@ proguard/
# Android Studio captures folder
captures/
-# Intellij
+# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
@@ -41,7 +41,8 @@ captures/
.idea/libraries
# Keystore files
-*.jks
+# Uncomment the following line if you do not want to check your keystore files in.
+#*.jks
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
diff --git a/vendor/gitignore/Autotools.gitignore b/vendor/gitignore/Autotools.gitignore
index e3923f96fce..ffa6ecc3f9b 100644
--- a/vendor/gitignore/Autotools.gitignore
+++ b/vendor/gitignore/Autotools.gitignore
@@ -31,3 +31,12 @@ Makefile.in
# http://www.gnu.org/software/texinfo
/texinfo.tex
+
+# http://www.gnu.org/software/m4/
+
+m4/libtool.m4
+m4/ltoptions.m4
+m4/ltsugar.m4
+m4/ltversion.m4
+m4/lt~obsolete.m4
+autom4te.cache
diff --git a/vendor/gitignore/Composer.gitignore b/vendor/gitignore/Composer.gitignore
index c4222678424..a67d42b32f8 100644
--- a/vendor/gitignore/Composer.gitignore
+++ b/vendor/gitignore/Composer.gitignore
@@ -1,6 +1,6 @@
composer.phar
/vendor/
-# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
+# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
# composer.lock
diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore
index ac67aaf3243..b6d65867dac 100644
--- a/vendor/gitignore/Elixir.gitignore
+++ b/vendor/gitignore/Elixir.gitignore
@@ -1,6 +1,8 @@
/_build
/cover
/deps
+/doc
+/.fetch
erl_crash.dump
*.ez
*.beam
diff --git a/vendor/gitignore/ExtJs.gitignore b/vendor/gitignore/ExtJs.gitignore
index c92aea0fe0c..ab97a8cc3e1 100644
--- a/vendor/gitignore/ExtJs.gitignore
+++ b/vendor/gitignore/ExtJs.gitignore
@@ -10,3 +10,5 @@ ext/
modern.json
modern.jsonp
resources/sass/.sass-cache/
+resources/.arch-internal-preview.css
+.arch-internal-preview.css
diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore
index 09dfde64b5f..cca150a88dd 100644
--- a/vendor/gitignore/Global/Matlab.gitignore
+++ b/vendor/gitignore/Global/Matlab.gitignore
@@ -19,4 +19,4 @@ slprj/
octave-workspace
# Simulink autosave extension
-.autosave
+*.autosave
diff --git a/vendor/gitignore/Global/Windows.gitignore b/vendor/gitignore/Global/Windows.gitignore
index dff26a9ab70..846a1db836c 100644
--- a/vendor/gitignore/Global/Windows.gitignore
+++ b/vendor/gitignore/Global/Windows.gitignore
@@ -7,7 +7,7 @@ ehthumbs_vista.db
*.stackdump
# Folder config file
-Desktop.ini
+[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
diff --git a/vendor/gitignore/Global/Xcode.gitignore b/vendor/gitignore/Global/Xcode.gitignore
index 37de8bb4793..cd0c7d3e45a 100644
--- a/vendor/gitignore/Global/Xcode.gitignore
+++ b/vendor/gitignore/Global/Xcode.gitignore
@@ -2,11 +2,17 @@
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
-## Build generated
+## User settings
+xcuserdata/
+
+## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
+*.xcscmblueprint
+*.xccheckout
+
+## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
-
-## Various settings
+*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
@@ -15,9 +21,3 @@ DerivedData/
!default.mode2v3
*.perspectivev3
!default.perspectivev3
-xcuserdata/
-
-## Other
-*.moved-aside
-*.xccheckout
-*.xcscmblueprint
diff --git a/vendor/gitignore/Global/macOS.gitignore b/vendor/gitignore/Global/macOS.gitignore
index 9d1061e8bc4..135767fc075 100644
--- a/vendor/gitignore/Global/macOS.gitignore
+++ b/vendor/gitignore/Global/macOS.gitignore
@@ -1,5 +1,5 @@
# General
-*.DS_Store
+.DS_Store
.AppleDouble
.LSOverride
diff --git a/vendor/gitignore/Joomla.gitignore b/vendor/gitignore/Joomla.gitignore
index 53a74e74657..b6bf3a9c96a 100644
--- a/vendor/gitignore/Joomla.gitignore
+++ b/vendor/gitignore/Joomla.gitignore
@@ -251,7 +251,7 @@
/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini
/administrator/language/en-GB/en-GB.xml
/administrator/language/overrides/*
-/administrator/logs/index.html
+/administrator/logs/*
/administrator/manifests/*
/administrator/modules/mod_custom/*
/administrator/modules/mod_feed/*
diff --git a/vendor/gitignore/OCaml.gitignore b/vendor/gitignore/OCaml.gitignore
index f7817ae5c36..da0b20424a0 100644
--- a/vendor/gitignore/OCaml.gitignore
+++ b/vendor/gitignore/OCaml.gitignore
@@ -18,3 +18,6 @@ _build/
# oasis generated files
setup.data
setup.log
+
+# Merlin configuring file for Vim and Emacs
+.merlin
diff --git a/vendor/gitignore/Perl.gitignore b/vendor/gitignore/Perl.gitignore
index 9bf1537f6ae..ecf66f84291 100644
--- a/vendor/gitignore/Perl.gitignore
+++ b/vendor/gitignore/Perl.gitignore
@@ -24,7 +24,7 @@ Build.bat
# Module::Install
inc/
-# ExtUitls::MakeMaker
+# ExtUtils::MakeMaker
/blib/
/_eumm/
/*.gz
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index 113294a5f18..af2f537516d 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -23,6 +23,7 @@ wheels/
*.egg-info/
.installed.cfg
*.egg
+MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
@@ -51,6 +52,8 @@ coverage.xml
# Django stuff:
*.log
+.static_storage/
+.media/
local_settings.py
# Flask stuff:
@@ -84,6 +87,8 @@ celerybeat-schedule
env/
venv/
ENV/
+env.bak/
+venv.bak/
# Spyder project settings
.spyderproject
diff --git a/vendor/gitignore/Qt.gitignore b/vendor/gitignore/Qt.gitignore
index fe67fdf1ee6..037a1e75790 100644
--- a/vendor/gitignore/Qt.gitignore
+++ b/vendor/gitignore/Qt.gitignore
@@ -31,11 +31,9 @@ ui_*.h
Makefile*
*build-*
-
# Qt unit tests
target_wrapper.*
-
# QtCreator
*.autosave
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index a0322dbd35a..b6418e51766 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -13,6 +13,7 @@
## Intermediate documents:
*.dvi
+*.xdv
*-converted-to.*
# these rules might exclude image files for figures etc.
# *.ps
diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore
index f20453be963..1fef4ab91e1 100644
--- a/vendor/gitignore/Terraform.gitignore
+++ b/vendor/gitignore/Terraform.gitignore
@@ -1,7 +1,9 @@
-# Compiled files
+# Local .terraform directories
+**/.terraform/*
+
+# .tfstate files
*.tfstate
-*.tfstate.*.backup
-*.tfstate.backup
+*.tfstate.*
-# Module directory
-.terraform/
+# .tfvars files
+*.tfvars
diff --git a/vendor/gitignore/Umbraco.gitignore b/vendor/gitignore/Umbraco.gitignore
index ea05e1fb2a9..b6b0743f62a 100644
--- a/vendor/gitignore/Umbraco.gitignore
+++ b/vendor/gitignore/Umbraco.gitignore
@@ -1,3 +1,7 @@
+## Ignore Umbraco files/folders generated for each instance
+##
+## Get latest from https://github.com/github/gitignore/blob/master/Umbraco.gitignore
+
# Note: VisualStudio gitignore rules may also be relevant
# Umbraco
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index f652b45c2ee..509668db67a 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -96,6 +96,9 @@ ipch/
*.vspx
*.sap
+# Visual Studio Trace Files
+*.e2e
+
# TFS 2012 Local Workspace
$tf/
@@ -168,11 +171,11 @@ PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
-**/packages/*
+**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
-!**/packages/build/
+!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
-#!**/packages/repositories.config
+#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
@@ -297,3 +300,6 @@ __pycache__/
*.btm.cs
*.odx.cs
*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
diff --git a/vendor/gitignore/ZendFramework.gitignore b/vendor/gitignore/ZendFramework.gitignore
index 80adb154900..f0b7d8585b7 100644
--- a/vendor/gitignore/ZendFramework.gitignore
+++ b/vendor/gitignore/ZendFramework.gitignore
@@ -19,7 +19,6 @@ temp/
data/DoctrineORMModule/Proxy/
data/DoctrineORMModule/cache/
-
# Legacy ZF1
demos/
extras/documentation
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index c93e6567baf..88261502d7f 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -335,7 +335,9 @@ production:
function check_kube_domain() {
if [ -z ${AUTO_DEVOPS_DOMAIN+x} ]; then
- echo "In order to deploy, AUTO_DEVOPS_DOMAIN must be set as a variable at the group or project level, or manually added in .gitlab-cy.yml"
+ echo "In order to deploy or use Review Apps, AUTO_DEVOPS_DOMAIN variable must be set"
+ echo "You can do it in Auto DevOps project settings or defining a secret variable at group or project level"
+ echo "You can also manually add it in .gitlab-ci.yml"
false
else
true
diff --git a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
index 02cfab3a5b2..36386a19fdc 100644
--- a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
@@ -16,7 +16,7 @@ image: "crystallang/crystal:latest"
# Cache shards in between builds
cache:
paths:
- - libs
+ - lib
# This is a basic example for a shard or script which doesn't use
# services such as redis or postgres
diff --git a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
index 8a214352d2a..86e4985d8d2 100644
--- a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
@@ -29,7 +29,7 @@ format:
compile:
stage: build
script:
- - go build -race -ldflags "-extldflags '-static'" -o mybinary
+ - go build -race -ldflags "-extldflags '-static'" -o $CI_PROJECT_DIR/mybinary
artifacts:
paths:
- mybinary
diff --git a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
index 91b096654d1..ba2efbd03a0 100644
--- a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
@@ -7,8 +7,8 @@
# This template will build and test your projects as well as create the documentation.
#
# * Caches downloaded dependencies and plugins between invocation.
-# * Does only verify merge requests but deploy built artifacts of the
-# master branch.
+# * Verify but don't deploy merge requests.
+# * Deploy built artifacts from master branch only.
# * Shows how to use multiple jobs in test stage for verifying functionality
# with multiple JDKs.
# * Uses site:stage to collect the documentation for multi-module projects.
@@ -20,7 +20,7 @@ variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
# As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used
# when running from the command line.
- # `installAtEnd` and `deployAtEnd`are only effective with recent version of the corresponding plugins.
+ # `installAtEnd` and `deployAtEnd` are only effective with recent version of the corresponding plugins.
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
# Cache downloaded dependencies and plugins between builds.
@@ -100,4 +100,3 @@ pages:
- public
only:
- master
-
diff --git a/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
index 636cb0a9a99..290b9997084 100644
--- a/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
@@ -31,14 +31,14 @@ test2:
- oc project "$CI_PROJECT_NAME-$CI_PROJECT_ID" 2> /dev/null || oc new-project "$CI_PROJECT_NAME-$CI_PROJECT_ID"
script:
- "oc get services $APP 2> /dev/null || oc new-app . --name=$APP --strategy=docker"
- - "oc start-build $APP --from-dir=. --follow || sleep 3s || oc start-build $APP --from-dir=. --follow"
+ - "oc start-build $APP --from-dir=. --follow || sleep 3s && oc start-build $APP --from-dir=. --follow"
- "oc get routes $APP 2> /dev/null || oc expose service $APP --hostname=$APP_HOST"
review:
<<: *deploy
stage: review
variables:
- APP: $CI_COMMIT_REF_NAME
+ APP: review-$CI_COMMIT_REF_NAME
APP_HOST: $CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN
environment:
name: review/$CI_COMMIT_REF_NAME
@@ -56,7 +56,7 @@ stop-review:
- oc delete all -l "app=$APP"
when: manual
variables:
- APP: $CI_COMMIT_REF_NAME
+ APP: review-$CI_COMMIT_REF_NAME
GIT_STRATEGY: none
environment:
name: review/$CI_COMMIT_REF_NAME
diff --git a/vendor/gitlab-ci-yml/Python.gitlab-ci.yml b/vendor/gitlab-ci-yml/Python.gitlab-ci.yml
new file mode 100644
index 00000000000..a2882a5407d
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Python.gitlab-ci.yml
@@ -0,0 +1,32 @@
+# This file is a template, and might need editing before it works on your project.
+image: python:latest
+
+before_script:
+ - python -V # Print out python version for debugging
+
+test:
+ script:
+ - python setup.py test
+ - pip install tox flake8 # you can also use tox
+ - tox -e py36,flake8
+
+run:
+ script:
+ - python setup.py bdist_wheel
+ # an alternative approach is to install and run:
+ - pip install dist/*
+ # run the command here
+ artifacts:
+ paths:
+ - dist/*.whl
+
+pages:
+ script:
+ - pip install sphinx sphinx-rtd-theme
+ - cd doc ; make html
+ - mv build/html/ ../public/
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml
index 7810121c350..6573eceaa59 100644
--- a/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml
@@ -1,6 +1,6 @@
-# Unofficial language image. Look for the different tagged releases at:
-# https://hub.docker.com/r/scorpil/rust/tags/
-image: "scorpil/rust:stable"
+# Official language image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/rust/tags/
+image: "rust:latest"
# Optional: Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
diff --git a/vendor/gitlab-ci-yml/dotNET.gitlab-ci.yml b/vendor/gitlab-ci-yml/dotNET.gitlab-ci.yml
new file mode 100644
index 00000000000..fc3d4ecdbba
--- /dev/null
+++ b/vendor/gitlab-ci-yml/dotNET.gitlab-ci.yml
@@ -0,0 +1,86 @@
+# The following script will work for any project that can be built from command line by msbuild
+# It uses powershell shell executor, so you need to add the following line to your config.toml file
+# (located in gitlab-runner.exe directory):
+# shell = "powershell"
+#
+# The script is composed of 3 stages: build, test and deploy.
+#
+# The build stage restores NuGet packages and uses msbuild to build the exe and msi
+# One major issue you'll find is that you can't build msi projects from command line
+# if you use vdproj. There are workarounds building msi via devenv, but they rarely work
+# The best solution is migrating your vdproj projects to WiX, as it can be build directly
+# by msbuild.
+#
+# The test stage runs nunit from command line against Test project inside your solution
+# It also saves the resulting TestResult.xml file
+#
+# The deploy stage copies the exe and msi from build stage to a network drive
+# You need to have the network drive mapped as Local System user for gitlab-runner service to see it
+# The best way to persist the mapping is via a scheduled task (see: https://stackoverflow.com/a/7867064/1288473),
+# running the following batch command: net use P: \\x.x.x.x\Projects /u:your_user your_pass /persistent:yes
+
+
+# place project specific paths in variables to make the rest of the script more generic
+variables:
+ EXE_RELEASE_FOLDER: 'YourApp\bin\Release'
+ MSI_RELEASE_FOLDER: 'Setup\bin\Release'
+ TEST_FOLDER: 'Tests\bin\Release'
+ DEPLOY_FOLDER: 'P:\Projects\YourApp\Builds'
+
+ NUGET_PATH: 'C:\NuGet\nuget.exe'
+ MSBUILD_PATH: 'C:\Program Files (x86)\MSBuild\14.0\Bin\msbuild.exe'
+ NUNIT_PATH: 'C:\Program Files (x86)\NUnit.org\nunit-console\nunit3-console.exe'
+
+stages:
+ - build
+ - test
+ - deploy
+
+build_job:
+ stage: build
+ only:
+ - tags # the build process will only be started by git tag commits
+ script:
+ - '& "$env:NUGET_PATH" restore' # restore Nuget dependencies
+ - '& "$env:MSBUILD_PATH" /p:Configuration=Release' # build the project
+ artifacts:
+ expire_in: 1 week # save gitlab server space, we copy the files we need to deploy folder later on
+ paths:
+ - '$env:EXE_RELEASE_FOLDER\YourApp.exe' # saving exe to copy to deploy folder
+ - '$env:MSI_RELEASE_FOLDER\YourApp Setup.msi' # saving msi to copy to deploy folder
+ - '$env:TEST_FOLDER\' # saving entire Test project so NUnit can run tests
+
+test_job:
+ stage: test
+ only:
+ - tags
+ script:
+ - '& "$env:NUNIT_PATH" ".\$env:TEST_FOLDER\Tests.dll"' # running NUnit tests
+ artifacts:
+ expire_in: 1 week # save gitlab server space, we copy the files we need to deploy folder later on
+ paths:
+ - '.\TestResult.xml' # saving NUnit results to copy to deploy folder
+ dependencies:
+ - build_job
+
+deploy_job:
+ stage: deploy
+ only:
+ - tags
+ script:
+ # Compose a folder for each release based on commit tag.
+ # Assuming your tag is Rev1.0.0.1, and your last commit message is 'First commit'
+ # the artifact files will be copied to:
+ # P:\Projects\YourApp\Builds\Rev1.0.0.1 - First commit\
+ - '$commitSubject = git log -1 --pretty=%s'
+ - '$deployFolder = $($env:DEPLOY_FOLDER) + "\" + $($env:CI_BUILD_TAG) + " - " + $commitSubject + "\"'
+
+ # xcopy takes care of recursively creating required folders
+ - 'xcopy /y ".\$env:EXE_RELEASE_FOLDER\YourApp.exe" "$deployFolder"'
+ - 'xcopy /y ".\$env:MSI_RELEASE_FOLDER\YourApp Setup.msi" "$deployFolder"'
+ - 'xcopy /y ".\TestResult.xml" "$deployFolder"'
+
+ dependencies:
+ - build_job
+ - test_job
+ \ No newline at end of file
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index 24623ff4c1f..6f6ca5f8b32 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -1,7 +1,11 @@
+"","","MIT,ISC,Apache 2.0,New BSD,Simplified BSD"
RedCloth,4.3.2,MIT
abbrev,1.0.9,ISC
+abbrev,1.1.0,ISC
accepts,1.3.3,MIT
ace-rails-ap,4.1.2,MIT
+acorn,3.3.0,MIT
+acorn,4.0.13,MIT
acorn,5.1.1,MIT
acorn-dynamic-import,2.0.2,MIT
acorn-jsx,3.0.1,MIT
@@ -13,9 +17,11 @@ activemodel,4.2.8,MIT
activerecord,4.2.8,MIT
activesupport,4.2.8,MIT
acts-as-taggable-on,4.0.0,MIT
-addressable,2.3.8,Apache 2.0
+addressable,2.5.2,Apache 2.0
after,0.8.2,MIT
-ajv,5.2.0,MIT
+ajv,4.11.8,MIT
+ajv,5.2.2,MIT
+ajv-keywords,1.5.1,MIT
ajv-keywords,2.1.0,MIT
akismet,2.0.0,MIT
align-text,0.1.4,MIT
@@ -24,8 +30,10 @@ alphanum-sort,1.0.2,MIT
amdefine,1.0.1,BSD-3-Clause OR MIT
ansi-escapes,1.4.0,MIT
ansi-html,0.0.5,"Apache, Version 2.0"
+ansi-html,0.0.7,Apache 2.0
ansi-regex,2.1.1,MIT
ansi-styles,2.2.1,MIT
+ansi-styles,3.2.0,MIT
anymatch,1.3.2,ISC
append-transform,0.4.0,MIT
aproba,1.1.1,ISC
@@ -37,6 +45,7 @@ arr-flatten,1.0.1,MIT
array-find,1.0.0,MIT
array-find-index,1.0.2,MIT
array-flatten,1.1.1,MIT
+array-flatten,2.1.1,MIT
array-slice,0.2.3,MIT
array-union,1.0.2,MIT
array-uniq,1.0.3,MIT
@@ -50,15 +59,18 @@ asn1,0.2.3,MIT
asn1.js,4.9.1,MIT
assert,1.4.1,MIT
assert-plus,0.2.0,MIT
+assert-plus,1.0.0,MIT
+async,0.9.2,MIT
+async,1.5.2,MIT
async,2.4.1,MIT
async-each,1.0.1,MIT
asynckit,0.4.0,MIT
atomic,1.1.99,Apache 2.0
attr_encrypted,3.0.3,MIT
attr_required,1.0.0,MIT
-autoparse,0.3.3,Apache 2.0
autoprefixer,6.7.7,MIT
autoprefixer-rails,6.2.3,MIT
+autosize,4.0.0,MIT
aws-sign2,0.6.0,Apache 2.0
aws4,1.6.0,MIT
axiom-types,0.1.1,MIT
@@ -138,6 +150,7 @@ babel-types,6.23.0,MIT
babosa,1.0.2,MIT
babylon,6.16.1,MIT
backo2,1.0.2,MIT
+balanced-match,0.4.2,MIT
balanced-match,1.0.0,MIT
base32,0.3.2,MIT
base64-arraybuffer,0.1.5,MIT
@@ -150,17 +163,20 @@ bcrypt_pbkdf,1.0.0,MIT
better-assert,1.0.2,MIT
big.js,3.1.3,MIT
binary-extensions,1.10.0,MIT
-bindata,2.3.5,ruby
+bindata,2.4.1,ruby
blob,0.0.4,unknown
block-stream,0.0.9,ISC
bluebird,2.11.0,MIT
+bluebird,3.5.0,MIT
bn.js,4.11.6,MIT
body-parser,1.17.2,MIT
bonjour,3.5.0,MIT
boom,2.10.1,New BSD
bootstrap-sass,3.3.6,MIT
bootstrap_form,2.7.0,MIT
+brace-expansion,1.1.7,MIT
brace-expansion,1.1.8,MIT
+braces,0.1.5,MIT
braces,1.8.5,MIT
brorand,1.0.7,MIT
browser,2.2.0,MIT
@@ -179,18 +195,23 @@ builder,3.2.3,MIT
builtin-modules,1.1.1,MIT
builtin-status-codes,3.0.0,MIT
bytes,2.4.0,MIT
+bytes,2.5.0,MIT
caller-path,0.1.0,MIT
callsite,1.0.0,unknown
callsites,0.2.0,MIT
+camelcase,1.2.1,MIT
+camelcase,2.1.1,MIT
+camelcase,3.0.0,MIT
camelcase,4.1.0,MIT
camelcase-keys,2.1.0,MIT
caniuse-api,1.6.1,MIT
caniuse-db,1.0.30000649,CC-BY-4.0
-carrierwave,1.1.0,MIT
+carrierwave,1.2.1,MIT
caseless,0.12.0,Apache 2.0
cause,0.1,MIT
center-align,0.1.3,MIT
chalk,1.1.3,MIT
+chalk,2.3.0,MIT
charlock_holmes,0.7.5,MIT
chokidar,1.7.0,MIT
chronic,0.10.2,MIT
@@ -203,6 +224,7 @@ clap,1.1.3,MIT
cli-cursor,1.0.2,MIT
cli-width,2.1.0,ISC
clipboard,1.6.1,MIT
+cliui,2.1.0,ISC
cliui,3.2.0,ISC
clone,1.0.2,MIT
co,4.6.0,MIT
@@ -220,6 +242,7 @@ combined-stream,1.0.5,MIT
commander,2.9.0,MIT
commondir,1.0.1,MIT
component-bind,1.0.0,unknown
+component-emitter,1.1.2,unknown
component-emitter,1.2.1,MIT
component-inherit,0.0.3,unknown
compressible,2.0.11,MIT
@@ -244,6 +267,7 @@ convert-source-map,1.3.0,MIT
cookie,0.3.1,MIT
cookie-signature,1.0.6,MIT
copy-webpack-plugin,4.0.1,MIT
+core-js,2.3.0,MIT
core-js,2.4.1,MIT
core-util-is,1.0.2,MIT
cosmiconfig,2.1.1,MIT
@@ -258,6 +282,7 @@ cryptiles,2.0.5,New BSD
crypto-browserify,3.11.0,MIT
css-color-names,0.0.4,MIT
css-loader,0.28.0,MIT
+css-selector-tokenizer,0.6.0,MIT
css-selector-tokenizer,0.7.0,MIT
css_parser,1.5.0,MIT
cssesc,0.1.0,MIT
@@ -265,16 +290,22 @@ cssnano,3.10.0,MIT
csso,2.3.2,MIT
currently-unhandled,0.4.1,MIT
custom-event,1.0.1,MIT
+d,0.1.1,MIT
d,1.0.0,MIT
d3,3.5.11,New BSD
d3_rails,3.5.11,MIT
dashdash,1.14.1,MIT
date-now,0.1.4,MIT
de-indent,1.0.2,MIT
+debug,2.2.0,MIT
+debug,2.3.3,MIT
+debug,2.6.7,MIT
debug,2.6.8,MIT
debugger-ruby_core_source,1.3.8,MIT
decamelize,1.2.0,MIT
deckar01-task_list,2.0.0,MIT
+declarative,0.0.10,MIT
+declarative-option,0.1.0,MIT
decompress-response,3.3.0,MIT
deep-equal,1.0.1,MIT
deep-extend,0.4.2,MIT
@@ -283,9 +314,11 @@ default-require-extensions,1.0.0,MIT
default_value_for,3.0.2,MIT
defined,1.0.0,MIT
del,2.2.2,MIT
+del,3.0.0,MIT
delayed-stream,1.0.0,MIT
delegate,3.1.2,MIT
delegates,1.0.0,MIT
+depd,1.1.0,MIT
depd,1.1.1,MIT
des.js,1.0.0,MIT
descendants_tracker,0.0.4,MIT
@@ -301,17 +334,19 @@ diffy,3.1.0,MIT
dns-equal,1.0.0,MIT
dns-packet,1.2.2,MIT
dns-txt,2.0.2,MIT
+doctrine,1.5.0,BSD
doctrine,2.0.0,Apache 2.0
document-register-element,1.3.0,MIT
dom-serialize,2.2.1,MIT
dom-serializer,0.1.0,MIT
domain-browser,1.1.7,MIT
domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0"
+domelementtype,1.1.3,unknown
domelementtype,1.3.0,unknown
domhandler,2.3.0,unknown
domutils,1.5.1,unknown
-doorkeeper,4.2.0,MIT
-doorkeeper-openid_connect,1.1.2,MIT
+doorkeeper,4.2.6,MIT
+doorkeeper-openid_connect,1.2.0,MIT
dropzone,4.2.0,MIT
dropzonejs-rails,0.7.2,MIT
duplexer,0.1.1,MIT
@@ -332,6 +367,7 @@ end-of-stream,1.4.0,MIT
engine.io,1.8.3,MIT
engine.io-client,1.8.3,MIT
engine.io-parser,1.3.2,MIT
+enhanced-resolve,0.9.1,MIT
enhanced-resolve,3.4.1,MIT
ent,2.2.0,MIT
entities,1.1.1,BSD-like
@@ -362,10 +398,13 @@ eslint-plugin-import,2.2.0,MIT
eslint-plugin-jasmine,2.2.0,MIT
eslint-plugin-promise,3.5.0,ISC
espree,3.5.0,Simplified BSD
+esprima,2.7.3,Simplified BSD
esprima,4.0.0,Simplified BSD
esquery,1.0.0,BSD
esrecurse,4.1.0,Simplified BSD
+estraverse,1.9.3,BSD
estraverse,4.1.1,Simplified BSD
+estraverse,4.2.0,Simplified BSD
esutils,2.0.2,BSD
et-orbi,1.0.3,MIT
etag,1.8.0,MIT
@@ -382,21 +421,23 @@ execjs,2.6.0,MIT
exit-hook,1.1.1,MIT
expand-braces,0.1.2,MIT
expand-brackets,0.1.5,MIT
+expand-range,0.1.1,MIT
expand-range,1.8.2,MIT
exports-loader,0.6.4,MIT
express,4.15.4,MIT
expression_parser,0.9.0,MIT
extend,3.0.1,MIT
extglob,0.3.2,MIT
-extlib,0.9.16,MIT
extsprintf,1.0.2,MIT
-faraday,0.12.1,MIT
+faraday,0.12.2,MIT
faraday_middleware,0.11.0.1,MIT
faraday_middleware-multi_json,0.0.6,MIT
-fast-deep-equal,0.1.0,MIT
+fast-deep-equal,1.0.0,MIT
fast-levenshtein,2.0.6,MIT
fast_gettext,1.4.0,"MIT,ruby"
fastparse,1.1.1,MIT
+faye-websocket,0.10.0,MIT
+faye-websocket,0.11.1,MIT
faye-websocket,0.7.3,MIT
ffi,1.9.18,New BSD
figures,1.7.0,MIT
@@ -405,17 +446,19 @@ file-loader,0.11.1,MIT
filename-regex,2.0.0,MIT
fileset,2.0.3,MIT
filesize,3.3.0,New BSD
+filesize,3.5.10,New BSD
fill-range,2.2.3,MIT
finalhandler,1.0.4,MIT
find-cache-dir,1.0.0,MIT
find-root,0.1.2,MIT
+find-up,1.1.2,MIT
find-up,2.1.0,MIT
flat-cache,1.2.2,MIT
flatten,1.0.2,MIT
flipper,0.10.2,MIT
flipper-active_record,0.10.2,MIT
flowdock,0.7.1,MIT
-fog-aliyun,0.1.0,MIT
+fog-aliyun,0.2.0,MIT
fog-aws,1.4.0,MIT
fog-core,1.44.3,MIT
fog-google,0.5.3,MIT
@@ -441,6 +484,7 @@ fsevents,1.1.2,MIT
fstream,1.0.11,ISC
fstream-ignore,1.0.5,ISC
function-bind,1.1.0,MIT
+fuzzaldrin-plus,0.5.0,MIT
gauge,2.7.4,ISC
gemnasium-gitlab-service,0.2.6,MIT
gemojione,3.3.0,MIT
@@ -453,27 +497,34 @@ get_process_mem,0.2.0,MIT
getpass,0.1.7,MIT
gettext_i18n_rails,1.8.0,MIT
gettext_i18n_rails_js,1.2.0,MIT
-gitaly-proto,0.33.0,MIT
+gitaly-proto,0.51.0,MIT
github-linguist,4.7.6,MIT
github-markup,1.6.1,MIT
gitlab-flowdock-git-hook,1.0.1,MIT
-gitlab-grit,2.8.1,MIT
-gitlab-markup,1.5.1,MIT
+gitlab-grit,2.8.2,MIT
+gitlab-markup,1.6.3,MIT
+gitlab-svgs,1.0.4,unknown
gitlab_omniauth-ldap,2.0.4,MIT
+glob,5.0.15,ISC
glob,6.0.4,ISC
+glob,7.1.1,ISC
+glob,7.1.2,ISC
glob-base,0.3.0,MIT
glob-parent,2.0.0,ISC
globalid,0.3.7,MIT
globals,9.18.0,MIT
globby,5.0.0,MIT
+globby,6.1.0,MIT
gollum-grit_adapter,1.0.1,MIT
gollum-lib,4.2.7,MIT
gollum-rugged_adapter,0.4.4,MIT
gon,6.1.0,MIT
good-listener,1.2.2,MIT
-google-api-client,0.8.7,Apache 2.0
-google-protobuf,3.4.0.2,New BSD
-googleauth,0.5.1,Apache 2.0
+google-api-client,0.13.6,Apache 2.0
+google-protobuf,3.4.1.1,New BSD
+googleapis-common-protos-types,1.0.0,Apache 2.0
+googleauth,0.5.3,Apache 2.0
+got,3.3.1,MIT
got,7.1.0,MIT
gpgme,2.0.13,LGPL-2.1+
graceful-fs,4.1.11,ISC
@@ -481,8 +532,8 @@ graceful-readlink,1.0.1,MIT
grape,1.0.0,MIT
grape-entity,0.6.0,MIT
grape-route-helpers,2.1.0,MIT
-grape_logging,1.6.0,MIT
-grpc,1.4.5,New BSD
+grape_logging,1.7.0,MIT
+grpc,1.6.6,Apache 2.0
gzip-size,3.0.0,MIT
hamlit,2.6.1,MIT
handle-thing,1.2.5,MIT
@@ -493,6 +544,7 @@ has,1.0.1,MIT
has-ansi,2.0.0,MIT
has-binary,0.1.7,MIT
has-cors,1.1.0,MIT
+has-flag,1.0.0,MIT
has-flag,2.0.0,MIT
has-symbol-support-x,1.3.0,MIT
has-to-string-tag-x,1.3.0,MIT
@@ -518,6 +570,7 @@ htmlparser2,3.9.2,MIT
http,0.9.8,MIT
http-cookie,1.0.3,MIT
http-deceiver,1.2.7,MIT
+http-errors,1.6.1,MIT
http-errors,1.6.2,MIT
http-form_data,1.0.1,MIT
http-proxy,1.16.2,MIT
@@ -543,6 +596,7 @@ indexof,0.0.1,unknown
infinity-agent,2.0.3,MIT
inflight,1.0.6,ISC
influxdb,0.2.3,MIT
+inherits,2.0.1,ISC
inherits,2.0.3,ISC
ini,1.3.4,ISC
inquirer,0.12.0,MIT
@@ -562,12 +616,16 @@ is-builtin-module,1.0.0,MIT
is-dotfile,1.0.2,MIT
is-equal-shallow,0.1.3,MIT
is-extendable,0.1.1,MIT
+is-extglob,1.0.0,MIT
is-extglob,2.1.1,MIT
is-finite,1.0.2,MIT
+is-fullwidth-code-point,1.0.0,MIT
is-fullwidth-code-point,2.0.0,MIT
+is-glob,2.0.1,MIT
is-glob,3.1.0,MIT
is-my-json-valid,2.16.0,MIT
is-npm,1.0.0,MIT
+is-number,0.1.1,MIT
is-number,2.1.0,MIT
is-object,1.0.1,MIT
is-path-cwd,1.0.0,MIT
@@ -587,6 +645,7 @@ is-typedarray,1.0.0,MIT
is-unc-path,0.1.2,MIT
is-utf8,0.2.1,MIT
is-windows,0.2.0,MIT
+isarray,0.0.1,MIT
isarray,1.0.0,MIT
isbinaryfile,3.0.2,MIT
isexe,1.1.2,ISC
@@ -615,10 +674,12 @@ js-beautify,1.6.12,MIT
js-cookie,2.1.3,MIT
js-tokens,3.0.1,MIT
js-yaml,3.7.0,MIT
+js-yaml,3.9.1,MIT
jsbn,0.1.1,MIT
+jsesc,0.5.0,MIT
jsesc,1.3.0,MIT
json,1.8.6,ruby
-json-jwt,1.7.1,MIT
+json-jwt,1.7.2,MIT
json-loader,0.5.7,MIT
json-schema,0.2.3,"AFLv2.1,BSD"
json-schema-traverse,0.3.1,MIT
@@ -649,18 +710,20 @@ kind-of,3.1.0,MIT
klaw,1.3.1,MIT
kubeclient,2.2.0,MIT
latest-version,1.0.1,MIT
-launchy,2.4.3,ISC
lazy-cache,1.0.4,MIT
lcid,1.0.0,MIT
levn,0.3.0,MIT
licensee,8.7.0,MIT
lie,3.1.1,MIT
little-plugger,1.1.4,MIT
+load-json-file,1.1.0,MIT
load-json-file,2.0.0,MIT
loader-runner,2.3.0,MIT
+loader-utils,0.2.16,MIT
loader-utils,1.1.0,MIT
locale,2.1.2,"ruby,LGPLv3+"
locate-path,2.0.0,MIT
+lodash,3.10.1,MIT
lodash,4.17.4,MIT
lodash._baseassign,3.2.0,MIT
lodash._basecopy,3.0.1,MIT
@@ -671,11 +734,13 @@ lodash._getnative,3.9.1,MIT
lodash._isiterateecall,3.0.9,MIT
lodash._topath,3.8.1,MIT
lodash.assign,3.2.0,MIT
+lodash.camelcase,4.1.1,MIT
lodash.camelcase,4.3.0,MIT
lodash.capitalize,4.2.1,MIT
lodash.cond,4.5.2,MIT
lodash.deburr,4.1.0,MIT
lodash.defaults,3.1.2,MIT
+lodash.get,3.7.0,MIT
lodash.get,4.4.2,MIT
lodash.isarguments,3.1.0,MIT
lodash.isarray,3.0.4,MIT
@@ -695,7 +760,9 @@ loofah,2.0.3,MIT
loose-envify,1.3.1,MIT
loud-rejection,1.6.0,MIT
lowercase-keys,1.0.0,MIT
+lru-cache,2.2.4,MIT
lru-cache,3.2.0,ISC
+lru-cache,4.0.2,ISC
macaddress,0.2.8,MIT
mail,2.6.6,MIT
mail_room,0.9.1,MIT
@@ -706,7 +773,8 @@ marked,0.3.6,MIT
math-expression-evaluator,1.2.16,MIT
media-typer,0.3.0,MIT
mem,1.1.0,MIT
-memoist,0.15.0,MIT
+memoist,0.16.0,MIT
+memory-fs,0.2.0,MIT
memory-fs,0.4.1,MIT
meow,3.7.0,MIT
merge-descriptors,1.0.1,MIT
@@ -716,22 +784,29 @@ micromatch,2.3.11,MIT
miller-rabin,4.0.0,MIT
mime,1.3.4,MIT
mime-db,1.27.0,MIT
-mime-types,2.99.3,"MIT,Artistic-2.0,GPL-2.0"
+mime-db,1.29.0,MIT
+mime-types,2.1.15,MIT
+mime-types,3.1,MIT
+mime-types-data,3.2016.0521,MIT
mimemagic,0.3.0,MIT
mimic-fn,1.1.0,MIT
mimic-response,1.0.0,MIT
-mini_portile2,2.2.0,MIT
+mini_portile2,2.3.0,MIT
minimalistic-assert,1.0.0,ISC
minimatch,3.0.3,ISC
+minimatch,3.0.4,ISC
minimist,0.0.8,MIT
+minimist,1.2.0,MIT
mkdirp,0.5.1,MIT
mmap2,2.2.7,ruby
moment,2.17.1,MIT
-monaco-editor,0.8.3,MIT
+monaco-editor,0.10.0,MIT
mousetrap,1.4.6,Apache 2.0
mousetrap-rails,1.4.6,"MIT,Apache"
+ms,0.7.1,MIT
+ms,0.7.2,MIT
ms,2.0.0,MIT
-multi_json,1.12.1,MIT
+multi_json,1.12.2,MIT
multi_xml,0.6.0,MIT
multicast-dns,6.1.1,MIT
multicast-dns-service-types,1.1.0,MIT
@@ -750,11 +825,15 @@ net-ssh,4.1.0,MIT
netrc,0.11.0,MIT
node-dir,0.1.17,MIT
node-forge,0.6.33,BSD
+node-libs-browser,1.1.1,MIT
node-libs-browser,2.0.0,MIT
node-pre-gyp,0.6.36,New BSD
+node-pre-gyp,0.6.37,New BSD
nodemon,1.11.0,MIT
-nokogiri,1.8.0,MIT
+nokogiri,1.8.1,MIT
+nopt,1.0.10,MIT
nopt,3.0.6,ISC
+nopt,4.0.1,ISC
normalize-package-data,2.4.0,Simplified BSD
normalize-path,2.1.1,MIT
normalize-range,0.1.2,MIT
@@ -768,6 +847,8 @@ numerizer,0.1.1,MIT
oauth,0.5.1,MIT
oauth-sign,0.8.2,Apache 2.0
oauth2,1.4.0,MIT
+object-assign,3.0.0,MIT
+object-assign,4.1.0,MIT
object-assign,4.1.1,MIT
object-component,0.0.3,unknown
object.omit,2.0.1,MIT
@@ -777,7 +858,7 @@ oj,2.17.5,MIT
omniauth,1.4.2,MIT
omniauth-auth0,1.4.1,MIT
omniauth-authentiq,0.3.1,MIT
-omniauth-azure-oauth2,0.0.6,MIT
+omniauth-azure-oauth2,0.0.9,MIT
omniauth-cas3,1.1.4,MIT
omniauth-facebook,4.0.0,MIT
omniauth-github,1.1.2,MIT
@@ -786,7 +867,7 @@ omniauth-google-oauth2,0.5.2,MIT
omniauth-kerberos,0.3.0,MIT
omniauth-multipassword,0.4.2,MIT
omniauth-oauth,1.1.0,MIT
-omniauth-oauth2,1.3.1,MIT
+omniauth-oauth2,1.4.0,MIT
omniauth-oauth2-generic,0.2.2,MIT
omniauth-saml,1.7.0,MIT
omniauth-shibboleth,1.2.1,MIT
@@ -807,6 +888,7 @@ orm_adapter,0.5.0,MIT
os,0.9.6,MIT
os-browserify,0.2.1,MIT
os-homedir,1.0.2,MIT
+os-locale,1.4.0,MIT
os-locale,2.1.0,MIT
os-tmpdir,1.0.2,MIT
osenv,0.1.4,ISC
@@ -817,6 +899,7 @@ p-locate,2.0.0,MIT
p-map,1.1.1,MIT
p-timeout,1.2.0,MIT
package-json,1.2.0,MIT
+pako,0.2.9,MIT
pako,1.0.5,(MIT AND Zlib)
paranoia,2.3.1,MIT
parse-asn1,5.0.0,ISC
@@ -827,12 +910,14 @@ parseqs,0.0.5,MIT
parseuri,0.0.5,MIT
parseurl,1.3.1,MIT
path-browserify,0.0.0,MIT
+path-exists,2.1.0,MIT
path-exists,3.0.0,MIT
path-is-absolute,1.0.1,MIT
path-is-inside,1.0.2,(WTFPL OR MIT)
path-key,2.0.1,MIT
path-parse,1.0.5,MIT
path-to-regexp,0.1.7,MIT
+path-type,1.1.0,MIT
path-type,2.0.0,MIT
pause-stream,0.0.11,"MIT,Apache2"
pbkdf2,3.0.9,MIT
@@ -848,9 +933,11 @@ peek-sidekiq,1.0.3,MIT
performance-now,0.2.0,MIT
pg,0.18.4,"BSD,ruby,GPL"
pify,2.3.0,MIT
-pikaday,1.5.1,"BSD,MIT"
+pify,3.0.0,MIT
+pikaday,1.6.1,MIT
pinkie,2.0.4,MIT
pinkie-promise,2.0.1,MIT
+pkg-dir,1.0.0,MIT
pkg-dir,2.0.0,MIT
pkg-up,1.0.0,MIT
pluralize,1.2.1,MIT
@@ -903,22 +990,26 @@ private,0.1.7,MIT
process,0.11.9,MIT
process-nextick-args,1.0.7,MIT
progress,1.1.8,MIT
-prometheus-client-mmap,0.7.0.beta14,Apache 2.0
+prometheus-client-mmap,0.7.0.beta18,Apache 2.0
proto-list,1.2.4,ISC
proxy-addr,1.1.5,MIT
prr,0.0.0,MIT
ps-tree,1.1.0,MIT
pseudomap,1.0.2,ISC
public-encrypt,4.0.0,MIT
+public_suffix,3.0.0,MIT
+punycode,1.3.2,MIT
punycode,1.4.1,MIT
pyu-ruby-sasl,0.0.3.3,MIT
q,1.5.0,MIT
qjobs,1.1.5,MIT
+qs,6.4.0,New BSD
qs,6.5.0,New BSD
query-string,4.3.2,MIT
querystring,0.2.0,MIT
querystring-es3,0.2.1,MIT
querystringify,0.0.4,MIT
+querystringify,1.0.0,MIT
rack,1.6.8,MIT
rack-accept,0.4.5,MIT
rack-attack,4.4.1,MIT
@@ -935,7 +1026,7 @@ rails-i18n,4.0.9,MIT
railties,4.2.8,MIT
rainbow,2.2.2,MIT
raindrops,0.18.0,LGPL-2.1+
-rake,12.0.0,MIT
+rake,12.1.0,MIT
randomatic,1.1.6,MIT
randombytes,2.0.3,MIT
range-parser,1.2.0,MIT
@@ -950,9 +1041,14 @@ rdoc,4.2.2,ruby
re2,1.1.1,New BSD
react-dev-utils,0.5.2,New BSD
read-all-stream,3.1.0,MIT
+read-pkg,1.1.0,MIT
read-pkg,2.0.0,MIT
+read-pkg-up,1.0.1,MIT
read-pkg-up,2.0.0,MIT
+readable-stream,1.0.34,MIT
readable-stream,2.0.6,MIT
+readable-stream,2.2.9,MIT
+readable-stream,2.3.3,MIT
readdirp,2.1.0,MIT
readline2,1.0.1,MIT
recaptcha,3.0.0,MIT
@@ -974,14 +1070,18 @@ regenerate,1.3.2,MIT
regenerator-runtime,0.10.1,MIT
regenerator-transform,0.9.8,BSD
regex-cache,0.4.3,MIT
+regexpu-core,1.0.0,MIT
regexpu-core,2.0.0,MIT
registry-url,3.1.0,MIT
regjsgen,0.2.0,MIT
regjsparser,0.1.5,BSD
remove-trailing-separator,1.1.0,ISC
repeat-element,1.1.2,MIT
+repeat-string,0.2.2,MIT
repeat-string,1.6.1,MIT
+repeating,1.1.3,MIT
repeating,2.0.1,MIT
+representable,3.0.4,MIT
request,2.81.0,Apache 2.0
request_store,1.3.1,MIT
require-directory,2.1.1,MIT
@@ -989,12 +1089,13 @@ require-from-string,1.2.1,MIT
require-main-filename,1.0.1,ISC
require-uncached,1.0.3,MIT
requires-port,1.0.0,MIT
+resolve,1.1.7,MIT
resolve,1.2.0,MIT
resolve-from,1.0.1,MIT
responders,2.3.0,MIT
rest-client,2.0.0,MIT
restore-cursor,1.0.1,MIT
-retriable,1.4.1,MIT
+retriable,3.1.1,MIT
right-align,0.1.3,MIT
rimraf,2.6.1,ISC
rinku,2.0.0,ISC
@@ -1013,6 +1114,7 @@ rufus-scheduler,3.4.0,MIT
rugged,0.26.0,MIT
run-async,0.1.0,MIT
rx-lite,3.1.2,Apache 2.0
+safe-buffer,5.0.1,MIT
safe-buffer,5.1.1,MIT
safe_yaml,1.0.4,MIT
sanitize,2.1.0,MIT
@@ -1027,6 +1129,7 @@ select-hose,2.0.0,MIT
select2,3.5.2-browserify,unknown
select2-rails,3.5.9.3,MIT
selfsigned,1.10.1,MIT
+semver,4.3.6,ISC
semver,5.3.0,ISC
semver-diff,2.1.0,MIT
send,0.15.4,MIT
@@ -1060,8 +1163,13 @@ socket.io-client,1.7.3,MIT
socket.io-parser,2.3.1,MIT
sockjs,0.3.18,MIT
sockjs-client,1.0.1,MIT
+sockjs-client,1.1.4,MIT
sort-keys,1.1.2,MIT
+source-list-map,0.1.8,MIT
source-list-map,2.0.0,MIT
+source-map,0.1.43,BSD
+source-map,0.2.0,BSD
+source-map,0.4.4,New BSD
source-map,0.5.6,New BSD
source-map-support,0.4.11,MIT
spdx-correct,1.0.2,Apache 2.0
@@ -1085,19 +1193,27 @@ stream-http,2.6.3,MIT
stream-shift,1.0.0,MIT
strict-uri-encode,1.1.0,MIT
string-length,1.0.1,MIT
+string-width,1.0.2,MIT
string-width,2.0.0,MIT
+string_decoder,0.10.31,MIT
+string_decoder,1.0.1,MIT
string_decoder,1.0.3,MIT
stringex,2.7.1,MIT
stringstream,0.0.5,MIT
strip-ansi,3.0.1,MIT
+strip-bom,2.0.0,MIT
strip-bom,3.0.0,MIT
strip-eof,1.0.0,MIT
strip-indent,1.0.1,MIT
strip-json-comments,2.0.1,MIT
+supports-color,2.0.0,MIT
+supports-color,3.2.3,MIT
supports-color,4.2.1,MIT
+svg4everybody,2.1.9,CC0-1.0
svgo,0.7.2,MIT
sys-filesystem,1.1.6,Artistic 2.0
table,3.8.3,New BSD
+tapable,0.1.10,MIT
tapable,0.2.8,MIT
tar,2.2.1,ISC
tar-pack,3.4.0,Simplified BSD
@@ -1114,7 +1230,9 @@ through,2.3.8,MIT
thunky,0.1.0,unknown
tilt,2.0.6,MIT
timeago.js,2.0.5,MIT
+timed-out,2.0.0,MIT
timed-out,4.0.1,MIT
+timers-browserify,1.4.2,MIT
timers-browserify,2.0.4,MIT
timfel-krb5-auth,0.8.3,LGPL
tiny-emitter,1.1.0,MIT
@@ -1130,25 +1248,29 @@ trim-newlines,1.0.0,MIT
trim-right,1.0.1,MIT
truncato,0.7.10,MIT
tryit,1.0.3,MIT
+ts-loader,3.1.1,MIT
tty-browserify,0.0.0,MIT
tunnel-agent,0.6.0,Apache 2.0
tweetnacl,0.14.5,Unlicense
type-check,0.3.2,MIT
type-is,1.6.15,MIT
typedarray,0.0.6,MIT
+typescript,2.6.1,Apache 2.0
tzinfo,1.2.3,MIT
u2f,0.2.1,MIT
+uber,0.1.0,MIT
uglifier,2.7.2,MIT
uglify-js,2.8.29,Simplified BSD
uglify-to-browserify,1.0.2,MIT
uglifyjs-webpack-plugin,0.4.6,MIT
uid-number,0.0.6,ISC
+ultron,1.0.2,MIT
ultron,1.1.0,MIT
unc-path-regex,0.1.2,MIT
undefsafe,0.0.3,MIT / http://rem.mit-license.org
underscore,1.8.3,MIT
unf,0.1.4,BSD
-unf_ext,0.0.7.2,MIT
+unf_ext,0.0.7.4,MIT
unicorn,5.1.0,ruby
unicorn-worker-killer,0.4.4,ruby
uniq,1.0.1,MIT
@@ -1159,6 +1281,8 @@ update-notifier,0.5.0,Simplified BSD
url,0.11.0,MIT
url-loader,0.5.8,MIT
url-parse,1.0.5,MIT
+url-parse,1.1.7,MIT
+url-parse,1.1.9,MIT
url-parse-lax,1.0.0,MIT
url-to-options,1.0.1,MIT
url_safe_base64,0.2.2,MIT
@@ -1167,6 +1291,7 @@ useragent,2.2.1,MIT
util,0.10.3,MIT
util-deprecate,1.0.2,MIT
utils-merge,1.0.0,MIT
+uuid,2.0.3,MIT
uuid,3.0.1,MIT
validate-npm-package-license,3.0.1,Apache 2.0
validates_hostname,1.0.6,MIT
@@ -1179,14 +1304,14 @@ visibilityjs,1.2.4,MIT
vm-browserify,0.0.4,MIT
vmstat,2.3.0,MIT
void-elements,2.0.1,MIT
-vue,2.2.6,MIT
+vue,2.5.2,MIT
vue-hot-reload-api,2.0.11,MIT
vue-loader,11.3.4,MIT
vue-resource,1.3.4,MIT
vue-style-loader,2.0.5,MIT
-vue-template-compiler,2.2.6,MIT
+vue-template-compiler,2.5.2,MIT
vue-template-es2015-compiler,1.5.1,MIT
-vuex,2.3.1,MIT
+vuex,3.0.0,MIT
warden,1.2.6,MIT
watchpack,1.4.0,MIT
wbuf,1.7.2,MIT
@@ -1201,15 +1326,19 @@ websocket-driver,0.6.5,MIT
websocket-extensions,0.1.1,MIT
whet.extend,0.9.9,MIT
which,1.2.12,ISC
+which-module,1.0.0,ISC
which-module,2.0.0,ISC
wide-align,1.1.2,ISC
wikicloth,0.8.1,MIT
window-size,0.1.0,MIT
wordwrap,0.0.2,MIT/X11
+wordwrap,0.0.3,MIT
+wordwrap,1.0.0,MIT
wrap-ansi,2.1.0,MIT
wrappy,1.0.2,ISC
write,0.2.1,MIT
write-file-atomic,1.3.4,ISC
+ws,1.1.2,MIT
ws,2.3.1,MIT
wtf-8,1.0.0,MIT
xdg-basedir,2.0.0,MIT
@@ -1218,6 +1347,9 @@ xmlhttprequest-ssl,1.5.3,MIT
xtend,4.0.1,MIT
y18n,3.2.1,ISC
yallist,2.1.2,ISC
+yargs,3.10.0,MIT
+yargs,6.6.0,MIT
yargs,8.0.2,MIT
+yargs-parser,4.2.1,ISC
yargs-parser,7.0.0,ISC
yeast,0.1.2,MIT
diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz
index 69e35e6aa40..7a811e1986b 100644
--- a/vendor/project_templates/express.tar.gz
+++ b/vendor/project_templates/express.tar.gz
Binary files differ
diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz
index 561b1e5902c..7db63ecc65f 100644
--- a/vendor/project_templates/rails.tar.gz
+++ b/vendor/project_templates/rails.tar.gz
Binary files differ
diff --git a/vendor/project_templates/spring.tar.gz b/vendor/project_templates/spring.tar.gz
index 0ba6ec7c60c..96f51ee804c 100644
--- a/vendor/project_templates/spring.tar.gz
+++ b/vendor/project_templates/spring.tar.gz
Binary files differ
diff --git a/yarn.lock b/yarn.lock
index ae86887630b..88897476a15 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,66 @@
# yarn lockfile v1
+"@babel/code-frame@7.0.0-beta.32", "@babel/code-frame@^7.0.0-beta.31":
+ version "7.0.0-beta.32"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.32.tgz#04f231b8ec70370df830d9926ce0f5add074ec4c"
+ dependencies:
+ chalk "^2.0.0"
+ esutils "^2.0.2"
+ js-tokens "^3.0.0"
+
+"@babel/helper-function-name@7.0.0-beta.32":
+ version "7.0.0-beta.32"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.32.tgz#6161af4419f1b4e3ed2d28c0c79c160e218be1f3"
+ dependencies:
+ "@babel/helper-get-function-arity" "7.0.0-beta.32"
+ "@babel/template" "7.0.0-beta.32"
+ "@babel/types" "7.0.0-beta.32"
+
+"@babel/helper-get-function-arity@7.0.0-beta.32":
+ version "7.0.0-beta.32"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.32.tgz#93721a99db3757de575a83bab7c453299abca568"
+ dependencies:
+ "@babel/types" "7.0.0-beta.32"
+
+"@babel/template@7.0.0-beta.32":
+ version "7.0.0-beta.32"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.32.tgz#e1d9fdbd2a7bcf128f2f920744a67dab18072495"
+ dependencies:
+ "@babel/code-frame" "7.0.0-beta.32"
+ "@babel/types" "7.0.0-beta.32"
+ babylon "7.0.0-beta.32"
+ lodash "^4.2.0"
+
+"@babel/traverse@^7.0.0-beta.31":
+ version "7.0.0-beta.32"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.32.tgz#b78b754c6e1af3360626183738e4c7a05951bc99"
+ dependencies:
+ "@babel/code-frame" "7.0.0-beta.32"
+ "@babel/helper-function-name" "7.0.0-beta.32"
+ "@babel/types" "7.0.0-beta.32"
+ babylon "7.0.0-beta.32"
+ debug "^3.0.1"
+ globals "^10.0.0"
+ invariant "^2.2.0"
+ lodash "^4.2.0"
+
+"@babel/types@7.0.0-beta.32", "@babel/types@^7.0.0-beta.31":
+ version "7.0.0-beta.32"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.32.tgz#c317d0ecc89297b80bbcb2f50608e31f6452a5ff"
+ dependencies:
+ esutils "^2.0.2"
+ lodash "^4.2.0"
+ to-fast-properties "^2.0.0"
+
+"@gitlab-org/gitlab-svgs@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.1.1.tgz#6e07ea02c3b104fa8b5d860a5e2fa9dab4edab96"
+
+"@types/jquery@^2.0.40":
+ version "2.0.48"
+ resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.48.tgz#3e90d8cde2d29015e5583017f7830cb3975b2eef"
+
abbrev@1, abbrev@1.0.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
@@ -56,6 +116,15 @@ ajv@^4.7.0, ajv@^4.9.1:
co "^4.6.0"
json-stable-stringify "^1.0.1"
+ajv@^5.0.0:
+ version "5.4.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.4.0.tgz#32d1cf08dbc80c432f426f12e10b2511f6b46474"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.3.0"
+
ajv@^5.1.5:
version "5.2.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
@@ -101,6 +170,12 @@ ansi-styles@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+ansi-styles@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
+ dependencies:
+ color-convert "^1.9.0"
+
anymatch@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
@@ -217,18 +292,12 @@ async@1.x, async@^1.4.0, async@^1.4.2, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
-async@2.4.1:
+async@2.4.1, async@^2.1.2, async@^2.1.4:
version "2.4.1"
resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
dependencies:
lodash "^4.14.0"
-async@^2.1.2, async@^2.1.4:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
- dependencies:
- lodash "^4.14.0"
-
async@~0.9.0:
version "0.9.2"
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
@@ -248,6 +317,10 @@ autoprefixer@^6.3.1:
postcss "^5.2.16"
postcss-value-parser "^3.2.3"
+autosize@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.0.tgz#7a0599b1ba84d73bd7589b0d9da3870152c69237"
+
aws-sign2@~0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
@@ -256,22 +329,20 @@ aws4@^1.2.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
-axios@^0.16.2:
- version "0.16.2"
- resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d"
+axios-mock-adapter@^1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.10.0.tgz#3ccee65466439a2c7567e932798fc0377d39209d"
dependencies:
- follow-redirects "^1.2.3"
- is-buffer "^1.1.5"
+ deep-equal "^1.0.1"
-babel-code-frame@^6.11.0, babel-code-frame@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
+axios@^0.17.1:
+ version "0.17.1"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d"
dependencies:
- chalk "^1.1.0"
- esutils "^2.0.2"
- js-tokens "^3.0.0"
+ follow-redirects "^1.2.5"
+ is-buffer "^1.1.5"
-babel-code-frame@^6.16.0:
+babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
dependencies:
@@ -279,173 +350,173 @@ babel-code-frame@^6.16.0:
esutils "^2.0.2"
js-tokens "^3.0.2"
-babel-core@^6.22.1, babel-core@^6.23.0:
- version "6.23.1"
- resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.23.1.tgz#c143cb621bb2f621710c220c5d579d15b8a442df"
+babel-core@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
dependencies:
- babel-code-frame "^6.22.0"
- babel-generator "^6.23.0"
- babel-helpers "^6.23.0"
+ babel-code-frame "^6.26.0"
+ babel-generator "^6.26.0"
+ babel-helpers "^6.24.1"
babel-messages "^6.23.0"
- babel-register "^6.23.0"
- babel-runtime "^6.22.0"
- babel-template "^6.23.0"
- babel-traverse "^6.23.1"
- babel-types "^6.23.0"
- babylon "^6.11.0"
- convert-source-map "^1.1.0"
- debug "^2.1.1"
- json5 "^0.5.0"
- lodash "^4.2.0"
- minimatch "^3.0.2"
- path-is-absolute "^1.0.0"
- private "^0.1.6"
+ babel-register "^6.26.0"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ convert-source-map "^1.5.0"
+ debug "^2.6.8"
+ json5 "^0.5.1"
+ lodash "^4.17.4"
+ minimatch "^3.0.4"
+ path-is-absolute "^1.0.1"
+ private "^0.1.7"
slash "^1.0.0"
- source-map "^0.5.0"
+ source-map "^0.5.6"
-babel-eslint@^7.2.1:
- version "7.2.1"
- resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.1.tgz#079422eb73ba811e3ca0865ce87af29327f8c52f"
+babel-eslint@^8.0.2:
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.0.2.tgz#e44fb9a037d749486071d52d65312f5c20aa7530"
dependencies:
- babel-code-frame "^6.22.0"
- babel-traverse "^6.23.1"
- babel-types "^6.23.0"
- babylon "^6.16.1"
+ "@babel/code-frame" "^7.0.0-beta.31"
+ "@babel/traverse" "^7.0.0-beta.31"
+ "@babel/types" "^7.0.0-beta.31"
+ babylon "^7.0.0-beta.31"
-babel-generator@^6.18.0, babel-generator@^6.23.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.23.0.tgz#6b8edab956ef3116f79d8c84c5a3c05f32a74bc5"
+babel-generator@^6.18.0, babel-generator@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5"
dependencies:
babel-messages "^6.23.0"
- babel-runtime "^6.22.0"
- babel-types "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
detect-indent "^4.0.0"
jsesc "^1.3.0"
- lodash "^4.2.0"
- source-map "^0.5.0"
+ lodash "^4.17.4"
+ source-map "^0.5.6"
trim-right "^1.0.1"
-babel-helper-bindify-decorators@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.22.0.tgz#d7f5bc261275941ac62acfc4e20dacfb8a3fe952"
+babel-helper-bindify-decorators@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz#14c19e5f142d7b47f19a52431e52b1ccbc40a330"
dependencies:
babel-runtime "^6.22.0"
- babel-traverse "^6.22.0"
- babel-types "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
-babel-helper-builder-binary-assignment-operator-visitor@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.22.0.tgz#29df56be144d81bdeac08262bfa41d2c5e91cdcd"
+babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664"
dependencies:
- babel-helper-explode-assignable-expression "^6.22.0"
+ babel-helper-explode-assignable-expression "^6.24.1"
babel-runtime "^6.22.0"
- babel-types "^6.22.0"
+ babel-types "^6.24.1"
-babel-helper-call-delegate@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.22.0.tgz#119921b56120f17e9dae3f74b4f5cc7bcc1b37ef"
+babel-helper-call-delegate@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
dependencies:
- babel-helper-hoist-variables "^6.22.0"
+ babel-helper-hoist-variables "^6.24.1"
babel-runtime "^6.22.0"
- babel-traverse "^6.22.0"
- babel-types "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
-babel-helper-define-map@^6.23.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.23.0.tgz#1444f960c9691d69a2ced6a205315f8fd00804e7"
+babel-helper-define-map@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f"
dependencies:
- babel-helper-function-name "^6.23.0"
- babel-runtime "^6.22.0"
- babel-types "^6.23.0"
- lodash "^4.2.0"
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
-babel-helper-explode-assignable-expression@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.22.0.tgz#c97bf76eed3e0bae4048121f2b9dae1a4e7d0478"
+babel-helper-explode-assignable-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa"
dependencies:
babel-runtime "^6.22.0"
- babel-traverse "^6.22.0"
- babel-types "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
-babel-helper-explode-class@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.22.0.tgz#646304924aa6388a516843ba7f1855ef8dfeb69b"
+babel-helper-explode-class@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz#7dc2a3910dee007056e1e31d640ced3d54eaa9eb"
dependencies:
- babel-helper-bindify-decorators "^6.22.0"
+ babel-helper-bindify-decorators "^6.24.1"
babel-runtime "^6.22.0"
- babel-traverse "^6.22.0"
- babel-types "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
-babel-helper-function-name@^6.22.0, babel-helper-function-name@^6.23.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.23.0.tgz#25742d67175c8903dbe4b6cb9d9e1fcb8dcf23a6"
+babel-helper-function-name@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
dependencies:
- babel-helper-get-function-arity "^6.22.0"
+ babel-helper-get-function-arity "^6.24.1"
babel-runtime "^6.22.0"
- babel-template "^6.23.0"
- babel-traverse "^6.23.0"
- babel-types "^6.23.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
-babel-helper-get-function-arity@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.22.0.tgz#0beb464ad69dc7347410ac6ade9f03a50634f5ce"
+babel-helper-get-function-arity@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d"
dependencies:
babel-runtime "^6.22.0"
- babel-types "^6.22.0"
+ babel-types "^6.24.1"
-babel-helper-hoist-variables@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.22.0.tgz#3eacbf731d80705845dd2e9718f600cfb9b4ba72"
+babel-helper-hoist-variables@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76"
dependencies:
babel-runtime "^6.22.0"
- babel-types "^6.22.0"
+ babel-types "^6.24.1"
-babel-helper-optimise-call-expression@^6.23.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.23.0.tgz#f3ee7eed355b4282138b33d02b78369e470622f5"
+babel-helper-optimise-call-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
dependencies:
babel-runtime "^6.22.0"
- babel-types "^6.23.0"
+ babel-types "^6.24.1"
-babel-helper-regex@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.22.0.tgz#79f532be1647b1f0ee3474b5f5c3da58001d247d"
+babel-helper-regex@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72"
dependencies:
- babel-runtime "^6.22.0"
- babel-types "^6.22.0"
- lodash "^4.2.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
-babel-helper-remap-async-to-generator@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.22.0.tgz#2186ae73278ed03b8b15ced089609da981053383"
+babel-helper-remap-async-to-generator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b"
dependencies:
- babel-helper-function-name "^6.22.0"
+ babel-helper-function-name "^6.24.1"
babel-runtime "^6.22.0"
- babel-template "^6.22.0"
- babel-traverse "^6.22.0"
- babel-types "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
-babel-helper-replace-supers@^6.22.0, babel-helper-replace-supers@^6.23.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.23.0.tgz#eeaf8ad9b58ec4337ca94223bacdca1f8d9b4bfd"
+babel-helper-replace-supers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a"
dependencies:
- babel-helper-optimise-call-expression "^6.23.0"
+ babel-helper-optimise-call-expression "^6.24.1"
babel-messages "^6.23.0"
babel-runtime "^6.22.0"
- babel-template "^6.23.0"
- babel-traverse "^6.23.0"
- babel-types "^6.23.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
-babel-helpers@^6.23.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.23.0.tgz#4f8f2e092d0b6a8808a4bde79c27f1e2ecf0d992"
+babel-helpers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
dependencies:
babel-runtime "^6.22.0"
- babel-template "^6.23.0"
+ babel-template "^6.24.1"
-babel-loader@^7.1.1:
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.1.tgz#b87134c8b12e3e4c2a94e0546085bc680a2b8488"
+babel-loader@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126"
dependencies:
find-cache-dir "^1.0.0"
loader-utils "^1.0.2"
@@ -463,13 +534,13 @@ babel-plugin-check-es2015-constants@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-istanbul@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.0.0.tgz#36bde8fbef4837e5ff0366531a2beabd7b1ffa10"
+babel-plugin-istanbul@^4.1.5:
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz#6760cdd977f411d3e175bb064f2bc327d99b2b6e"
dependencies:
find-up "^2.1.0"
- istanbul-lib-instrument "^1.4.2"
- test-exclude "^4.0.0"
+ istanbul-lib-instrument "^1.7.5"
+ test-exclude "^4.1.1"
babel-plugin-syntax-async-functions@^6.8.0:
version "6.13.0"
@@ -503,46 +574,46 @@ babel-plugin-syntax-trailing-function-commas@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
-babel-plugin-transform-async-generator-functions@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.22.0.tgz#a720a98153a7596f204099cd5409f4b3c05bab46"
+babel-plugin-transform-async-generator-functions@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz#f058900145fd3e9907a6ddf28da59f215258a5db"
dependencies:
- babel-helper-remap-async-to-generator "^6.22.0"
+ babel-helper-remap-async-to-generator "^6.24.1"
babel-plugin-syntax-async-generators "^6.5.0"
babel-runtime "^6.22.0"
-babel-plugin-transform-async-to-generator@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.22.0.tgz#194b6938ec195ad36efc4c33a971acf00d8cd35e"
+babel-plugin-transform-async-to-generator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761"
dependencies:
- babel-helper-remap-async-to-generator "^6.22.0"
+ babel-helper-remap-async-to-generator "^6.24.1"
babel-plugin-syntax-async-functions "^6.8.0"
babel-runtime "^6.22.0"
-babel-plugin-transform-class-properties@^6.22.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.23.0.tgz#187b747ee404399013563c993db038f34754ac3b"
+babel-plugin-transform-class-properties@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac"
dependencies:
- babel-helper-function-name "^6.23.0"
+ babel-helper-function-name "^6.24.1"
babel-plugin-syntax-class-properties "^6.8.0"
babel-runtime "^6.22.0"
- babel-template "^6.23.0"
+ babel-template "^6.24.1"
-babel-plugin-transform-decorators@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.22.0.tgz#c03635b27a23b23b7224f49232c237a73988d27c"
+babel-plugin-transform-decorators@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d"
dependencies:
- babel-helper-explode-class "^6.22.0"
+ babel-helper-explode-class "^6.24.1"
babel-plugin-syntax-decorators "^6.13.0"
babel-runtime "^6.22.0"
- babel-template "^6.22.0"
- babel-types "^6.22.0"
+ babel-template "^6.24.1"
+ babel-types "^6.24.1"
-babel-plugin-transform-define@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-define/-/babel-plugin-transform-define-1.2.0.tgz#f036bda05162f29a542e434f585da1ccf1e7ec6a"
+babel-plugin-transform-define@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-define/-/babel-plugin-transform-define-1.3.0.tgz#94c5f9459c810c738cc7c50cbd44a31829d6f319"
dependencies:
- lodash.get "4.4.2"
+ lodash "4.17.4"
traverse "0.6.6"
babel-plugin-transform-es2015-arrow-functions@^6.22.0:
@@ -557,36 +628,36 @@ babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-block-scoping@^6.22.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.23.0.tgz#e48895cf0b375be148cd7c8879b422707a053b51"
+babel-plugin-transform-es2015-block-scoping@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f"
dependencies:
- babel-runtime "^6.22.0"
- babel-template "^6.23.0"
- babel-traverse "^6.23.0"
- babel-types "^6.23.0"
- lodash "^4.2.0"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
-babel-plugin-transform-es2015-classes@^6.22.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.23.0.tgz#49b53f326202a2fd1b3bbaa5e2edd8a4f78643c1"
+babel-plugin-transform-es2015-classes@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
dependencies:
- babel-helper-define-map "^6.23.0"
- babel-helper-function-name "^6.23.0"
- babel-helper-optimise-call-expression "^6.23.0"
- babel-helper-replace-supers "^6.23.0"
+ babel-helper-define-map "^6.24.1"
+ babel-helper-function-name "^6.24.1"
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-helper-replace-supers "^6.24.1"
babel-messages "^6.23.0"
babel-runtime "^6.22.0"
- babel-template "^6.23.0"
- babel-traverse "^6.23.0"
- babel-types "^6.23.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
-babel-plugin-transform-es2015-computed-properties@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.22.0.tgz#7c383e9629bba4820c11b0425bdd6290f7f057e7"
+babel-plugin-transform-es2015-computed-properties@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
dependencies:
babel-runtime "^6.22.0"
- babel-template "^6.22.0"
+ babel-template "^6.24.1"
babel-plugin-transform-es2015-destructuring@^6.22.0:
version "6.23.0"
@@ -594,12 +665,12 @@ babel-plugin-transform-es2015-destructuring@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-duplicate-keys@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.22.0.tgz#672397031c21610d72dd2bbb0ba9fb6277e1c36b"
+babel-plugin-transform-es2015-duplicate-keys@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
dependencies:
babel-runtime "^6.22.0"
- babel-types "^6.22.0"
+ babel-types "^6.24.1"
babel-plugin-transform-es2015-for-of@^6.22.0:
version "6.23.0"
@@ -607,13 +678,13 @@ babel-plugin-transform-es2015-for-of@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-function-name@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.22.0.tgz#f5fcc8b09093f9a23c76ac3d9e392c3ec4b77104"
+babel-plugin-transform-es2015-function-name@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
dependencies:
- babel-helper-function-name "^6.22.0"
+ babel-helper-function-name "^6.24.1"
babel-runtime "^6.22.0"
- babel-types "^6.22.0"
+ babel-types "^6.24.1"
babel-plugin-transform-es2015-literals@^6.22.0:
version "6.22.0"
@@ -621,63 +692,63 @@ babel-plugin-transform-es2015-literals@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-modules-amd@^6.24.0:
- version "6.24.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.0.tgz#a1911fb9b7ec7e05a43a63c5995007557bcf6a2e"
+babel-plugin-transform-es2015-modules-amd@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
dependencies:
- babel-plugin-transform-es2015-modules-commonjs "^6.24.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
babel-runtime "^6.22.0"
- babel-template "^6.22.0"
+ babel-template "^6.24.1"
-babel-plugin-transform-es2015-modules-commonjs@^6.24.0:
- version "6.24.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.0.tgz#e921aefb72c2cc26cb03d107626156413222134f"
+babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a"
dependencies:
- babel-plugin-transform-strict-mode "^6.22.0"
- babel-runtime "^6.22.0"
- babel-template "^6.23.0"
- babel-types "^6.23.0"
+ babel-plugin-transform-strict-mode "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-types "^6.26.0"
-babel-plugin-transform-es2015-modules-systemjs@^6.22.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.23.0.tgz#ae3469227ffac39b0310d90fec73bfdc4f6317b0"
+babel-plugin-transform-es2015-modules-systemjs@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
dependencies:
- babel-helper-hoist-variables "^6.22.0"
+ babel-helper-hoist-variables "^6.24.1"
babel-runtime "^6.22.0"
- babel-template "^6.23.0"
+ babel-template "^6.24.1"
-babel-plugin-transform-es2015-modules-umd@^6.24.0:
- version "6.24.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.0.tgz#fd5fa63521cae8d273927c3958afd7c067733450"
+babel-plugin-transform-es2015-modules-umd@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
dependencies:
- babel-plugin-transform-es2015-modules-amd "^6.24.0"
+ babel-plugin-transform-es2015-modules-amd "^6.24.1"
babel-runtime "^6.22.0"
- babel-template "^6.23.0"
+ babel-template "^6.24.1"
-babel-plugin-transform-es2015-object-super@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.22.0.tgz#daa60e114a042ea769dd53fe528fc82311eb98fc"
+babel-plugin-transform-es2015-object-super@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
dependencies:
- babel-helper-replace-supers "^6.22.0"
+ babel-helper-replace-supers "^6.24.1"
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-parameters@^6.22.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.23.0.tgz#3a2aabb70c8af945d5ce386f1a4250625a83ae3b"
+babel-plugin-transform-es2015-parameters@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
dependencies:
- babel-helper-call-delegate "^6.22.0"
- babel-helper-get-function-arity "^6.22.0"
+ babel-helper-call-delegate "^6.24.1"
+ babel-helper-get-function-arity "^6.24.1"
babel-runtime "^6.22.0"
- babel-template "^6.23.0"
- babel-traverse "^6.23.0"
- babel-types "^6.23.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
-babel-plugin-transform-es2015-shorthand-properties@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.22.0.tgz#8ba776e0affaa60bff21e921403b8a652a2ff723"
+babel-plugin-transform-es2015-shorthand-properties@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
dependencies:
babel-runtime "^6.22.0"
- babel-types "^6.22.0"
+ babel-types "^6.24.1"
babel-plugin-transform-es2015-spread@^6.22.0:
version "6.22.0"
@@ -685,13 +756,13 @@ babel-plugin-transform-es2015-spread@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-sticky-regex@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.22.0.tgz#ab316829e866ee3f4b9eb96939757d19a5bc4593"
+babel-plugin-transform-es2015-sticky-regex@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
dependencies:
- babel-helper-regex "^6.22.0"
+ babel-helper-regex "^6.24.1"
babel-runtime "^6.22.0"
- babel-types "^6.22.0"
+ babel-types "^6.24.1"
babel-plugin-transform-es2015-template-literals@^6.22.0:
version "6.22.0"
@@ -705,19 +776,19 @@ babel-plugin-transform-es2015-typeof-symbol@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-unicode-regex@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.22.0.tgz#8d9cc27e7ee1decfe65454fb986452a04a613d20"
+babel-plugin-transform-es2015-unicode-regex@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
dependencies:
- babel-helper-regex "^6.22.0"
+ babel-helper-regex "^6.24.1"
babel-runtime "^6.22.0"
regexpu-core "^2.0.0"
-babel-plugin-transform-exponentiation-operator@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.22.0.tgz#d57c8335281918e54ef053118ce6eb108468084d"
+babel-plugin-transform-exponentiation-operator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e"
dependencies:
- babel-helper-builder-binary-assignment-operator-visitor "^6.22.0"
+ babel-helper-builder-binary-assignment-operator-visitor "^6.24.1"
babel-plugin-syntax-exponentiation-operator "^6.8.0"
babel-runtime "^6.22.0"
@@ -728,143 +799,147 @@ babel-plugin-transform-object-rest-spread@^6.22.0:
babel-plugin-syntax-object-rest-spread "^6.8.0"
babel-runtime "^6.22.0"
-babel-plugin-transform-regenerator@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.22.0.tgz#65740593a319c44522157538d690b84094617ea6"
+babel-plugin-transform-regenerator@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
dependencies:
- regenerator-transform "0.9.8"
+ regenerator-transform "^0.10.0"
-babel-plugin-transform-strict-mode@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.22.0.tgz#e008df01340fdc87e959da65991b7e05970c8c7c"
+babel-plugin-transform-strict-mode@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
dependencies:
babel-runtime "^6.22.0"
- babel-types "^6.22.0"
+ babel-types "^6.24.1"
-babel-preset-es2015@^6.24.0:
- version "6.24.0"
- resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.0.tgz#c162d68b1932696e036cd3110dc1ccd303d2673a"
+babel-preset-es2015@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939"
dependencies:
babel-plugin-check-es2015-constants "^6.22.0"
babel-plugin-transform-es2015-arrow-functions "^6.22.0"
babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
- babel-plugin-transform-es2015-block-scoping "^6.22.0"
- babel-plugin-transform-es2015-classes "^6.22.0"
- babel-plugin-transform-es2015-computed-properties "^6.22.0"
+ babel-plugin-transform-es2015-block-scoping "^6.24.1"
+ babel-plugin-transform-es2015-classes "^6.24.1"
+ babel-plugin-transform-es2015-computed-properties "^6.24.1"
babel-plugin-transform-es2015-destructuring "^6.22.0"
- babel-plugin-transform-es2015-duplicate-keys "^6.22.0"
+ babel-plugin-transform-es2015-duplicate-keys "^6.24.1"
babel-plugin-transform-es2015-for-of "^6.22.0"
- babel-plugin-transform-es2015-function-name "^6.22.0"
+ babel-plugin-transform-es2015-function-name "^6.24.1"
babel-plugin-transform-es2015-literals "^6.22.0"
- babel-plugin-transform-es2015-modules-amd "^6.24.0"
- babel-plugin-transform-es2015-modules-commonjs "^6.24.0"
- babel-plugin-transform-es2015-modules-systemjs "^6.22.0"
- babel-plugin-transform-es2015-modules-umd "^6.24.0"
- babel-plugin-transform-es2015-object-super "^6.22.0"
- babel-plugin-transform-es2015-parameters "^6.22.0"
- babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.24.1"
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+ babel-plugin-transform-es2015-modules-systemjs "^6.24.1"
+ babel-plugin-transform-es2015-modules-umd "^6.24.1"
+ babel-plugin-transform-es2015-object-super "^6.24.1"
+ babel-plugin-transform-es2015-parameters "^6.24.1"
+ babel-plugin-transform-es2015-shorthand-properties "^6.24.1"
babel-plugin-transform-es2015-spread "^6.22.0"
- babel-plugin-transform-es2015-sticky-regex "^6.22.0"
+ babel-plugin-transform-es2015-sticky-regex "^6.24.1"
babel-plugin-transform-es2015-template-literals "^6.22.0"
babel-plugin-transform-es2015-typeof-symbol "^6.22.0"
- babel-plugin-transform-es2015-unicode-regex "^6.22.0"
- babel-plugin-transform-regenerator "^6.22.0"
+ babel-plugin-transform-es2015-unicode-regex "^6.24.1"
+ babel-plugin-transform-regenerator "^6.24.1"
-babel-preset-es2016@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-preset-es2016/-/babel-preset-es2016-6.22.0.tgz#b061aaa3983d40c9fbacfa3743b5df37f336156c"
+babel-preset-es2016@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2016/-/babel-preset-es2016-6.24.1.tgz#f900bf93e2ebc0d276df9b8ab59724ebfd959f8b"
dependencies:
- babel-plugin-transform-exponentiation-operator "^6.22.0"
+ babel-plugin-transform-exponentiation-operator "^6.24.1"
-babel-preset-es2017@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-preset-es2017/-/babel-preset-es2017-6.22.0.tgz#de2f9da5a30c50d293fb54a0ba15d6ddc573f0f2"
+babel-preset-es2017@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2017/-/babel-preset-es2017-6.24.1.tgz#597beadfb9f7f208bcfd8a12e9b2b29b8b2f14d1"
dependencies:
babel-plugin-syntax-trailing-function-commas "^6.22.0"
- babel-plugin-transform-async-to-generator "^6.22.0"
+ babel-plugin-transform-async-to-generator "^6.24.1"
-babel-preset-latest@^6.24.0:
- version "6.24.0"
- resolved "https://registry.yarnpkg.com/babel-preset-latest/-/babel-preset-latest-6.24.0.tgz#a68d20f509edcc5d7433a48dfaebf7e4f2cd4cb7"
+babel-preset-latest@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-latest/-/babel-preset-latest-6.24.1.tgz#677de069154a7485c2d25c577c02f624b85b85e8"
dependencies:
- babel-preset-es2015 "^6.24.0"
- babel-preset-es2016 "^6.22.0"
- babel-preset-es2017 "^6.22.0"
+ babel-preset-es2015 "^6.24.1"
+ babel-preset-es2016 "^6.24.1"
+ babel-preset-es2017 "^6.24.1"
-babel-preset-stage-2@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.22.0.tgz#ccd565f19c245cade394b21216df704a73b27c07"
+babel-preset-stage-2@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1"
dependencies:
babel-plugin-syntax-dynamic-import "^6.18.0"
- babel-plugin-transform-class-properties "^6.22.0"
- babel-plugin-transform-decorators "^6.22.0"
- babel-preset-stage-3 "^6.22.0"
+ babel-plugin-transform-class-properties "^6.24.1"
+ babel-plugin-transform-decorators "^6.24.1"
+ babel-preset-stage-3 "^6.24.1"
-babel-preset-stage-3@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.22.0.tgz#a4e92bbace7456fafdf651d7a7657ee0bbca9c2e"
+babel-preset-stage-3@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz#836ada0a9e7a7fa37cb138fb9326f87934a48395"
dependencies:
babel-plugin-syntax-trailing-function-commas "^6.22.0"
- babel-plugin-transform-async-generator-functions "^6.22.0"
- babel-plugin-transform-async-to-generator "^6.22.0"
- babel-plugin-transform-exponentiation-operator "^6.22.0"
+ babel-plugin-transform-async-generator-functions "^6.24.1"
+ babel-plugin-transform-async-to-generator "^6.24.1"
+ babel-plugin-transform-exponentiation-operator "^6.24.1"
babel-plugin-transform-object-rest-spread "^6.22.0"
-babel-register@^6.23.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.23.0.tgz#c9aa3d4cca94b51da34826c4a0f9e08145d74ff3"
+babel-register@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
dependencies:
- babel-core "^6.23.0"
- babel-runtime "^6.22.0"
- core-js "^2.4.0"
+ babel-core "^6.26.0"
+ babel-runtime "^6.26.0"
+ core-js "^2.5.0"
home-or-tmp "^2.0.0"
- lodash "^4.2.0"
+ lodash "^4.17.4"
mkdirp "^0.5.1"
- source-map-support "^0.4.2"
+ source-map-support "^0.4.15"
-babel-runtime@^6.18.0, babel-runtime@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611"
+babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
dependencies:
core-js "^2.4.0"
- regenerator-runtime "^0.10.0"
+ regenerator-runtime "^0.11.0"
-babel-template@^6.16.0, babel-template@^6.22.0, babel-template@^6.23.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.23.0.tgz#04d4f270adbb3aa704a8143ae26faa529238e638"
+babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
dependencies:
- babel-runtime "^6.22.0"
- babel-traverse "^6.23.0"
- babel-types "^6.23.0"
- babylon "^6.11.0"
- lodash "^4.2.0"
+ babel-runtime "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ lodash "^4.17.4"
-babel-traverse@^6.18.0, babel-traverse@^6.22.0, babel-traverse@^6.23.0, babel-traverse@^6.23.1:
- version "6.23.1"
- resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.23.1.tgz#d3cb59010ecd06a97d81310065f966b699e14f48"
+babel-traverse@^6.18.0, babel-traverse@^6.24.1, babel-traverse@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
dependencies:
- babel-code-frame "^6.22.0"
+ babel-code-frame "^6.26.0"
babel-messages "^6.23.0"
- babel-runtime "^6.22.0"
- babel-types "^6.23.0"
- babylon "^6.15.0"
- debug "^2.2.0"
- globals "^9.0.0"
- invariant "^2.2.0"
- lodash "^4.2.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ debug "^2.6.8"
+ globals "^9.18.0"
+ invariant "^2.2.2"
+ lodash "^4.17.4"
-babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.23.0.tgz#bb17179d7538bad38cd0c9e115d340f77e7e9acf"
+babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
dependencies:
- babel-runtime "^6.22.0"
+ babel-runtime "^6.26.0"
esutils "^2.0.2"
- lodash "^4.2.0"
- to-fast-properties "^1.0.1"
+ lodash "^4.17.4"
+ to-fast-properties "^1.0.3"
+
+babylon@7.0.0-beta.32, babylon@^7.0.0-beta.31:
+ version "7.0.0-beta.32"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.32.tgz#e9033cb077f64d6895f4125968b37dc0a8c3bc6e"
-babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0, babylon@^6.16.1:
- version "6.16.1"
- resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3"
+babylon@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
backo2@1.0.2:
version "1.0.2"
@@ -914,6 +989,17 @@ binary-extensions@^1.0.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0"
+blackst0ne-mermaid@^7.1.0-fixed:
+ version "7.1.0-fixed"
+ resolved "https://registry.yarnpkg.com/blackst0ne-mermaid/-/blackst0ne-mermaid-7.1.0-fixed.tgz#3707b3a113d78610e3068e18a588f46b4688de49"
+ dependencies:
+ d3 "3.5.17"
+ dagre-d3-renderer "^0.4.24"
+ dagre-layout "^0.8.0"
+ he "^1.1.1"
+ lodash "^4.17.4"
+ moment "^2.18.1"
+
blob@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
@@ -928,11 +1014,7 @@ bluebird@^2.10.2:
version "2.11.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
-bluebird@^3.0.5, bluebird@^3.1.1:
- version "3.4.7"
- resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
-
-bluebird@^3.3.0:
+bluebird@^3.1.1, bluebird@^3.3.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
@@ -1063,10 +1145,6 @@ buffer-indexof@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.0.tgz#f54f647c4f4e25228baa656a2e57e43d5f270982"
-buffer-shims@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51"
-
buffer-xor@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
@@ -1156,7 +1234,7 @@ center-align@^0.1.1:
align-text "^0.1.3"
lazy-cache "^1.0.3"
-chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
+chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
dependencies:
@@ -1166,6 +1244,14 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
+chalk@^2.0.0, chalk@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
+ dependencies:
+ ansi-styles "^3.1.0"
+ escape-string-regexp "^1.0.5"
+ supports-color "^4.0.0"
+
chokidar@^1.4.1, chokidar@^1.4.3, chokidar@^1.6.0, chokidar@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
@@ -1197,6 +1283,10 @@ clap@^1.0.9:
dependencies:
chalk "^1.1.3"
+classlist-polyfill@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz#935bc2dfd9458a876b279617514638bcaa964a2e"
+
cli-cursor@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
@@ -1207,13 +1297,13 @@ cli-width@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
-clipboard@^1.5.5:
- version "1.6.1"
- resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.6.1.tgz#65c5b654812466b0faab82dc6ba0f1d2f8e4be53"
+clipboard@^1.5.5, clipboard@^1.7.1:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b"
dependencies:
- good-listener "^1.2.0"
+ good-listener "^1.2.2"
select "^1.1.2"
- tiny-emitter "^1.0.0"
+ tiny-emitter "^2.0.0"
cliui@^2.1.0:
version "2.1.0"
@@ -1249,9 +1339,9 @@ code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
-color-convert@^1.3.0:
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
+color-convert@^1.3.0, color-convert@^1.9.0:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
dependencies:
color-name "^1.1.1"
@@ -1360,13 +1450,6 @@ concat-stream@^1.5.2:
readable-stream "^2.2.2"
typedarray "^0.0.6"
-config-chain@~1.1.5:
- version "1.1.11"
- resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2"
- dependencies:
- ini "^1.3.4"
- proto-list "~1.2.1"
-
configstore@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/configstore/-/configstore-1.4.0.tgz#c35781d0501d268c25c54b8b17f6240e8a4fb021"
@@ -1425,9 +1508,9 @@ content-type@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
-convert-source-map@^1.1.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.3.0.tgz#e9f3e9c6e2728efc2676696a70eb382f73106a67"
+convert-source-map@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
cookie-signature@1.0.6:
version "1.0.6"
@@ -1450,14 +1533,14 @@ copy-webpack-plugin@^4.0.1:
minimatch "^3.0.0"
node-dir "^0.1.10"
-core-js@^2.2.0:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086"
-
-core-js@^2.4.0, core-js@^2.4.1:
+core-js@^2.2.0, core-js@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
+core-js@^2.4.0, core-js@^2.5.0:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b"
+
core-js@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65"
@@ -1630,6 +1713,10 @@ custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
+d3@3.5.17:
+ version "3.5.17"
+ resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
+
d3@^3.5.11:
version "3.5.11"
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c"
@@ -1646,6 +1733,22 @@ d@^0.1.1:
dependencies:
es5-ext "~0.10.2"
+dagre-d3-renderer@^0.4.24:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/dagre-d3-renderer/-/dagre-d3-renderer-0.4.24.tgz#b36ce2fe4ea20de43e7698627c6ede2a9f15ec45"
+ dependencies:
+ d3 "3.5.17"
+ dagre-layout "^0.8.0"
+ graphlib "^2.1.1"
+ lodash "^4.17.4"
+
+dagre-layout@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/dagre-layout/-/dagre-layout-0.8.0.tgz#7147b6afb655602f855158dfea171db9aa98d4ff"
+ dependencies:
+ graphlib "^2.1.1"
+ lodash "^4.17.4"
+
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -1678,12 +1781,18 @@ debug@2.6.7:
dependencies:
ms "2.0.0"
-debug@2.6.8, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0, debug@^2.4.5, debug@^2.6.6, debug@^2.6.8:
+debug@2.6.8, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0, debug@^2.6.6, debug@^2.6.8:
version "2.6.8"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
dependencies:
ms "2.0.0"
+debug@^3.0.1, debug@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+ dependencies:
+ ms "2.0.0"
+
decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -1795,6 +1904,10 @@ di@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
+diff@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
+
diffie-hellman@^5.0.0:
version "5.0.2"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
@@ -1906,15 +2019,6 @@ ecc-jsbn@~0.1.1:
dependencies:
jsbn "~0.1.0"
-editorconfig@^0.13.2:
- version "0.13.2"
- resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.13.2.tgz#8e57926d9ee69ab6cb999f027c2171467acceb35"
- dependencies:
- bluebird "^3.0.5"
- commander "^2.9.0"
- lru-cache "^3.2.0"
- sigmund "^1.0.1"
-
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -2074,11 +2178,7 @@ es6-map@^0.1.3:
es6-symbol "~3.1.1"
event-emitter "~0.3.5"
-es6-promise@^3.0.2:
- version "3.3.1"
- resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
-
-es6-promise@~3.0.2:
+es6-promise@^3.0.2, es6-promise@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6"
@@ -2259,10 +2359,6 @@ esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1:
version "2.7.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
-esprima@^3.1.1:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
-
esprima@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
@@ -2444,6 +2540,10 @@ fast-deep-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
+fast-json-stable-stringify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
+
fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
@@ -2569,11 +2669,11 @@ flatten@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
-follow-redirects@^1.2.3:
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.3.tgz#01abaeca85e3609837d9fcda3167a7e42fdaca21"
+follow-redirects@^1.2.5:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.6.tgz#4dcdc7e4ab3dd6765a97ff89c3b4c258117c79bf"
dependencies:
- debug "^2.4.5"
+ debug "^3.1.0"
for-each@~0.3.2:
version "0.3.2"
@@ -2663,14 +2763,18 @@ fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
mkdirp ">=0.5 0"
rimraf "2"
-function-bind@^1.0.2:
+function-bind@^1.0.2, function-bind@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
-function-bind@^1.1.1, function-bind@~1.1.0:
+function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+fuzzaldrin-plus@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/fuzzaldrin-plus/-/fuzzaldrin-plus-0.5.0.tgz#ef5f26f0c2fc7e9e9a16ea149a802d6cb4804b1e"
+
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -2712,10 +2816,6 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
-"gitlab-svgs@https://gitlab.com/gitlab-org/gitlab-svgs.git":
- version "1.0.2"
- resolved "https://gitlab.com/gitlab-org/gitlab-svgs.git#e7621d7b028607ae9c69f8b496cd49b42fe607e4"
-
glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -2749,29 +2849,33 @@ glob@^6.0.4:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.0, glob@^7.1.1, glob@~7.1.2:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
- minimatch "^3.0.4"
+ minimatch "^3.0.2"
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.3, glob@^7.0.5:
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
+glob@~7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
- minimatch "^3.0.2"
+ minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
-globals@^9.0.0, globals@^9.14.0:
+globals@^10.0.0:
+ version "10.4.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-10.4.0.tgz#5c477388b128a9e4c5c5d01c7a2aca68c68b2da7"
+
+globals@^9.14.0, globals@^9.18.0:
version "9.18.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
@@ -2796,7 +2900,7 @@ globby@^6.1.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
-good-listener@^1.2.0:
+good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
dependencies:
@@ -2844,6 +2948,12 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "1.0.1"
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
+graphlib@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.1.tgz#42352c52ba2f4d035cb566eb91f7395f76ebc951"
+ dependencies:
+ lodash "^4.11.1"
+
gzip-size@3.0.0, gzip-size@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520"
@@ -2938,7 +3048,7 @@ hawk@~3.1.3:
hoek "2.x.x"
sntp "1.x.x"
-he@^1.1.0:
+he@^1.1.0, he@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
@@ -2970,14 +3080,10 @@ html-comment-regex@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
-html-entities@1.2.0:
+html-entities@1.2.0, html-entities@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2"
-html-entities@^1.2.0:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
-
htmlparser2@^3.8.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
@@ -3107,7 +3213,7 @@ inherits@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
-ini@^1.3.4, ini@~1.3.0:
+ini@~1.3.0:
version "1.3.4"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
@@ -3139,7 +3245,7 @@ interpret@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
-invariant@^2.2.0:
+invariant@^2.2.0, invariant@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
dependencies:
@@ -3383,10 +3489,6 @@ isexe@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0"
-isexe@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
-
isobject@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
@@ -3413,9 +3515,9 @@ istanbul-api@^1.1.1:
mkdirp "^0.5.1"
once "^1.4.0"
-istanbul-lib-coverage@^1.0.0, istanbul-lib-coverage@^1.0.0-alpha, istanbul-lib-coverage@^1.0.0-alpha.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.1.tgz#f263efb519c051c5f1f3343034fc40e7b43ff212"
+istanbul-lib-coverage@^1.0.0, istanbul-lib-coverage@^1.0.0-alpha, istanbul-lib-coverage@^1.0.0-alpha.0, istanbul-lib-coverage@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da"
istanbul-lib-hook@^1.0.0:
version "1.0.0"
@@ -3423,16 +3525,16 @@ istanbul-lib-hook@^1.0.0:
dependencies:
append-transform "^0.4.0"
-istanbul-lib-instrument@^1.3.0, istanbul-lib-instrument@^1.4.2:
- version "1.4.2"
- resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.4.2.tgz#0e2fdfac93c1dabf2e31578637dc78a19089f43e"
+istanbul-lib-instrument@^1.3.0, istanbul-lib-instrument@^1.7.5:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.1.tgz#250b30b3531e5d3251299fdd64b0b2c9db6b558e"
dependencies:
babel-generator "^6.18.0"
babel-template "^6.16.0"
babel-traverse "^6.18.0"
babel-types "^6.18.0"
- babylon "^6.13.0"
- istanbul-lib-coverage "^1.0.0"
+ babylon "^6.18.0"
+ istanbul-lib-coverage "^1.1.1"
semver "^5.3.0"
istanbul-lib-report@^1.0.0-alpha.3:
@@ -3499,49 +3601,29 @@ jed@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4"
-jquery-ujs@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.1.tgz#6ee75b1ef4e9ac95e7124f8d71f7d351f5548e92"
+jquery-ujs@1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.2.tgz#6a8ef1020e6b6dda385b90a4bddc128c21c56397"
dependencies:
jquery ">=1.8.0"
-"jquery@>= 1.9.1", jquery@>=1.8.0, jquery@^2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f"
+"jquery@>= 1.9.1", jquery@>=1.8.0, jquery@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.4.tgz#2c89d6889b5eac522a7eea32c14521559c6cbf02"
js-base64@^2.1.9:
version "2.1.9"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce"
-js-beautify@^1.6.3:
- version "1.6.12"
- resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.6.12.tgz#78b75933505d376da6e5a28e9b7887e0094db8b5"
- dependencies:
- config-chain "~1.1.5"
- editorconfig "^0.13.2"
- mkdirp "~0.5.0"
- nopt "~3.0.1"
-
js-cookie@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.1.3.tgz#48071625217ac9ecfab8c343a13d42ec09ff0526"
-js-tokens@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
-
-js-tokens@^3.0.2:
+js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
-js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.7.0:
- version "3.8.1"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628"
- dependencies:
- argparse "^1.0.7"
- esprima "^3.1.1"
-
-js-yaml@^3.5.1:
+js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0:
version "3.9.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0"
dependencies:
@@ -3778,7 +3860,7 @@ loader-utils@^0.2.15, loader-utils@^0.2.5:
json5 "^0.5.0"
object-assign "^4.0.1"
-loader-utils@^1.0.2, loader-utils@^1.1.0:
+loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
dependencies:
@@ -3873,10 +3955,6 @@ lodash.defaults@^3.1.2:
lodash.assign "^3.0.0"
lodash.restparam "^3.0.0"
-lodash.get@4.4.2:
- version "4.4.2"
- resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
-
lodash.get@^3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-3.7.0.tgz#3ce68ae2c91683b281cc5394128303cbf75e691f"
@@ -3930,14 +4008,14 @@ lodash.words@^4.0.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.words/-/lodash.words-4.2.0.tgz#5ecfeaf8ecf8acaa8e0c8386295f1993c9cf4036"
+lodash@4.17.4, lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
+ version "4.17.4"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+
lodash@^3.8.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
-lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
- version "4.17.4"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
-
log4js@^0.6.31:
version "0.6.38"
resolved "https://registry.yarnpkg.com/log4js/-/log4js-0.6.38.tgz#2c494116695d6fb25480943d3fc872e662a522fd"
@@ -3974,18 +4052,12 @@ lru-cache@2.2.x:
version "2.2.4"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d"
-lru-cache@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-3.2.0.tgz#71789b3b7f5399bec8565dda38aa30d2a097efee"
- dependencies:
- pseudomap "^1.0.1"
-
-lru-cache@^4.0.1:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e"
+lru-cache@^4.0.1, lru-cache@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
dependencies:
- pseudomap "^1.0.1"
- yallist "^2.0.0"
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
macaddress@^0.2.8:
version "0.2.8"
@@ -4082,7 +4154,7 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-"mime-db@>= 1.29.0 < 2", mime-db@~1.29.0:
+"mime-db@>= 1.29.0 < 2":
version "1.29.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878"
@@ -4090,13 +4162,7 @@ mime-db@~1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
-mime-types@^2.1.12, mime-types@~2.1.7:
- version "2.1.16"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23"
- dependencies:
- mime-db "~1.29.0"
-
-mime-types@~2.1.11, mime-types@~2.1.15:
+mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7:
version "2.1.15"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
dependencies:
@@ -4118,18 +4184,18 @@ minimalistic-assert@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
-"minimatch@2 || 3", minimatch@3.0.3, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
- dependencies:
- brace-expansion "^1.0.0"
-
-minimatch@^3.0.4:
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
dependencies:
brace-expansion "^1.1.7"
+minimatch@3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
+ dependencies:
+ brace-expansion "^1.0.0"
+
minimist@0.0.8, minimist@~0.0.1:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@@ -4144,13 +4210,13 @@ mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkd
dependencies:
minimist "0.0.8"
-moment@2.x:
- version "2.17.1"
- resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82"
+moment@2.x, moment@^2.18.1:
+ version "2.19.2"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe"
-monaco-editor@0.8.3:
- version "0.8.3"
- resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.8.3.tgz#523bdf2d1524db2c2dfc3cae0a7b6edc48d6dea6"
+monaco-editor@0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.10.0.tgz#6604932585fe9c1f993f000a503d0d20fbe5896a"
mousetrap@^1.4.6:
version "1.4.6"
@@ -4301,7 +4367,7 @@ nodemon@^1.11.0:
undefsafe "0.0.3"
update-notifier "0.5.0"
-nopt@3.x, nopt@~3.0.1:
+nopt@3.x:
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
dependencies:
@@ -4320,16 +4386,7 @@ nopt@~1.0.10:
dependencies:
abbrev "1"
-normalize-package-data@^2.3.2:
- version "2.3.5"
- resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df"
- dependencies:
- hosted-git-info "^2.1.4"
- is-builtin-module "^1.0.0"
- semver "2 || 3 || 4 || 5"
- validate-npm-package-license "^3.0.1"
-
-normalize-package-data@^2.3.4:
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
version "2.4.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
dependencies:
@@ -4504,7 +4561,7 @@ os-locale@^2.0.0:
lcid "^1.0.0"
mem "^1.1.0"
-os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2:
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@@ -4619,7 +4676,7 @@ path-exists@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
-path-is-absolute@^1.0.0:
+path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
@@ -4677,9 +4734,9 @@ pify@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
-pikaday@^1.5.1:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/pikaday/-/pikaday-1.5.1.tgz#0a48549bc1a14ea1d08c44074d761bc2f2bfcfd3"
+pikaday@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/pikaday/-/pikaday-1.6.1.tgz#b91bcb9b8539cedd8d6d08e4e7465e12095671b0"
optionalDependencies:
moment "2.x"
@@ -4975,7 +5032,7 @@ postcss-zindex@^2.0.1:
postcss "^5.0.4"
uniqs "^2.0.0"
-postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.21, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16:
+postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16:
version "5.2.16"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.16.tgz#732b3100000f9ff8379a48a53839ed097376ad57"
dependencies:
@@ -4984,6 +5041,14 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
source-map "^0.5.6"
supports-color "^3.2.3"
+postcss@^6.0.8:
+ version "6.0.14"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.14.tgz#5534c72114739e75d0afcf017db853099f562885"
+ dependencies:
+ chalk "^2.3.0"
+ source-map "^0.6.1"
+ supports-color "^4.4.0"
+
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -4996,15 +5061,19 @@ preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+prettier@^1.7.0:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.2.tgz#bff83e7fd573933c607875e5ba3abbdffb96aeb8"
+
prismjs@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.6.0.tgz#118d95fb7a66dba2272e343b345f5236659db365"
optionalDependencies:
clipboard "^1.5.5"
-private@^0.1.6:
- version "0.1.7"
- resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
+private@^0.1.6, private@^0.1.7:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
process-nextick-args@~1.0.6:
version "1.0.7"
@@ -5018,10 +5087,6 @@ progress@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
-proto-list@~1.2.1:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
-
proxy-addr@~1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918"
@@ -5039,7 +5104,7 @@ ps-tree@^1.0.1:
dependencies:
event-stream "~3.3.0"
-pseudomap@^1.0.1:
+pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
@@ -5200,7 +5265,7 @@ read-pkg@^2.0.0:
normalize-package-data "^2.3.2"
path-type "^2.0.0"
-readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.9:
+readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.0, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.9:
version "2.3.3"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
dependencies:
@@ -5223,18 +5288,6 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable
string_decoder "~0.10.x"
util-deprecate "~1.0.1"
-readable-stream@^2.1.0:
- version "2.2.2"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e"
- dependencies:
- buffer-shims "^1.0.0"
- core-util-is "~1.0.0"
- inherits "~2.0.1"
- isarray "~1.0.0"
- process-nextick-args "~1.0.6"
- string_decoder "~0.10.x"
- util-deprecate "~1.0.1"
-
readable-stream@~1.0.2:
version "1.0.34"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
@@ -5298,13 +5351,13 @@ regenerate@^1.2.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260"
-regenerator-runtime@^0.10.0:
- version "0.10.1"
- resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz#257f41961ce44558b18f7814af48c17559f9faeb"
+regenerator-runtime@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1"
-regenerator-transform@0.9.8:
- version "0.9.8"
- resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.8.tgz#0f88bb2bc03932ddb7b6b7312e68078f01026d6c"
+regenerator-transform@^0.10.0:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
dependencies:
babel-runtime "^6.18.0"
babel-types "^6.19.0"
@@ -5435,9 +5488,11 @@ resolve@1.1.x:
version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
-resolve@^1.1.6, resolve@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c"
+resolve@^1.1.6, resolve@^1.2.0, resolve@^1.4.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
+ dependencies:
+ path-parse "^1.0.5"
resolve@~1.4.0:
version "1.4.0"
@@ -5496,6 +5551,12 @@ sax@~1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828"
+schema-utils@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf"
+ dependencies:
+ ajv "^5.0.0"
+
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -5520,14 +5581,10 @@ semver-diff@^2.0.0:
dependencies:
semver "^5.0.3"
-"semver@2 || 3 || 4 || 5", semver@^5.3.0:
+"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
-semver@^5.0.3:
- version "5.4.1"
- resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
-
semver@~4.3.3:
version "4.3.6"
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
@@ -5611,10 +5668,6 @@ shelljs@^0.7.5:
interpret "^1.0.0"
rechoir "^0.6.2"
-sigmund@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
-
signal-exit@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
@@ -5724,13 +5777,13 @@ source-list-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
-source-map-support@^0.4.2:
- version "0.4.11"
- resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.11.tgz#647f939978b38535909530885303daf23279f322"
+source-map-support@^0.4.15:
+ version "0.4.18"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
dependencies:
- source-map "^0.5.3"
+ source-map "^0.5.6"
-source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3:
+source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
@@ -5746,6 +5799,10 @@ source-map@^0.4.4:
dependencies:
amdefine ">=0.0.4"
+source-map@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+
source-map@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d"
@@ -5935,6 +5992,12 @@ supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.1.2, supports-co
dependencies:
has-flag "^1.0.0"
+supports-color@^4.0.0, supports-color@^4.4.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b"
+ dependencies:
+ has-flag "^2.0.0"
+
supports-color@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836"
@@ -6015,9 +6078,9 @@ tar@^2.2.1:
fstream "^1.0.2"
inherits "2"
-test-exclude@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.0.0.tgz#0ddc0100b8ae7e88b34eb4fd98a907e961991900"
+test-exclude@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26"
dependencies:
arrify "^1.0.1"
micromatch "^2.3.11"
@@ -6049,13 +6112,11 @@ thunky@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e"
-time-stamp@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357"
-
-timeago.js@^2.0.5:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-2.0.5.tgz#730c74fbdb0b0917a553675a4460e3a7f80db86c"
+timeago.js@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-3.0.2.tgz#32a67e7c0d887ea42ca588d3aae26f77de5e76cc"
+ dependencies:
+ "@types/jquery" "^2.0.40"
timed-out@^2.0.0:
version "2.0.0"
@@ -6077,22 +6138,16 @@ timers-browserify@^2.0.2:
dependencies:
setimmediate "^1.0.4"
-tiny-emitter@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb"
+tiny-emitter@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"
-tmp@0.0.31:
+tmp@0.0.31, tmp@0.0.x:
version "0.0.31"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
dependencies:
os-tmpdir "~1.0.1"
-tmp@0.0.x:
- version "0.0.33"
- resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
- dependencies:
- os-tmpdir "~1.0.2"
-
to-array@0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
@@ -6101,9 +6156,13 @@ to-arraybuffer@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
-to-fast-properties@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320"
+to-fast-properties@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+
+to-fast-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
touch@1.0.0:
version "1.0.0"
@@ -6356,26 +6415,27 @@ void-elements@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
-vue-hot-reload-api@^2.0.11:
- version "2.0.11"
- resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.0.11.tgz#bf26374fb73366ce03f799e65ef5dfd0e28a1568"
+vue-hot-reload-api@^2.2.0:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.2.4.tgz#683bd1d026c0d3b3c937d5875679e9a87ec6cd8f"
-vue-loader@^11.3.4:
- version "11.3.4"
- resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-11.3.4.tgz#65e10a44ce092d906e14bbc72981dec99eb090d2"
+vue-loader@^13.5.0:
+ version "13.5.0"
+ resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-13.5.0.tgz#52f7b3790a267eff80012b77ea187a54586dd5d4"
dependencies:
consolidate "^0.14.0"
hash-sum "^1.0.2"
- js-beautify "^1.6.3"
loader-utils "^1.1.0"
- lru-cache "^4.0.1"
- postcss "^5.0.21"
+ lru-cache "^4.1.1"
+ postcss "^6.0.8"
postcss-load-config "^1.1.0"
postcss-selector-parser "^2.0.0"
- source-map "^0.5.6"
- vue-hot-reload-api "^2.0.11"
- vue-style-loader "^2.0.0"
- vue-template-es2015-compiler "^1.2.2"
+ prettier "^1.7.0"
+ resolve "^1.4.0"
+ source-map "^0.6.1"
+ vue-hot-reload-api "^2.2.0"
+ vue-style-loader "^3.0.0"
+ vue-template-es2015-compiler "^1.6.0"
vue-resource@^1.3.4:
version "1.3.4"
@@ -6383,31 +6443,31 @@ vue-resource@^1.3.4:
dependencies:
got "^7.0.0"
-vue-style-loader@^2.0.0:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-2.0.5.tgz#f0efac992febe3f12e493e334edb13cd235a3d22"
+vue-style-loader@^3.0.0:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-3.0.3.tgz#623658f81506aef9d121cdc113a4f5c9cac32df7"
dependencies:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
-vue-template-compiler@^2.2.6:
- version "2.2.6"
- resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.2.6.tgz#2e2928daf0cd0feca9dfc35a9729adeae173ec68"
+vue-template-compiler@^2.5.8:
+ version "2.5.8"
+ resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.8.tgz#826ae77e1d5faa7fa5fca554f33872dde38de674"
dependencies:
de-indent "^1.0.2"
he "^1.1.0"
-vue-template-es2015-compiler@^1.2.2:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.5.1.tgz#0c36cc57aa3a9ec13e846342cb14a72fcac8bd93"
+vue-template-es2015-compiler@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18"
-vue@^2.2.6:
- version "2.2.6"
- resolved "https://registry.yarnpkg.com/vue/-/vue-2.2.6.tgz#451714b394dd6d4eae7b773c40c2034a59621aed"
+vue@^2.5.8:
+ version "2.5.8"
+ resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.8.tgz#f855c1c27255184a82225f4bef225473e8faf15b"
-vuex@^2.3.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/vuex/-/vuex-2.3.1.tgz#cde8e997c1f9957719bc7dea154f9aa691d981a6"
+vuex@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2"
watchpack@^1.4.0:
version "1.4.0"
@@ -6439,7 +6499,7 @@ webpack-bundle-analyzer@^2.8.2:
opener "^1.4.3"
ws "^2.3.1"
-webpack-dev-middleware@^1.0.11:
+webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9"
dependencies:
@@ -6448,16 +6508,6 @@ webpack-dev-middleware@^1.0.11:
path-is-absolute "^1.0.0"
range-parser "^1.0.3"
-webpack-dev-middleware@^1.11.0:
- version "1.12.0"
- resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz#d34efefb2edda7e1d3b5dbe07289513219651709"
- dependencies:
- memory-fs "~0.4.1"
- mime "^1.3.4"
- path-is-absolute "^1.0.0"
- range-parser "^1.0.3"
- time-stamp "^2.0.0"
-
webpack-dev-server@^2.6.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.7.1.tgz#21580f5a08cd065c71144cf6f61c345bca59a8b8"
@@ -6546,18 +6596,12 @@ which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
-which@^1.1.1, which@^1.2.1:
+which@^1.1.1, which@^1.2.1, which@^1.2.9:
version "1.2.12"
resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192"
dependencies:
isexe "^1.1.1"
-which@^1.2.9:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
- dependencies:
- isexe "^2.0.0"
-
wide-align@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
@@ -6580,6 +6624,13 @@ wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+worker-loader@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-1.1.0.tgz#8cf21869a07add84d66f821d948d23c1eb98e809"
+ dependencies:
+ loader-utils "^1.0.0"
+ schema-utils "^0.3.0"
+
wrap-ansi@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
@@ -6641,7 +6692,7 @@ y18n@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
-yallist@^2.0.0:
+yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"